meander

Tanner Emerson 2022-12-07T20:10:07.483839Z

I am trying to grab a single list of free and paid item ids from this nested structure. I have tried multiple approaches with no luck. I will post my attempts in the thread. Any ideas on how to approach this problem?

{:games [{:id "foo"
          :items {:free [{:id "a"} {:id "b"}]
                  :paid [{:id "c"}]}}

         {:id "bar"
          :items {:free [{:id "d"} {:id "c"}]
                  :paid [{:id "f"}]}}

         {:id "baz"}]}

target => ["a" "b" "c" "d" "c" "f"] 
;; order does not matter
;; ideally no duplicates but not required

Tanner Emerson 2022-12-07T20:12:26.171049Z

This attempt accumulates all free items but ignores paid items

(m/rewrites {:games [{:id "foo"
                      :items {:free [{:id "a"} {:id "b"}]
                              :paid [{:id "c"}]}}

                     {:id "bar"
                      :items {:free [{:id "d"} {:id "c"}]
                              :paid [{:id "f"}]}}

                     {:id "baz"}]}
  {:games (m/scan {:items {:free (m/scan {:id !item-ids})
                           :paid (m/scan {:id !item-ids})}})}
  !item-ids)

Tanner Emerson 2022-12-07T20:17:47.628159Z

This attempt gave me an empty list

(m/rewrites {:games [{:id "foo"
                      :items {:free [{:id "a"} {:id "b"}]
                              :paid [{:id "c"}]}}

                     {:id "bar"
                      :items {:free [{:id "d"} {:id "c"}]
                              :paid [{:id "f"}]}}

                     {:id "baz"}]}
  {:games [{:items {:free [{:id !free-ids}
                           ...]
                    :paid [{:id !paid-ids}
                           ...]}}
           ...]}
  [!free-ids ... . !paid-ids ...])

gdubs 2022-12-08T18:24:01.932309Z

How about this?

gdubs 2022-12-08T18:49:09.900249Z

I don't have an explanation for why the paid ids are being duplicated.

gdubs 2022-12-08T18:50:32.652559Z

If I switch the order of the m/or arguments, I get different duplicates. That surprises me.

Matthew Odendahl 2022-12-08T18:50:56.035489Z

This works, and is general.

(ns solution                                                  
  (:require [clojure.test :refer [is]]                       
            [meander.epsilon :as m]))                        
                                                             
(def example                                                 
  {:games [{:id "foo"                                        
            :items {:free [{:id "a"} {:id "b"}]              
                    :paid [{:id "c"}]}}                      
                                                             
           {:id "bar"                                        
            :items {:free [{:id "d"} {:id "c"}]              
                    :paid [{:id "f"}]}}                      
                                                             
           {:id "baz"}]})                                    
                                                             
(defn transform                                              
  {:test #(is (transform example) ["a" "b" "c" "d" "c" "f"])}
  [input]                                                    
  (vec (m/search input                                       
         {:games (m/scan {:items {(m/or :free :paid)         
                                  (m/scan {:id ?x})}})}      
         ?x)))                                               
                                                             

πŸ™Œ 3
Jimmy Miller 2022-12-08T20:53:25.944679Z

You can also do this without searching. I threw in the distinct there, but not strictly needed from the requirements.

(m/rewrite data
 {:games [(m/or {:items {:free [{:id !free} ...]
                         :paid [{:id !paid} ...]}}
                _) ...]}
  (m/app distinct [!free ... !paid ...]))

πŸ™Œ 2
gdubs 2022-12-08T23:27:47.238209Z

Thank you @jimmy for your 2019 meander strange loop talk. That's what got us here!

πŸ™‚ 1
grant 2022-12-07T20:20:00.694119Z

In an effort to learn Meander I am attempting to port some existing code to use it. Some of the examples have been pretty easy, but I’m struggling to figure out an elegant way, with Meander, to take input and produce target-out. Any suggestions or advice would be greatly appreciated.

(def input {:required  [{:id :e1 :a 1}
                          {:id :e2 :a 2}
                          {:id :e3 :a 3}
                          {:id :e4 :a 4}]
              :optional1 [{:id :e1 :b 1}
                          {:id :e2 :b 2}]
              :optional2 [{:id :e1 :c 1}
                          {:id :e3 :c 3}]})
(def target-out '({:id :e1 :a 1 :b 1 :c 1}
                    {:id :e2 :a 2 :b 2     }
                    {:id :e3 :a 3      :c 3}
                    {:id :e4 :a 4          }))

grant 2022-12-10T18:44:37.407769Z

Thanks you @noprompt and @gdwarner. Both of these are much clearer than the attempts I had managed so far. At this point, is Epsilon still recommended over Zeta, or should I start using Zeta since I don’t have any legacy code (or knowledge) to worry about?

gdubs 2022-12-07T22:02:43.314109Z

I've had this same question too. Its like you want to do an outer join instead of an inner join.

gdubs 2022-12-08T23:53:18.721019Z

This is really close

noprompt 2022-12-09T06:19:43.758699Z

Yep. The other thing here is that m/or on epsilon finds all of the solutions which is why the solution set we get with m/rewrites has more results than we want. We can get around this by querying the required portion of the map with m/search and then extracting the optional stuff with m/find.

(m/search input
  {:required (m/scan {:id ?id, :as ?attrs})
   :optional1 ?optional1
   :optional2 ?optional2}
  (m/find [?id ?optional1 ?optional2]
    [?id
     (m/or (m/scan {:id ?id, & !attrs}) _)
     (m/or (m/scan {:id ?id, & !attrs}) _)]
    (reduce merge ?attrs !attrs)))
;; =>
({:id :e1, :a 1, :b 1, :c 1}
 {:id :e2, :a 2, :b 2}
 {:id :e3, :a 3, :c 3}
 {:id :e4, :a 4})

noprompt 2022-12-09T06:21:14.839169Z

This would actually work, however, if epsilon had a short circuiting m/or.

noprompt 2022-12-09T06:21:24.270289Z

Without the m/find.

noprompt 2022-12-09T06:23:30.899699Z

This is what the rule currently looks like on zeta that finds the solution.

(m/rule
 {:required (m/scan {:id ?x & ?a}),
  :optional1 ?optional1,
  :optional2 ?optional2}
 (m/let [(m/pick (m/scan {:id ?x & ?b}) _) ?optional1
         (m/pick (m/scan {:id ?x & ?c}) _) ?optional2]
   {:id ?x
    &0 ?a
    &1 (m/pick ?b {})
    &2 (m/pick ?c {})}))

noprompt 2022-12-09T06:24:26.599229Z

zeta has a short circuiting m/or called m/pick.

noprompt 2022-12-09T06:26:02.999139Z

It is also slightly different in that everything that you can do on the left you can do on the right which is why m/let is there an there are guards around the variables ?b and ?c which may not be bound.

noprompt 2022-12-09T06:32:06.279479Z

It might be possible add something like m/or! to epsilon

πŸ‘ 1