How to store data of a functional chain of Monoidal List?
Asked Answered
E

2

1

This is an advanced topic of my prior question here:

How to store data of a functional chain?

The brief idea is

A simple function below:

const L = a => L;

forms

L
L(1)
L(1)(2)
...

This seems to form a list but the actual data is not stored at all, so if it's required to store the data such as [1,2], what is the smartest practice to have the task done?

One of the prominent ideas is from @user633183 which I marked as an accepted answer(see the Question link), and another version of the curried function is also provided by @Matías Fidemraizer .

So here goes:

const L = a => {
  const m = list => x => !x
    ? list
    : m([...list, x]);
  return m([])(a);
}; 

const list1 = (L)(1)(2)(3); //lazy : no data evaluation here
const list2 = (L)(4)(5)(6);

console.log(list1()) // now evaluated by the tail ()
console.log(list2())  

What I really like is it turns out lazy evaluation.

Although the given approach satisfies what I mentioned, this function has lost the outer structure or I must mentiion:

Algebraic structure

 const L = a => L;

which forms list and more fundamentally gives us an algebraic structure of identity element, potentially along with Monoid or Magma.

Left an Right identity

One of the easiest examples of Monoids and identity is number and "Strings" and [Array] in JavaScript.

0 + a === a === a + 0
1 * a === a === a * 1

In Strings, the empty quoate "" is the identity element.

  "" + "Hello world" === "Hello world" === "Hello world" + ""

Same goes to [Array].

Same goes to L:

(L)(a) === (a) === (a)(L)

const L = a => L;

const a = L(5); // number is wrapped or "lift" to Type:L
                // Similarity of String and Array
                // "5"  [5]

//left identity
console.log(
  (L)(a) === (a)    //true 
);
 
//right identity
console.log(
  (a) === (a)(L)    //true
); 

and the obvious identity immutability:

const L = a => L;
 
console.log(
  (L)(L) === (L)    //true
); 
console.log(
  (L)(L)(L) === (L)    //true
); 
console.log(
  (L)(L)(L)(L) === (L)    //true
); 

Also the below:

const L = a => L;

const a = (L)(1)(2)(3);
const b = (L)(1)(L)(2)(3)(L);

 
console.log(
   (a) === (b)    //true 
);
 

Questions

What is the smartest or most elegant way (very functional and no mutations (no Array.push, also)) to implement L that satisfies 3 requirements:

Requirement 0 - Identity

A simple function:

const L = a => L;

already satisfies the identity law as we already have seen.

Requirement 1 - eval() method

Although L satisfies the identity law, there is no method to access to the listed/accumulated data.

(Answers provided in my previous question provide the data accumulation ability, but breaks the Identity law.)

Lazy evaluation seems the correct approach, so providing a clearer specification:

provide eval method of L

const L = a => L; // needs to enhance to satisfy the requirements

const a = (L)(1)(2)(3);
const b = (L)(1)(L)(2)(3)(L);


console.log(
   (a) === (b)    //true 
);

console.log(
   (a).eval()    //[1, 2, 3]
);

console.log(
   (b).eval()    //[1, 2, 3]
);

Requirement 3 - Monoid Associative law

In addition to the prominent Identify structure, Monoids also satisfies Associative law

(a * b) * c === a * b * c === a * (b * c)

This simply means "flatten the list", in other words, the structure does not contain nested lists.

[a, [b, c]] is no good.

Sample:

const L = a => L; // needs to enhance to satisfy the requirements

const a = (L)(1)(2);
const b = (L)(3)(4);
const c = (L)(99);

const ab = (a)(b);
const bc = (b)(c);
const abc1 = (ab)(c);
const abc2 = (a)(bc);

console.log(
   abc1 === abc2  // true for Associative
);

console.log(
   (ab).eval()    //[1, 2, 3, 4]
);

console.log(
   (abc1).eval()   //[1, 2, 3, 4, 99]
);
console.log(
   (abc2).eval()   //[1, 2, 3, 4, 99]
);

That is all for 3 requirements to implement L as a monoid.

This is a great challenge for functional programming to me, and actually I tried by myself for a while, but asking the previous questions, it's very good practice to share my own challenge and hear the people and read their elegant code.

Thank you.

Efrem answered 12/7, 2018 at 3:55 Comment(14)
First of all, if you want to embrace functional programming, avoid variadic functions or curiously mixed return types.Huntington
Just saying, not storing any data does satisfy your monoid laws. So, how do you want to access the data afterwards? That's the first question you will need to answer.Huntington
a(L) makes no sense given that a might not be a function at allHuntington
@Huntington , see my code: function: identityType, a(L) makes sense. It's right identity as defined,and comment out ``` T[TYPE] = T; //right identity``` line, and how it breaks the logic/output.Brothel
a is always a function. const a = L(5); numbers/any values are wrapped or "lift" to Type:L Similarity of String and Array such as "5" ` [5]`. It's been clarified from the first place in Q.Efrem
@bayesian-study Bergi is right. It doesn't matter whether you mean that a is always supposed to be a function because that's not the way you use it. If a is always supposed to be a function then you can't do (L)(1) because 1 is not a function. The point is that you're using your function inconsistently. From a functional programming point of view, what you're doing really makes no sense at all.Octagonal
@bayesian-study It's a different issue that JavaScript allows you to do things that make no sense. It's very lenient in that respect. However, just because you can do something doesn't mean that you should. Furthermore, since your question explicitly asks for "the smartest or most elegant way (very functional and no mutations ...)" it immediately disqualifies user633183 and KenOKABE's answers because they use a tagging mechanism that uses mutation to provide different answers depending upon context.Octagonal
@bayesian-study It's not their fault though. You have to use a hack like that tagging mechanism to get the behavior you want. There's no way to get such behavior using functional programming. Again, this is because your data type is inconsistent. By the way, there's a disadvantage to using that tagging mechanism. The disadvantage is that it'll be impossible to create a list of lists because all the lists will be flattened (e.g. ((L)(1)(2))((L)(3)(4)) should be [[1,2],[3,4]] but will instead be [1,2,3,4]). There's a reason why we say that one function should only do one thing. This is it.Octagonal
@Aadit M Shah I think you are wrong on some points. #1 As you can see my code, it is very possible not to flatten the list. #2 This is the difference list. #3 Type-lifting is common in Monad programming. You are not justified to keep saying (L)(1) is inconsistent, etc. It is exactly the same to keep claiming Monad(value) is inconsistent.Brothel
@KenOKABE First, where in your code have you not flattened the list? I can't see any example of a list like [[1,2],[3,4]] being created. Second, you've not implemented a difference list. Difference lists don't use accumulators. They use function composition. Third, type lifting is common in functional programming but you're not doing type lifting. You're combining two functions, snoc and append, into one function and choosing when to do what using tags. Type lifting is when you have a separate function that lifts a value into a type. Being separate is needed for choosing when to lift.Octagonal
Obviously, there is absolutely no relation between the tagging mechanism and to flatten a list. Even for difference list, you intentionally generate list because you concat that include flattening there. If you simply add or map list there, you will have Magma without associative law, and binary tree structure instead of list.Brothel
Can't you just see " //evaluation to be associative operation: flatten list const associative = list => flatArray(flatList(list));"?? Just calm down and stop typing and read the code before you say incossitent things.Brothel
If you never listen, just go somewhere else, I mean, Make your own new Question here, whether this is difference list or not. Just ask people around here vote. I will vote this is absolutely the difference-list. I even could put my money on table. How about you?Brothel
I made a little modification to remove some Object. property mutation "shortcut". Therefore, @AaditMShah 's comment is invalid now for the inconsistency data type or non-functional programming etc.Brothel
O
2

Your data type is inconsistent!

So, you want to create a monoid. Consider the structure of a monoid:

class Monoid m where
    empty :: m           -- identity element
    (<*>) :: m -> m -> m -- binary operation

-- It satisfies the following laws:

empty <*> x = x = x <*> empty     -- identity law
(x <*> y) <*> z = x <*> (y <*> z) -- associativity law

Now, consider the structure of your data type:

(L)(a) = (a) = (a)(L)     // identity law
((a)(b))(c) = (a)((b)(c)) // associativity law

Hence, according to you the identity element is L and the binary operation is function application. However:

(L)(1)                  // This is supposed to be a valid expression.
(L)(1) != (1) != (1)(L) // But it breaks the identity law.

// (1)(L) is not even a valid expression. It throws an error. Therefore:

((L)(1))(L)                // This is supposed to be a valid expression.
((L)(1))(L) != (L)((1)(L)) // But it breaks the associativity law.

The problem is that you are conflating the binary operation with the reverse list constructor:

// First, you're using function application as a reverse cons (a.k.a. snoc):

// cons :: a -> [a] -> [a]
// snoc :: [a] -> a -> [a] -- arguments flipped

const xs = (L)(1)(2); // [1,2]
const ys = (L)(3)(4); // [3,4]

// Later, you're using function application as the binary operator (a.k.a. append):

// append :: [a] -> [a] -> [a]

const zs = (xs)(ys); // [1,2,3,4]

If you're using function application as snoc then you can't use it for append as well:

snoc   :: [a] ->  a  -> [a]
append :: [a] -> [a] -> [a]

Notice that the types don't match, but even if they did you still don't want one operation to do two things.

What you want are difference lists.

A difference list is a function that takes a list and prepends another list to it. For example:

const concat = xs => ys => xs.concat(ys); // This creates a difference list.

const f = concat([1,2,3]); // This is a difference list.

console.log(f([])); // You can get its value by applying it to the empty array.

console.log(f([4,5,6])); // You can also apply it to any other array.

The cool thing about difference lists are that they form a monoid because they are just endofunctions:

const id = x => x; // The identity element is just the id function.

const compose = (f, g) => x => f(g(x)); // The binary operation is composition.

compose(id, f) = f = compose(f, id);                   // identity law
compose(compose(f, g), h) = compose(f, compose(g, h)); // associativity law

Even better, you can package them into a neat little class where function composition is the dot operator:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));   // Construct DList from value.

const concat = xs => new DList(ys => xs.concat(ys)); // Construct DList from array.

id . concat([1, 2, 3]) = concat([1, 2, 3]) = concat([1, 2, 3]) . id // identity law

concat([1, 2]) . cons(3) = cons(1) . concat([2, 3]) // associativity law

You can use the apply method to retrieve the value of the DList as follows:

class DList {
    constructor(f) {
        this.f  = f;
        this.id = this;
    }

    cons(x) {
        return new DList(ys => this.f([x].concat(ys)));
    }

    concat(xs) {
        return new DList(ys => this.f(xs.concat(ys)));
    }

    apply(xs) {
        return this.f(xs);
    }
}

const id = new DList(x => x);

const cons = x => new DList(ys => [x].concat(ys));

const concat = xs => new DList(ys => xs.concat(ys));

const identityLeft  = id . concat([1, 2, 3]);
const identityRight = concat([1, 2, 3]) . id;

const associativityLeft  = concat([1, 2]) . cons(3);
const associativityRight = cons(1) . concat([2, 3]);

console.log(identityLeft.apply([]));  // [1,2,3]
console.log(identityRight.apply([])); // [1,2,3]

console.log(associativityLeft.apply([]));  // [1,2,3]
console.log(associativityRight.apply([])); // [1,2,3]

An advantage of using difference lists over regular lists (functional lists, not JavaScript arrays) is that concatenation is more efficient because the lists are concatenated from right to left. Hence, it doesn't copy the same values over and over again if you're concatenating multiple lists.

Octagonal answered 13/7, 2018 at 7:40 Comment(5)
Thanks a lot for your contribution. Very informative, I think. However you made the same mistake by Bergi. Please read the Question well. a is always a function. const a = L(5); numbers/any values are wrapped or "lift" to Type:L Similarity of String and Array such as "5" ` [5]`. It's been clarified from the first place in Q.Efrem
So, I think you may want to modify your answer like "data type is inconsistent", so that I can +1, otherwise I would -1. For "difference lists", I did not know this is formalized in that term, and acgually, this IS the difference lists implementation, although your code of concepts is much clearer and suitable to introduce the nice concept to everyone. Thanks again.Efrem
I made a little modification to remove some Object. property mutation "shortcut". Therefore, @AaditMShah 's comment is invalid now for the inconsistency data type or non-functional programming etc.Brothel
Aadit, this is a very nice answer and demonstration of applied technique.Sob
#51486431 This is a new topic related to this topic. Please review, thanks!Efrem
S
1

mirror test

To make L self-aware we have to somehow tag the values it creates. This is a generic trait and we can encode it using a pair of functions. We set an expectation of the behavior –

is (Foo, 1)            // false   1 is not a Foo
is (Foo, tag (Foo, 1)) // true    tag (Foo, 1) is a Foo

Below we implement is and tag. We want to design them such that we can put in any value and we can reliably determine the value's tag at a later time. We make exceptions for null and undefined.

const Tag =
  Symbol ()

const tag = (t, x) =>
  x == null
    ? x
    : Object.assign (x, { [Tag]: t })
    
const is = (t, x) =>
  x == null
    ? false
    : x[Tag] === t
  
const Foo = x =>
  tag (Foo, x)
  
console.log
  ( is (Foo, 1)         // false
  , is (Foo, [])        // false
  , is (Foo, {})        // false
  , is (Foo, x => x)    // false
  , is (Foo, true)      // false
  , is (Foo, undefined) // false
  , is (Foo, null)      // false
  )
  
console.log
  ( is (Foo, Foo (1))         // true    we can tag primitives
  , is (Foo, Foo ([]))        // true    we can tag arrays
  , is (Foo, Foo ({}))        // true    we can tag objects
  , is (Foo, Foo (x => x))    // true    we can even tag functions
  , is (Foo, Foo (true))      // true    and booleans too
  , is (Foo, Foo (undefined)) // false   but! we cannot tag undefined
  , is (Foo, Foo (null))      // false   or null
  )

We now have a function Foo which is capable of distinguishing values it produced. Foo becomes self-aware –

const Foo = x =>
  is (Foo, x)
    ? x              // x is already a Foo
    : tag (Foo, x)   // tag x as Foo

const f =
  Foo (1)

Foo (f) === f // true

L of higher consciousness

Using is and tag we can make List self-aware. If given an input of a List-tagged value, List can respond per your design specification.

const None =
  Symbol ()

const L = init =>
{ const loop = (acc, x = None) =>
    // x is empty: return the internal array
    x === None
      ? acc

    // x is a List: concat the two internal arrays and loop
    : is (L, x)
      ? tag (L, y => loop (acc .concat (x ()), y))

    // x is a value: append and loop
    : tag (L, y => loop ([ ...acc, x ], y))

  return loop ([], init) 
}

We try it out using your test data –

const a =
  L (1) (2)

const b =
  L (3) (4)

const c =
  L (99)

console.log
  ( (a) (b) (c) () // [ 1, 2, 3, 4, 99 ]
  , (a (b)) (c) () // [ 1, 2, 3, 4, 99 ]
  , (a) (b (c)) () // [ 1, 2, 3, 4, 99 ]
  )

It's worth comparing this implementation to the last one –

// previous implementation
const L = init =>
{ const loop = (acc, x) =>
    x === undefined   // don't use !x, read more below
      ? acc
      : y => loop ([...acc, x], y)
  return loop ([], init)
}

In our revision, a new branch is added for is (L, x) that defines the new monoidal behavior. And most importantly, any returned value in wrapped in tag (L, ...) so that it can later be identified as an L-tagged value. The other change is the explicit use of a None symbol; additional remarks on this have been added a the end of this post.

equality of L values

To determine equality of L(x) and L(y) we are faced with another problem. Compound data in JavaScript are represented with Objects which cannot be simply compared with the === operator

console.log
  ( { a: 1 } === { a: 1 } ) // false

We can write an equality function for L, perhaps called Lequal

const l1 =
  L (1) (2) (3)

const l2 =
  L (1) (2) (3)

const l3 =
  L (0)

console.log
  ( Lequal (l1, l2) // true
  , Lequal (l1, l3) // false
  , Lequal (l2, l3) // false
  )

But I won't go into how to do that in this post. If you're interested, I covered that topic in this Q&A.

// Hint:
const Lequal = (l1, l2) =>
  arrayEqual   // compare two arrays
    ( l1 ()    // get actual array of l1
    , l2 ()    // get actual array of l2
    )

tagging in depth

The tagging technique I used here is one I use in other answers. It is accompanied by a more extensive example here.

other remarks

Don't use !x to test for an empty value because it will return true for any "falsy" x. For example, if you wanted to make a list of L (1) (0) (3) ... It will stop after 1 because !0 is true. Falsy values include 0, "" (empty string), null, undefined, NaN, and of course false itself. It's for this reason we use an explicit None symbol to more precisely identify when the list terminates. All other inputs are appended to the internal array.

And don't rely on hacks like JSON.stringify to test for object equality. Structural traversal is absolutely required.

const x = { a: 1, b: 2 }
const y = { b: 2, a: 1 }

console.log
  (JSON.stringify (x) === JSON.stringify (y)) // false

console.log
  (Lequal (L (x), L (y))) // should be true!

For advice on how to solve this problem, see this Q&A

Sob answered 12/7, 2018 at 14:45 Comment(32)
This is really an educational post. Impressive! For !x, 've just followed the example, but you are absolutely right, and I modified my code in my answer. Also the comment for the JSON.stringify is true, but I must comment that even your Lequal /arrayEqual comparison method does not cover the object traversability.Brothel
Thanks @user633183, this answer is also fascinating. In fact, as I mentioned in the Q, I prefer the curried version and testing my code in that context, so (1) could you rewrite your code in curried (including tag and is) if possible. (2) I prefer @Ken OKABE's solution except the tag idea, because your code evaluate in the list concatenation. My appreciations!Efrem
Ken, that's correct. If you had a L of objects, you have to combine arrayEqual and compare objects using diff.Sob
I've never read or heard of this tag technique and I love your term "self-aware" which give me an impression of something fundamental. Where did you get this idea? Or is this your original?? @user633183. In fact, this seems a new way to define type stuff in JavaScript or functional programming. I may be wrong though. Any way every post by you is really educational! I also wonder why a skillful programmer like you use non-curry stuff. For instance tag look more elegant with curried like tag(type)(value)Brothel
I have made some update of my answer. tag technique is used.Brothel
Tagging is a really bad idea. You're combining two functions, snoc and append, into one by distinguishing lists from other values. However, what if you wanted to create a list of lists? It would be impossible because your tagging mechanism would just flatten all the lists. In general, it's a bad idea to have one function do two different things. Furthermore, your tagging mechanism doesn't solve the monoid law problem. Yes, it satisfies the left identity law (i.e. (L)(1) = (1)) but it doesn't satisfy the right identity law (i.e. (1)(L) = (L)) or by extension the associativity law.Octagonal
Aadit, I tried to address type inconsistency and an alternative implementation in my answer to his/her first question. These questions seem to be about designing a fantasy api for a custom data type, L and so category theory cannot be a rule, only a guide. I don't understand your perspective on tagging as a generalized technique. These simple implementations aren't necessarily bulletproof but they don't bury the learner with complexity either. Thank you for the discussion.Sob
Ken, thanks for your comment. I personally try to avoid terms as I think they can sometimes stand in the way of intuition. I use the phrase "self-aware" because I think it fosters a better intuition for how/why we design is/tag – contrast this to ideas confined to only what one already understands about preexisting things; like monoids, currying, or even types. SICP shows a tagging technique (among countless other techniques) in Scheme, but the adaptation here is my own.Sob
In general I try to focus on demonstrating tiny techniques that enable freedom of thought. As soon as you are burdened with thinking about things that are tangential to what you are trying to do, that is the time to abstract and put things into your own terms, whatever they may be. Studying functional programming, category theory, recursion schemes, etc will equip you with a plethora of techniques but don't become a slave to what you know!Sob
Hey Naomi. Didn't realize that you were the one who wrote this answer. Anyway, tagging definitely does have its uses. However, I believe that it should only be used as a last resort. There are usually better ways to tackle problems than introducing tags. Hence, tagging in my humble opinion is a code smell. For example, in this particular case the deeper problem is that the OP is trying to design a fantasy API which goes against the principles of FP.Octagonal
@user633183 Thank you again. Your post is amazing as usual and I feel often underestimated among the community. You are the great mentor of FP in my humble opinion. Also, thanks for mentioning SICP, I will dig for it.Brothel
@AaditMShah I still think you made mistake to consider is/tag is mutable. It is immutable and as same struture of recursive definition such as Fibonacci sequence or etc. Diffrence list is also defined with recursion and so tagging is.Brothel
Or. more precisely this is a definition of fixed point en.wikipedia.org/wiki/Fixed_point_(mathematics) .Brothel
@KenOKABE The tag function mutates the object passed to it by assigning a [TAG] property to it using Object.assign. That's a side effect. Not to mention that it silently converts primitive values into objects.Octagonal
@AaditMShah Never. You don't understand what Object.assign is. It's an immutable operation to return new value. See developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Brothel
@KenOKABE Read it yourself. Object.assign doesn't "return a new value". It returns the target object (i.e. the first argument) after mutating it. There's nothing immutable about Object.assign. In fact, assignment is the antithesis of immutability.Octagonal
@AaditMShah Probably you even don't know how to read the API specification. Prove this is mutable, if you can. codepen.io/kenokabe/pen/zLveVx?editors=1111Brothel
@KenOKABE And to prove that it is in fact mutating the input object: jsfiddle.net/m6kd2usrOctagonal
Alright, it's my horrible mistake. Sorry about that. You are correct in this sense. I admit. Having said that, we do not have to use this mutable function, instead, some immutable function should do the job. Anyway, thanks for pointing out. This prevents further destruction.Brothel
I mean to define tagging or fixed-point recursively we should not depend on this Object.assign function. Instead, what is the immutable function ?Brothel
@AaditMShah Here's thing. You are right on the spot of Object.assign is mutable, and I agree it should not be in use, however, it's as simple as to prepare a function to immutablly operate some deep clone and add TYPE propery on it and return the new Object. It's just function speficication issue, not tagging techniquie itself. You are still very wrong in that point.Brothel
@user633183 I have just found ES6 ObjectProxy developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… is the immutable solution instead of the mutable Object.assign.Brothel
@AaditMShah I have just found ES6 ObjectProxy developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… is the immutable solution instead of the mutable Object.assign.Brothel
@user633183 it's the same one of java.lang.reflect.Proxy, and of course "reflect" is the idea you mentioned "mirror test"Brothel
Just because Object.assign can produce a side effect, we’re not using it in this way in the program above. Mutation of the constructed objects can only be observed locally, and is invisible outside of L. In other words, the only observable side effects above are the console.log statements. In other examples, I demonstrate creating a tagged value using an object literal such as {tag: X, foo: ...}. The “mutation” discussion has no basis here and only the other aspects of the technique can be remarked upon.Sob
I liken this to the same discipline required to program JavaScript in functional style. There are many ways we could cause side effects, but if we are careful we can write a pure program. tag is effectively used as a constructor and only new values are passed in. tag (X, someExisitngValue) is possible but we exercise the discipline to avoid it.Sob
Ken, you won’t have to dig far for SICP; it’s freely available to read at that link I provided. Proxies are a neat idea but I haven’t found ways in which they make writing functional programs easier or more intuitive. Btw, this is the mirror test I had in mind when writing the section title :DSob
@user633183 I post the another topic #51344030 and a member suggested we can pass a new empty object as the first argument to Object.assign and then it will not mutate the original object. eg. Object.assign({}, x, { [TYPE]: t }). In ES2018 you can do { ...x, [TYPE]: t } which already work in my current node.js environment! So, just tweak it a little and after all it's immutable!!Efrem
@AaditMShah for future reference, with the little modification above, the provided solution turns out to be an immutable.Efrem
The very purpose of abstraction is to create a barrier between the user and complexity. If tag is used as intended (as a constructor accepting new values), mutation cannot be observed. The fact that Object.assign is used in the implementation is not the user's concern. tag performs as intended and adjusting it the way you have only enables the user to use tag against intention...Sob
... We could have designed tag differently, but really that's tangential to question. In the context of L, we consider tag opaque and use it as designed. Rewriting tag as you have done doesn't change the behavior of L and so there is no benefit. And sure, if you want to explore how to write a more robust tagging system, go for it! But remember, I designed tag to implement an idea of my mind – if you have a different idea of how L should/could work, you should design forward from your idea instead of backwards from mine!Sob
@user633183 I refactored the "self aware" strucutre. It turns out, Symbol() Object.assign() Proxy all of them are not nessary to implement Reflection (computer programming) at least for this topic. See my code : #51344030Brothel

© 2022 - 2024 — McMap. All rights reserved.