Fork me on GitHub
#clojurescript
<
2023-03-24
>
Lidor Cohen06:03:21

Hi everyone! Is there an implementation for a "multimap" (I may have made up a word) a map which allows duplicate keys?

tomd07:03:28

A map with vectors or sets for values would do the job. Would that not satisfy your requirements?

Lidor Cohen07:03:21

I need multiple assoc to the same key

❓ 2
tomd07:03:00

You can use update (and maybe fnil)

Grigory Shepelev07:03:31

Maybe you'd tell us about exact problem you solving? Maybe you guide yourself into some esoteric use case which can be solved without having this (weird?) Requirement?

daveliepmann07:03:16

For example I'd first reach for something like (update {} :foo (fnil conj #{}) "foo" "bar") and hopefully there's no need for more

βž• 6
daveliepmann07:03:28

Or, this might better communicate "multiple assoc to the same key":

(-> {}
    (update :foo (fnil conj #{}) "foo")
    (update :bar (fnil conj #{}) "bar")
    (update :foo (fnil conj #{}) "foo2"))

Lidor Cohen08:03:28

It's quite complicated but the gist is: I'm trying to design a structure that when evaluated will conditionally accumulate values to keys (by some accumulation logic) based on custom predicates. I want the predicates to be co-located with their predicated values, but without interfering with the structure of the data (as metadata). So for example:

{:a (with-meta-pred is-x? {:x "x"})
 :a (with-meta-pred is-y? {:y "y"})}
Will evaluate:
{:a {:x "x" ;;if is-x?
     :y "y"} ;;if is-y?
Based on is-x? And is-y? This is part of some greater design goal, and it is based on conscious design decisions to have that data structure the way it is. I say this because, I know the tendency to tell someone that doing something unusual that they might be doing it wrong. I probably am wrong, but at the api design level and everyone wrong in some way when it comes to language/api design. The multimap is an implementation detail to support the goals I specified in this post. Clojure and ClojureScript too had to do unusual (and even "dirty") stuff to accommodate for the disparity between their ideal interface and the reality of their underlying platform, so take that angle when looking at my question please πŸ™. I'm sorry if I sound unpleasant, I'm just trying to save us some long discussion on how we got to that question, I'm trying to assure you it makes sense in some context ☺️.

dgb2312:03:26

To me this looks like a relational (set) structure rather than a map. If you squint:

#{{:key :a :dispatch (with-meta-pred is-x? {:x "x"})}
  {:key :a :dispatch (with-meta-pred is-y? {:y "y"})}}

Lidor Cohen13:03:21

Or a vector πŸ˜‰

lilactown16:03:01

a map with a collection at the key is the simplest way to do exactly what you want. you could create your own type that wraps it and implements get/assoc/conj/etc. in such a way that

(-> (my-thing)
    (assoc :a 1)
    (assoc :a 2)
    (get :a))
;; => #{1 2}
is true. but I don't think anyone has implemented such a thing because there are simpler ways of achieving it, like using what @U05092LD5 said with update and (fnil conj #{}) or using a set like @U01EFUL1A8M provided

Grigory Shepelev09:03:01

@U8RHR1V60 sorry for somewhat late reply. as far as I've got your explanation about what's you need to do, I can recommend looking at re-frame and it's dispatching pattern. re-frame encodes events with payloads as vectors [:<event-a> <payload-a>] (generally) like this [:event-a :payload-b 1 :more-payload-b] (specific). https://day8.github.io/re-frame/api-re-frame.core/

(dispatch [:order "pizza" {:supreme 2 :meatlovers 1 :veg 1}])

adi13:03:30

Spitballing... Your description sounds like one or both of these: 1. a write-ahead log of operations 2. a "data DSL" based interpreter/accumulator Both models imply sequential processing of a set (or a stream) of operations (potentially as purely functional data transformations for idempotence, e.g. what if there are multiple operations that produce the exact same result?). Additionally, error handling is absent from your description (though you may be doing it somewhere). e.g. What if predicates fail? Maybe the interceptor pattern will serve your requirement well.

adi13:03:36

Or even more simply, just a fold/reduction... the idea is right there in the problem statement, viz. "... accumulate ...": > I'm trying to design a structure that when evaluated will conditionally accumulate values to keys (by some accumulation logic) based on custom predicates.

[{:a {}} {:b {}} ... {:a {}} ] -> {:a {}, :b {}}

adi13:03:24

I would model the solution over a stream of operations. Simpler than worrying about look-up semantics of a potential "multi-map". Should a lookup for a key (say, :a ) return one, some, all options? (Which, to me, is exactly the question one must address when querying a table that does not have a unique key constraint).

Lidor Cohen18:03:35

Thank you everyone for the engagement. We're still at the design phase so spitballing is exactly what we need. I'll try to elaborate a bit more on our goal here: We're trying some kind of https://en.wikipedia.org/wiki/Prototype-based_programming with multiple conditioned properties that will be late-binded based on the latest system state. @U051MHSEK you were spot on with the "data-dsl" option and with the sequential processing. The evaluation we've got so far is a sequential fold which will result in a map. If two predicates result in true for the same key they will be merged or the latter will apply, so we are exposed to ordering errors with this map, if a predicate returns false it will not be merged to the final result. The idea behind that is to be able to keep modifying programmatically deep nested properties behind conditional logic. We are looking for ideas on how to best implement such a thing in clojure, so by all means keep spitballing us 😁. Another source of inspiration is the prototype implementation presented in https://livebook.manning.com/book/the-joy-of-clojure-second-edition/chapter-9/section-9-2?origin=product-toc. Again, thanks for all the engagement, we'll probably have to implement something customized for this, probably based on vectors that implement some of the map protocol.