If the committee would implement deep value semantics for Map and Set, this proposal could be implemented very easily:
import { EquivMap } from "@thi.ng/associative"
let pattern_matcher = args => {
let { a, b, c } = args
return (
new EquivMap([
[{ a, b, c }, `${a} doin' it ${b} the ${c}`],
[{ a, c }, `I ${c} you ${a} cuz you ${c} me!`],
[{ c }, `do you ${c} me?`]
]).get(args) || "i got nothin'"
)
}
pattern_matcher({ a: "just", b: "for", c: "likes" })
//-> just doin' it for the likes 
pattern_matcher({ c: "likes" })
//-> do you likes me?
pattern_matcher({ c: "dislike", a: "alot" })
//-> I dislike you alot cuz you dislike me!
let guarded_matcher = args => {
let { a, b, c } = args
return new EquivMap([[{ a, b, [c > 3 && "c"]: c }, "guarded"]]).get(args) || "go away!"
}
guarded_matcher({ a: "a", b: "b", c: 4 })
//-> guarded
This is a working implementation (works for functions as well, not just template strings) using thi.ng/associative, which has deep value semantics
@loganpowell that's a very interesting use of EquivMap indeed, but unfortunately not very scalable, since with that approach you'd have to perform many, potentially partially duplicate, deep equality checks for the various pattern objects. The main challenge with pattern matching is usually to convert the set of possible patterns into a form of optimized decision tree, e.g. as described in this paper: http://pauillac.inria.fr/~maranget/papers/ml05e-maranget.pdf
Value based equivalence can (and in some cases should) still play a role in this, but the patterns in your first example above should in principle end up something like:
// pseudo code
// arg = object to be matched
if (arg.hasOwnProperty("a")) {
if (arg.hasOwnProperty("c")) {
if (arg.hasOwnProperty("b")) {
return `${arg.a} doin' it ${arg.b} the ${arg.c}`;
}
return `I ${arg.c} you ${arg.a} cuz you ${arg.c} me!`
}
} else if (arg.hasOwnProperty("c")) {
return `do you ${arg.c} me?`;
}
return "i got nothin'";
@postspectacular you make a good point. The performance could be horrible, but - from my experience - these kinds of conditionals are only warranted when instrumenting a rather high-level API. When I usually reach for pattern matching (over other conditionals), it's because I have some pretty complex logic that needs to be deployed based on certain subsets of arguments. I.e., I rarely reach for pattern matching when I need performance, but - rather - when my logic becomes much harder to follow without it.
I suppose there is always a trade off that people can make, given choices, but I have to say THANK YOU for giving us another choice ;)
PS: @postspectacular is the author of all @thi.ngs
@loganpowell that's all fair enough and it's also my own observation, that so far I haven't used pattern matching in hot spots, but that's because I knew the implementation used wasn't very good. Yet, here we're talking about a new language feature, where IMHO every effort should be made to make this as fast as feasible. So, with my earlier reply I only wanted to point out that once such a concept is part of a language, it will be used in all sorts of places (and by people who don't know anything about the impl) and so it shouldn't come with a disclaimer that it's only to be used in some situations, but would be detrimental in others. In languages where pattern matching is a key feature (e.g. ML, Rust etc.), it's not just syntax sugar but will actually generate often better code than handwritten nested conditionals. Even switch is being optimized in most langs...
Regardless, your original use of EquivMap above is pretty ingenious and definitely food for thought! 🤔
@postspectacular I see what you mean and now better understand your intentions. Yes, of course - if possible - the implementation should be comparable to bare-bones if..else (or even better if some version of the decision-tree optimizations you reference where to make it in). But, couldn't (at least some of) those optimizations be made at the Map and Set deep-value semantics implementation? That way the optimization could "percolate up"?
@loganpowell I think the ship has sailed on that front, i.e. at this point Map & Set semantics can't be changed anymore without horribly breaking everyone's code. Though, there could be new native implementations / types which do use value equality instead of identity comparisons... time to open another proposal?
You may be right, correct me if I'm wrong, but wouldn't existing reference based semantics still hold for value semantics? E.g., if an object that's currently being checked by reference - instead - gets checked by deep-value, you'd still get the same result = true, no? Let me know if I'm saying something stupid/obviously wrong here...
Yes, but in places where you relied on getting false, you could suddenly get true, and that would break code.
@ljharb I thought about that, but - unless I'm again overlooking something obvious - with the current Map/Set reference semantics, their is little use of the value-based equality checks for non-primitives. Can you provide me with an example of how one would use a falsey reference check for a non-primitive member of a Map/Set?
I don't want to push a square peg into a round hole. Perhaps I'm conflating two things in the hopes of "killing two birds with one stone". You may sense some of my bias here. I don't really understand why Map/Set shipped with reference semantics. I mean, why? I apologize for this bias, but please understand that I also very much desire pattern matching in JS and found a solution that I figured could be implemented without much fuss.
Perhaps I was wrong.
At any rate, for those who are looking for an interim solution, @thi.ng/associative is one way forward for the time being (for me). I hope others may be able to benefit from @postspectacular 's great work (you should also check out the rest of thi.ng/umbrella). It's full of goodies like this!
@loganpowell const mapOfMaps = new Map(); mapOfMaps.set(otherMap, someValue); mapOfMaps.get(likeOtherMapButNotTheSameReference) !== someValue
mapOfMaps.get(likeOtherMapButNotTheSameReference) !== someValue
The purpose of getting a non-primitive (ever - by reference) as a member of a Map still escapes me...
but I concede, if you do use this it would be broken by "fixing" Map
The entire purpose of Map is to have a non-primitive key :-)
I think both @loganpowell and I are viewing this somewhat from a Clojure perspective, where value semantics are the norm and IMHO have a lot more use cases than the current identity based approach. But be that as it may, this current approach has been out in the wild for several years now and therefore too late to change (again, IMHO)...
@loganpowell I've got some ideas how to optimize/compile your above example and I will open a new issue in thi.ng/umbrella to discuss further. Maybe some of this can flow back into here in the near future, even if just as reference...
A little bit of found history:
https://esdiscuss.org/topic/maps-with-object-keys#content-5
Somewhat related: a JS proposal introducing data structures with deep-equality semantics: https://github.com/tc39/proposal-record-tuple
Changing Map and Set isn't on the table; nor is changing. that JS doesn't have value semantics.
Records and Tuples would be a new value-type-like primitive, and both proposals will have to figure out how they impact each other as part of their advancement.
I'm going to close this; we can open separate issues when needed to discuss interaction with other proposals.
edited: Sorry for the redundant reference @mheiber!
Update 🆕
We might be getting closer to this with Records and Tuples
The proposal Record & Tuple (by Robin Ricard and Rick Button) lets us create compound values that are compared by value.
For, example, by prefixing an object literal with a number sign (#), we create a record – a compound value that is compared by value and immutable:
#{x: 1, y: 4} === #{x: 1, y: 4}
//=> true
If we prefix an Array literal with #, we create a tuple – an Array that is compared by value and immutable:
#['a', 'b'] === #['a', 'b']
//=> true
Compound values that are compared by value are called compound primitive values or compound primitives.
Records and tuples are primitives
We can see that records and tuples are primitives when we use typeof:
typeof #{x: 1, y: 4}
//=> 'record'
typeof #['a', 'b']
//=> 'tuple'
Restrictions of what can be inside records and tuples
Records:
Tuples:
Most helpful comment
Somewhat related: a JS proposal introducing data structures with deep-equality semantics: https://github.com/tc39/proposal-record-tuple