Fork me on GitHub
#meander
<
2021-03-20
>
wilkerlucio23:03:49

hello, I have a case that I'm trying to figure if meander can solve, I have data that can come in the following formats:

; contains rental prices and sales price
{:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                   :warranties              [],
                                   :monthlyRentalTotalPrice "4520"},
                 :yearlyIptu      "278",
                 :price           "3690",
                 :businessType    "RENTAL",
                 :monthlyCondoFee "830"}
                {:yearlyIptu      "278",
                 :price           "990000",
                 :businessType    "SALE",
                 :monthlyCondoFee "830"}]}

; contains only sales price
{:pricingInfos [{:yearlyIptu      "278",
                 :price           "990000",
                 :businessType    "SALE",
                 :monthlyCondoFee "830"}]}
and I like to match on the sales price only, I tried:
(m/match {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                            :warranties              [],
                                            :monthlyRentalTotalPrice "4520"},
                          :yearlyIptu      "278",
                          :price           "3690",
                          :businessType    "RENTAL",
                          :monthlyCondoFee "830"}
                         {:yearlyIptu      "278",
                          :price           "990000",
                          :businessType    "SALE",
                          :monthlyCondoFee "830"}]}
  {:pricingInfos [{:price        ?price
                   :businessType "SALE"}]}
  {:sales-price ?price})
but that triggers an error of non exhaustive pattern match, is there a way to do a match like this with meander? (that kinda "finds" the matching item from a list)

phronmophobic23:03:44

I think m/find with m/scan will do what you want:

> (m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}]}
  {:pricingInfos (m/scan
                  {:price        ?price
                   :businessType "SALE"})}
  {:sales-price ?price})

;;{:sales-price "990000"}

wilkerlucio23:03:08

work as a charm, thanks!

👍 3
phronmophobic23:03:43

if you want to find multiple items, you can use m/search in place of m/find

wilkerlucio23:03:10

cool, what if I like to optionally matching also the rental price (which sometimes may not be there), what is the way to go?

wilkerlucio23:03:37

I was able to match here when both are available, but the same pattern returns nil in case I remove the rental data

phronmophobic23:03:59

not sure I understand the question. do you have an example?

wilkerlucio00:03:02

this works as expected:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                             :warranties              [],
                                             :monthlyRentalTotalPrice "4520"},
                           :yearlyIptu      "278",
                           :price           "3690",
                           :businessType    "RENTAL",
                           :monthlyCondoFee "830"}
                          {:yearlyIptu      "278",
                           :price           "990000",
                           :businessType    "SALE",
                           :monthlyCondoFee "830"}]}
    {:pricingInfos (m/scan
                     {:price        ?rental-price
                      :businessType "RENTAL"}
                     {:price        ?price
                      :businessType "SALE"})}
    {:sales-price  (Integer/parseInt ?price)
     :rental-price (Integer/parseInt ?rental-price)})
=> {:sales-price 990000, :rental-price 3690}

wilkerlucio00:03:20

but in case the rental goes missing, it fails completly:

(m/find {:pricingInfos [{:yearlyIptu      "278",
                           :price           "990000",
                           :businessType    "SALE",
                           :monthlyCondoFee "830"}]}
    {:pricingInfos (m/scan
                     {:price        ?rental-price
                      :businessType "RENTAL"}
                     {:price        ?price
                      :businessType "SALE"})}
    {:sales-price  (Integer/parseInt ?price)
     :rental-price (Integer/parseInt ?rental-price)})
=> nil

wilkerlucio00:03:39

I would like to every match to be optional, and fill with nil in case the option isn't present

phronmophobic00:03:47

I've been using something like:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}
                          ]}
  {:pricingInfos (unordered
                  {:price ?sales-price
                   :businessType "SALE"}
                  {:price ?rental-price
                   :businessType "RENTAL"})}
  {:sales-price ?sales-price
   :rental-price ?rental-price})

phronmophobic00:03:06

with unordered defined as:

(m/defsyntax unordered [& patterns]
  `(m/app set
          ~(set patterns)))

phronmophobic00:03:27

oh wait, that doesn't work if one is missing

phronmophobic00:03:29

There's probably a way to do the exact transformation in one go, but I would probably just use something like

(m/search {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                             :warranties              [],
                                             :monthlyRentalTotalPrice "4520"},
                           :yearlyIptu      "278",
                           :price           "3690",
                           :businessType    "RENTAL",
                           :monthlyCondoFee "830"}
                          {:yearlyIptu      "278",
                           :price           "990000",
                             :businessType    "SALE",
                             :monthlyCondoFee "830"}
                          ]}
  {:pricingInfos (m/scan
                  {:price ?price
                   :businessType ?type})}
  
  {:price ?price
   :type ?type})
;; ({:price "3690", :type "RENTAL"} {:price "990000", :type "SALE"})

wilkerlucio00:03:06

thanks, I really love to see a way to do that in one go, because I have a lot of cases like this ,and being able to break the categories in a match like that somehow would be great

phronmophobic00:03:39

this is a little verbose, but seems to work:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}
                        ]}
  {:pricingInfos (m/or
                  (unordered
                   {:price ?sales-price
                    :businessType "SALE"}
                   {:price ?rental-price
                    :businessType "RENTAL"})
                  (m/let [?sales-price nil]
                    (unordered
                     {:price ?rental-price
                      :businessType "RENTAL"}))
                  (m/let [?rental-price nil]
                    (unordered
                     {:price ?sales-price
                      :businessType "SALE"}))
                  (m/let [?rental-price nil
                          ?sales-price nil] _))}
  {:sales-price ?sales-price
   :rental-price ?rental-price})

phronmophobic00:03:31

there's probably a way to simplify it, possibly with m/app or m/defsyntax

wilkerlucio04:03:23

hello, I tried to run it now, but always getting nils

wilkerlucio04:03:24

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}]}
  {:pricingInfos (m/or
                   (unordered
                     {:price ?sales-price
                      :businessType "SALE"}
                     {:price ?rental-price
                      :businessType "RENTAL"})
                   (m/let [?sales-price nil]
                     (unordered
                       {:price ?rental-price
                        :businessType "RENTAL"}))
                   (m/let [?rental-price nil]
                     (unordered
                       {:price ?sales-price
                        :businessType "SALE"}))
                   (m/let [?rental-price nil
                           ?sales-price nil] _))}
  {:sales-price  ?sales-price
   :rental-price ?rental-price})
=> {:sales-price nil, :rental-price nil}

phronmophobic04:03:30

weird. I just copy and pasted the code from your message into my repl and it gave:

{:sales-price "990000", :rental-price "3690"}
I did just upgrade meander to meander/epsilon {:mvn/version "0.0.602"} . what version are you using?

wilkerlucio04:03:57

this same, 0.0.602, on MacOS

phronmophobic04:03:20

and you have:

(m/defsyntax unordered [& patterns]
  `(m/app set
          ~(set patterns)))
to define unordered?

phronmophobic04:03:51

I'm on MacOS as well

wilkerlucio04:03:15

and I just found another way to do it, with less verbosity 🙂

wilkerlucio04:03:17

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                             :warranties              [],
                                             :monthlyRentalTotalPrice "4520"},
                           :yearlyIptu      "278",
                           :price           "3690",
                           :businessType    "RENTAL",
                           :monthlyCondoFee "830"}
                          {:yearlyIptu      "278",
                           :price           "990000",
                           :businessType    "SALE",
                           :monthlyCondoFee "830"}]}
    {:pricingInfos (m/and
                     (m/or (m/scan
                             {:price        ?rental-price
                              :businessType "RENTAL"})
                       (m/let [?rental-price nil]))
                     (m/or
                       (m/scan
                         {:price        ?price
                          :businessType "SALE"})
                       (m/let [?price nil])))}
    {:sales-price  ?price
     :rental-price ?rental-price})
=> {:sales-price "990000", :rental-price "3690"}

toot 3
wilkerlucio04:03:33

that covers all cases, and including new cases can be done with 1 new addition

wilkerlucio04:03:27

that unordered trick is cool, I'm very excited with Meander 🙂

phronmophobic04:03:11

the m/and is a neat trick too

wilkerlucio04:03:30

next is learn how to abstract that in some defsyntax

phronmophobic04:03:02

yea, was just going to say that it should be possible to add syntax to make it as short as the original attempt

wilkerlucio04:03:19

yeah, my wishful syntax looks like this:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                             :warranties              [],
                                             :monthlyRentalTotalPrice "4520"},
                           :yearlyIptu      "278",
                           :price           "3690",
                           :businessType    "RENTAL",
                           :monthlyCondoFee "830"}
                          {:yearlyIptu      "278",
                           :price           "990000",
                           :businessType    "SALE",
                           :monthlyCondoFee "830"}]}
    {:pricingInfos (list-pick
                     {:price        ?rental-price
                      :businessType "RENTAL"}
                     {:price        ?price
                      :businessType "SALE"})}
    {:sales-price  ?price
     :rental-price ?rental-price})

notbad 3
phronmophobic04:03:15

there's probably also a way set defaults for unbound logic variables. maybe it's worth filing an issue. I think it comes up regularly in the chat

phronmophobic04:03:55

my wishful syntax is like:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}]}
  {:pricingInfos (with-defaults [?rental-price nil
                                 ?price nil]
                   (unordered
                    {:price        ?rental-price
                     :businessType "RENTAL"}
                    {:price        ?price
                     :businessType "SALE"}))}
  {:sales-price  ?price
   :rental-price ?rental-price})
and it only scans through the list once

👀 3
wilkerlucio05:03:17

got the list-pick working 😄

wilkerlucio05:03:19

(defn collect-variables [pattern]
  (let [vars* (volatile! #{})]
    (walk/postwalk
      (fn [x]
        (if (and (symbol? x)
                 (str/starts-with? (name x) "?"))
          (vswap! vars* conj x))
        x)
      pattern)
    @vars*))

(m/defsyntax list-pick [& patterns]
  `(m/and
     ~@(map
         (fn [pattern]
           (let [vars (collect-variables pattern)
                 defaults (vec (interleave vars (repeat nil)))]
             `(m/or
                (m/scan ~pattern)
                (m/let ~defaults))))
         patterns)))

wilkerlucio05:03:00

still many scans, but in my case these lists rarely go over 5 items

clojure-spin 3
phronmophobic05:03:07

the collect-variables works. there's also a builtin function that finds all the logic variables. For example:

(m/defsyntax mor
  "work around for 'Every pattern of an or pattern must have references to the same unbound logic variables.'"
  [& patterns]
  (let [vars
        (into []
              (comp
               (map r.syntax/parse)
               (map r.syntax/variables)
               (map #(map r.syntax/unparse %))
               (map set))
              patterns)
        all-vars (into #{} cat vars)]
    `(m/or
      ~@(for [[pvars pattern] (map vector vars patterns)
              :let [missing (clojure.set/difference all-vars pvars)]]
          (if (seq missing)
            (let [bindings
                  (into []
                        cat
                        (for [v missing]
                          [v nil]))]
              `(m/let ~bindings
                 ~pattern))
            pattern)))))

🙌 3
phronmophobic05:03:01

you can also use r.syntax/logic-variables if instead of r.syntax/variables if you only care about ?variables and not !variables

wilkerlucio05:03:04

my next challenged is how to deal when a binding is shared between cases, but may be blank (due to the OR binding when the item isn't there)

wilkerlucio05:03:06

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                             :warranties              [],
                                             :monthlyRentalTotalPrice "4520"},
                           :yearlyIptu      "278",
                           :price           "3690",
                           :businessType    "RENTAL",
                           :monthlyCondoFee "830"}
                          {:yearlyIptu      "278",
                           :price           "990000",
                           :businessType    "SALE",
                           :monthlyCondoFee "830"}]}
    {:pricingInfos (wm/list-pick
                     {:rentalInfo      {:period                  ?rental-pay-period
                                        :monthlyRentalTotalPrice ?rental-price-total}
                      :price           ?rental-price
                      :yearlyIptu      ?iptu
                      :monthlyCondoFee ?fee
                      :businessType    "RENTAL"}
                     {:price           ?sales-price
                      :yearlyIptu      ?iptu
                      :monthlyCondoFee ?fee
                      :businessType    "SALE"})}
    {:sales-price  ?sales-price
     :rental-price ?rental-price
     :iptu ?iptu
     :condo-fee ?fee})

wilkerlucio05:03:39

in this case, ?iptu and ?fee are the same, when present, but if there is no rental for example, then it would fail because it would be set to nil in rental, but with a value in sale

wilkerlucio05:03:39

I guess the defaults you wished for could make this easier

phronmophobic05:03:48

I also just remembered the return expression is just a normal expression which means you can do stuff like:

(m/find {:pricingInfos [{:rentalInfo      {:period                  "MONTHLY",
                                           :warranties              [],
                                           :monthlyRentalTotalPrice "4520"},
                         :yearlyIptu      "278",
                         :price           "3690",
                         :businessType    "RENTAL",
                         :monthlyCondoFee "830"}
                        {:yearlyIptu      "278",
                         :price           "990000",
                         :businessType    "SALE",
                         :monthlyCondoFee "830"}]}
  {:pricingInfos (list-pick
                  {:yearlyIptu      !iptu
                   :businessType    "RENTAL"}

                  {:price      !rental-price
                   :businessType    "RENTAL"}
                  {:price      !sales-price
                   :businessType    "SALE"}

                  {:monthlyCondoFee !fee}
                  {:yearlyIptu      !iptu
                   :businessType    "SALE"})}
  {:iptu (some identity !iptu)
   :rental-price (some identity !rental-price)
   :condo-fee (some identity !fee)
   :sales-price (some identity !sales-price)
   })

wilkerlucio05:03:36

yup, funny enough, that's exactly the direction I'm trying to automate

wilkerlucio05:03:43

but I'm hiding the accumulators on the syntax itself

🙌 3
wilkerlucio05:03:51

(trying to at least)

phronmophobic05:03:35

that would be cool

phronmophobic05:03:39

one of the other tools that is less documented, but powerful is m/cata which can let you do some really interesting stuff

🆒 3
wilkerlucio05:03:20

(defn logic->mutable [var]
  (symbol (str "!" (name var))))

(defn collect-variables [pattern]
  (let [vars*    (volatile! #{})
        pattern' (walk/postwalk
                   (fn [x]
                     (if (and (symbol? x)
                              (str/starts-with? (name x) "?"))
                       (do
                         (vswap! vars* conj x)
                         (logic->mutable x))
                       x))
                   pattern)]
    [@vars* pattern']))

(m/defsyntax list-pick [& patterns]
  (let [all-vars (into #{} (mapcat (comp first collect-variables)) patterns)
        all-let  (vec (interleave all-vars
                        (map
                          (fn [v]
                            (let [new-name (logic->mutable v)]
                              `(some identity ~new-name)))
                          all-vars)))]
    `(m/and
       ~@(map
           (fn [pattern]
             (let [[vars pattern] (collect-variables pattern)
                   vars-set (vec (interleave (map logic->mutable vars) (repeat nil)))]
               `(m/or
                  (m/scan ~pattern)
                  (m/let ~vars-set))))
           patterns)
       (m/let ~all-let))))

phronmophobic05:03:38

ok. I think I'm starting to figure it out. That's neat!

phronmophobic06:03:30

I didn't realize you could use m/let with m/and to just bind variables at the end. That's a useful technique.

wilkerlucio06:03:45

yeah, was a guess, glad it worked 🙂

clojure-spin 3