Fork me on GitHub
#meander
<
2022-05-17
>
xiongtx03:05:41

Wondering how best to deal w/ matching on nested map data where keys are sometimes missing. This is fine:

(require '[meander.epsilon :as meander])

(meander/match {:foo 1}
  {:foo ?foo
   :bar ?bar} [?foo ?bar])
;; => [1 nil]
But adding a layer breaks it
(meander/match {:foo 1}
  {:foo ?foo
   :bar {:baz ?baz}} [?foo ?baz])
;; => non exhaustive pattern match
I’d like the result to be [1 nil] again. Using [meander/epsilon "0.0.602"].

xiongtx03:05:22

I guess the problem is that nil doesn’t match a map.

(meander/match nil
  {:foo ?foo} ?foo)
;; => non exhaustive pattern match

xiongtx03:05:06

Would be nice to have something like:

(meander/match {:foo 1}
  {:foo ?foo
   :bar (meander/optional {:baz ?baz})} [?foo ?baz])
;; => [1 nil]

xiongtx03:05:25

I see that there’s a recipe for a “Maybe pattern”: https://github.com/noprompt/meander/blob/epsilon/doc/cookbook.md#the-maybe-pattern But in my case if there are many keys beyond just :baz, all of them have to be specified in the (m/and nil ...) clause. Which is fine…but rather tedious and inelegant.

noprompt04:05:31

Yes. It is not elegant. Another solution, which is also not elegant, is to use cata.

(m/match {:foo 1}
  {:foo ?foo
   :bar (m/cata ?x)} [?foo ?x]

  {:baz (m/some ?baz)} ?baz

  _ nil)

noprompt04:05:49

This returns [1 nil]

noprompt04:05:03

This also returns [1 nil]:

(m/match {:foo 1}
  {:foo ?foo
   :bar (m/or {:baz ?baz}
              ?baz)}
  [?foo ?baz])

xiongtx04:05:27

I think the last solution works if there’s only one expected key in the :bar map, but in my case there could be many…

noprompt04:05:31

(The (m/and nil ,,,) can be dropped because either you have a map or you don't. If you do have a map, then ?baz will bind to whatever is there. If you don't have a map then ?baz will be bound to that.

noprompt04:05:36

If there are many optional keys, then it will be a lot of m/or. FWIW this is actually the price of maps with optional keys.

noprompt04:05:50

In vanilla Clojure there's (if-some [e (find m k)] ,,,) , (get-in m [k1 k2] not-found), (or (get m k) not-found), etc.

noprompt04:05:22

This is one of those things where I'm open to suggestions about how it could be more elegant.

xiongtx04:05:03

The meander/optional could just be a rewrite of the cookbook’s solution, where it collects all the binding vars & sticks them in an (m/and nil …).

noprompt04:05:25

[?foo (m/optional [:baz ?baz]) ?baz]
What happens in a case like this?

xiongtx04:05:50

I’d think it’d be equivalent to

[?foo (m/or (m/and nil ?baz) [:baz ?baz]) ?baz]

noprompt04:05:05

(require '[meander.epsilon :as m])
(require '[meander.syntax.epsilon :as m.syntax])

(m/defsyntax optional [p]
  (if (m/match-syntax? &env)
    (let [var-syms (map :symbol (m.syntax/variables (m.syntax/parse p)))]
      `(m/or ~p (m/and nil ~@var-syms)))
    (throw (ex-info "substition not supported" {:form &form}))))
(m/match {:foo 1}
  {:foo ?foo
   :bar (optional {:baz ?baz})}
  [?foo ?baz])
;; =>
[1 nil]

noprompt04:05:02

This is approximately what I would do. Maybe some more thinking on it. But you can use this in your project if it helps you and irons out some of that ugliness. 🙂

👏 1
xiongtx04:05:04

It’s hardcoded to :baz ?baz though? Or should that be replaced w/ something?

noprompt05:05:43

Yes, it should be replaced with p. (I fixed the code above).

xiongtx21:07:42

@U06MDAPTP Unfortunately optional doesn’t seem to nest:

(m/match {:foo {:bar "bar"
                :baz nil}}
  {:foo (optional {:bar ?bar
                   :baz (optional {:quux ?quux})})} ?quux)

;; => 1. Caused by clojure.lang.ExceptionInfo
   substition not supported
   {:form (optional {:quux ?quux})}

xiongtx03:07:41

Also running into performance issue when using a lot of optional : https://github.com/noprompt/meander/issues/234

xiongtx21:06:22

Changing the definition of optional to the following allows nesting:

(defsyntax optional [p]
  (if (m/match-syntax? &env)
    (let [var-syms (map :symbol (m-syntax/variables (m-syntax/parse p)))]
      `(m/or ~p (m/and nil ~@var-syms)))
    &form))
This seems to be the [pattern](https://github.com/noprompt/meander/blob/epsilon/doc/defsyntax.md) used by most defsyntax constructs.

xiongtx06:05:44

Just ran into the strangest bug, but only on [meander/epsilon "0.0.602"] (the version I was on). W/ a project containing nothing but:

(ns meander-test.core
  (:require [meander.epsilon :as m]))

(defn extract
  [m]
  (m/match m
    {:letters (m/or
               (m/and nil
                      ?a
                      ?b
                      ?c
                      ?d
                      ?e
                      ?f
                      ?g
                      ?h
                      ?i
                      ?j
                      ?k
                      ?l
                      ?m
                      ?n)
               {:a ?a
                :b ?b
                :c ?c
                :d ?d
                :e ?e
                :f ?f
                :g ?g
                :h ?h
                :i ?i
                :j ?j
                :k ?k
                :l ?l
                :m ?m
                :n ?n})}
    {:a ?a}))
cider-jack-in hangs & causes CPU usage to go crazy. Doesn’t happen w/ 0.0.626.

xiongtx06:05:31

But, if I comment out one element in the and, it works fine again! 🤯

(ns meander-test.core
  (:require [meander.epsilon :as m]))

(defn extract
  [m]
  (m/match m
    {:letters (m/or
               (m/and nil
                      ?a
                      ?b
                      ?c
                      ?d
                      ?e
                      ?f
                      ?g
                      ?h
                      ?i
                      ?j
                      ?k
                      ?l
                      ?m
                      #_?n)
               {:a ?a
                :b ?b
                :c ?c
                :d ?d
                :e ?e
                :f ?f
                :g ?g
                :h ?h
                :i ?i
                :j ?j
                :k ?k
                :l ?l
                :m ?m
                :n ?n})}
    {:a ?a}))

xiongtx06:05:01

No 💡 why it does this but boy was this one a doozy to 🔨 🐛 !