meander

All things about https://github.com/noprompt/meander Need help and no one responded? Feel free to ping @U5K8NTHEZ
Lucy Wang 2020-09-24T09:10:11.001300Z

Hey channel, I worked on it for a while, but failed to find a solution. How could I achieve this with meander:

;; original
(def data
  {:people [{:name :john
             :age  10}
            {:name :jen
             :age  11}
            {:name :jack
             :age  12}]
   :bonus  [{:name   :john
             :amount 100}
            {:name   :jack
             :amount 200}]})

;; wanted
{:people [{:name         :john
           :bonus-amount 100
           :age          10}
          {:name :jen
           :age  11}
          {:name         :jack
           :bonus-amount 200
           :age          12}]
 :bonus  [{:name   :john
           :amount 100}
          {:name   :jack
           :amount 200}]}
It's very like a join of two tables.

Lucy Wang 2020-09-24T09:16:19.001800Z

one version is this, but it returns a flattened list

(me/search data
  {:people (me/scan {:name ?name :as ?person})
   :bonus (me/scan {:name ?name :amount ?amount})}
  
  {:people (assoc ?person :bonus-amount ?amount)})
;; => ({:people {:name :john, :age 10, :bonus-amount 100}}
;;     {:people {:name :jack, :age 12, :bonus-amount 200}})

Lucy Wang 2020-09-24T09:30:07.003400Z

And a closer but incorrect version using memory variables

(me/rewrite data
  {:people [{:name !name :as !person} ...]
   :bonus [{:name !name :amount !amount} ...]}
  
  {:people [{:bonus-amount !amount & !person} ...]})
;; => {:people
;;     [{:name :john, :age 10, :bonus-amount 100}
;;      {:name :jen, :age 11, :bonus-amount 200}]}

Lucy Wang 2020-09-24T09:48:32.004700Z

ok, an ugly solution of a combo of search+match, but it works

(-> (me/search data
       {:people (me/scan {:name ?name :as ?person})
        :bonus (me/scan {:name ?name :amount ?amount})}
  
       {:people (assoc ?person :bonus-amount ?amount)})
     (me/match 
       ({:people !people} ...)
       {:people !people
        :bonus (:bonus data)}))
;; => {:people
;;     [{:name :john, :age 10, :bonus-amount 100}
;;      {:name :jack, :age 12, :bonus-amount 200}],
;;     :bonus [{:name :john, :amount 100} {:name :jack, :amount 200}]}

Lucy Wang 2020-09-24T10:05:04.004900Z

created an issue in GH: https://github.com/noprompt/meander/issues/137

unbalanced 2020-09-24T13:22:52.006100Z

Struggling to understand this behavior:

(m/match [3 4 5 6 7 8]
  [3 4 . !xs !ys ...]
  [!xs !ys])
;; =>
[[5 7] [6 8]]
my intuition tells me this should be
=> [5, [6, 7, 8]]

unbalanced 2020-09-24T13:23:37.006600Z

ah except maybe that would be for [3 4 . ?xs !ys ...] instead

unbalanced 2020-09-24T13:24:07.007Z

I think it's the behavior of !xs !ys that works on every other number that is confusing.

jimmy 2020-09-24T13:37:16.007200Z

Hey lucy. Will get back later today on this one. Sorry have been a bit busy lately

Lucy Wang 2020-09-24T13:37:42.007400Z

Thanks @jimmy!

jimmy 2020-09-24T13:39:50.009Z

The dot there is telling it where to stop the repeating. So you are asking it to repeat !xs !ys over and over again. That is why it is giving you pairs. If you moved the dot like this [3 4 !xs . !ys ...] now only the !ys are repeating and you'd get [[5], [6, 7, 8]]

Lucy Wang 2020-09-24T13:43:04.010200Z

yeah, my understanding is that ... looks to its left until it sees either a dot or the start of the current list/vector, and repeats everything in between

👍 1
unbalanced 2020-09-24T13:43:32.010800Z

ok that makes sense. But why does it do the sliding window like that? i.e. why not [[5 6] [7 8]]? Ah, is it because it scans two by two?

Lucy Wang 2020-09-24T13:46:03.012Z

yeah, it's "interleaving", so [1 !xs ...] matches [1 2 1 3 1 :foo 1 :bar]

unbalanced 2020-09-24T13:47:50.013500Z

😮 fascinating! so it's kind of like a flattened cross product. And [1 !xs] would be [[1 2] [1 3] [1 :foo] [1 :bar]]?

Lucy Wang 2020-09-24T13:47:57.013700Z

speaking in a regex way, it's like (1.)*

Lucy Wang 2020-09-24T13:48:24.014200Z

in the RHS, !xs would be [2 3 :foo :bar]

unbalanced 2020-09-24T13:48:29.014500Z

ahhh and with re-search

unbalanced 2020-09-24T13:48:50.014700Z

or perhaps re-find

Lucy Wang 2020-09-24T13:49:14.014900Z

but you can also use [1 !xs ...] on the RHS to expand it to [1 2 1 3 1 :foo 1 :bar], but not [[1 2] [1 3] [1 :foo] [1 :bar]]

jimmy 2020-09-24T13:49:29.015300Z

Yeah, !xs capture one value, the !ys capture one value, so on and so forth. So you are seeing what is in the !xs and the !ys. Using rewrite you can get the back out in the same order.

(m/rewrite [3 4 5 6 7 8]
  [3 4 . !xs !ys ...]
  [!xs !ys ...])
;; =>
[5 6 7 8]

👍 2
unbalanced 2020-09-24T13:52:58.016700Z

sweet! 🤓 thanks for clarifying

noprompt 2020-09-24T16:30:15.017700Z

https://clojars.org/meander/epsilon/versions/0.0.492

noprompt 2020-09-24T16:31:03.018900Z

This new release improves performance for scan-like patterns such as _ … p1 ,,, pN . _ ….

noprompt 2020-09-24T16:31:14.019100Z

Significantly.

noprompt 2020-09-24T16:39:45.022600Z

I think it would be nice to have an “unsafe” notation which tells the compiler to assume the type is known. I’m not sure what it should look like, however. Maybe something like

^::m/unsafe {:assumed-to-be-a-map? true}

noprompt 2020-09-24T16:41:15.023400Z

You can the motivation for this idea here toward the bottom of my reply: https://github.com/noprompt/meander/issues/135#issuecomment-698215991

unbalanced 2020-09-24T17:46:42.024900Z

I'm new to this whole thing but my leaning would be towards assuming it's a map by default based on the notation? Or at least supports the associative abstraction

unbalanced 2020-09-24T17:48:26.025300Z

we're talking about this case, right?

(m/find data
     {:people (m/scan {:name ?name :id ?id :inactive true})
      :addresses (m/scan {:person-id ?id :as ?address})}
     {:name ?name
      :found 1
      :address ?address}))

jimmy 2020-09-24T17:50:28.027700Z

The idea here is that right now the generated code explicitly checks if the data you pass in is passes the map? predicate. If it did not check this and you pass something other than a map, it could error or give weird results. But sometimes you want that extra speed and know for sure you are giving us a map.

unbalanced 2020-09-24T17:52:06.028800Z

yep -- just trying to imagine a scenario (besides a GI/GO situation) where someone would pass in something besides an associative abstraction and expect valid results without specifying the type

jimmy 2020-09-24T17:53:33.030200Z

You might have some heterogeneous set of data and the whole thing you are doing with meander is trying to find the bits that actually match the shape you specify.

unbalanced 2020-09-24T17:55:46.031300Z

hmmm

unbalanced 2020-09-24T17:57:09.033400Z

two thoughts on that... and maybe I'm overly hostile, but first I would wonder if want to go that route is try/catch cheaper than (if (map? ...) ...) , and the second perhaps overly hostile perspective would be, isn't it on the user to make sure the sequence is full of things that support the associative abstraction?

jimmy 2020-09-24T17:57:09.033500Z

But also we like to have safety by default. And changing that default would be a breaking change which we don't do.

unbalanced 2020-09-24T17:57:16.033800Z

aha

unbalanced 2020-09-24T17:57:22.034100Z

that's a fair point

jimmy 2020-09-24T17:59:08.034200Z

Might be nice to just have an elide-collection-type-checks all together as well.

1
jimmy 2020-09-24T18:02:53.039100Z

One of the benefits of pattern matching is that you get some of the benefits of static types without some of the downsides. In dynamic languages you can accidentally do some operation on the wrong type of thing and get weird results. (like contains? On a vec in clojure). Meander let's you be clear about those types through the data literals. If we didn't check the types what the code does now becomes unclear. But at the same time, we want people to be able to opt-in to more performant code when needed.

unbalanced 2020-09-24T18:04:41.040300Z

From an API perspective, I might think having an "unsafe" set of namespaces would be preferable to cluttering up notation with the type hinting

unbalanced 2020-09-24T18:06:37.043500Z

(that's without having a clue about the challenge of implementing that... yet!)

jimmy 2020-09-24T18:06:42.043600Z

Well sense we use data literals you'd then have to opt-out of all safety or forgo the data literals syntax instead of being able to do it piecemeal with an existing match. Maybe you want the vector check but not the map one. We should support both.

jimmy 2020-09-24T18:07:54.044700Z

Improving the performance of your code should not require you to go require something else. Just modify your code in place.

unbalanced 2020-09-24T18:17:54.046500Z

totally get it. In my mind I'd opt out of all safety if that were an option b/c that's just what I'm used to -- if I feed in the wrong spec it's my fault. But not sure that's a healthy attitude haha. I mean, if I had to trade safety for clean notation and speed, that would be my vote

unbalanced 2020-09-24T18:18:54.047300Z

but maybe that's cause I come from a crazy Python/cljs background where that's always the case

noprompt 2020-09-24T18:19:39.047900Z

It’s primarily unhealthy when the assumptions unchecked code makes are broken by code changes upstream.

unbalanced 2020-09-24T18:20:01.048600Z

yes, agree -- need to maintain backwards compatibility

noprompt 2020-09-24T18:20:45.049500Z

So, from this perspective, Meander aims to be safe by default.

unbalanced 2020-09-24T18:21:17.050500Z

yep... 100% understand that, I thought we were discussing unsafe options

noprompt 2020-09-24T18:21:50.051Z

But as I point out in the issue there’s a huge performance savings to be had if you know there is virtually zero risk in eliding the check.

unbalanced 2020-09-24T18:26:25.053900Z

mhm. What I'm advocating for is that for a user like me, I'd be putting {:unsafe ...} next to EVERYTHING

noprompt 2020-09-24T18:26:42.054700Z

Oh, well, no one wants that. 🙂

noprompt 2020-09-24T18:27:13.056200Z

But to get to where we don’t have to do that, we actually have to start from that position where we do.

unbalanced 2020-09-24T18:27:25.056400Z

ahh excellent point

noprompt 2020-09-24T18:27:55.056800Z

Verbosity is the first step toward abstraction.

🤯 1
🔥 1
☯️ 1
unbalanced 2020-09-24T18:28:07.057Z

!!!

unbalanced 2020-09-24T18:28:12.057200Z

I need to meditate on that one

unbalanced 2020-09-24T18:40:45.058400Z

Does meander have transducer/xfn variations of the m/* series yet? Would that be a welcome contribution?

noprompt 2020-09-24T18:42:25.059800Z

I think m/search could possibly return a transducer instead of results. I’ve looked into this and requires a bit of work.

unbalanced 2020-09-24T18:43:14.061100Z

It would probably just be sugar on top of something like...

(comp 
  (map (fn [data] (m/search ...))
  cat)
or the like.

noprompt 2020-09-24T18:43:18.061200Z

The transducer version of mapcat throws a bit of wrench into the mix.

unbalanced 2020-09-24T18:47:40.062600Z

is that b/c of possible thrown exceptions?

noprompt 2020-09-24T19:04:57.064700Z

No. It’s because an infinitely deep search space can prevent the transducer from ever returning anything.

noprompt 2020-09-24T19:09:23.069300Z

It’s a solvable problem. Just like the last patch, it probably will involve a way of indicating that a something has a particular property that is salient and compilation can be adjusted accordingly i.e.

(let [[min max] (search-space-depth-range pattern environment)]
  (infinite? max))

unbalanced 2020-09-24T19:14:43.069500Z

aha

unbalanced 2020-09-24T19:15:16.069900Z

m/search itself could be infinite depending on the pattern?

noprompt 2020-09-24T19:24:14.073700Z

Yes. At the moment, I don’t think this is really an issue but in the future there will be several patterns that have infinite spaces. For example,

(m/sum ?a ?b)
finds all possible values ?a and ?b such that (+ ?a ?b) equals the target.

unbalanced 2020-09-24T19:24:29.074Z

ohhhhh nice! I didn't know that was a thing

noprompt 2020-09-24T19:24:34.074200Z

It isn’t yet.

noprompt 2020-09-24T19:24:57.074800Z

I’ve been hacking on it on the side though and it will appear in the next version of the library.

unbalanced 2020-09-24T19:26:31.076900Z

that's fascinating -- I've been thinking about the synergy between meander and core.logic

noprompt 2020-09-24T19:27:00.077600Z

Along with things like

(long ?min ?max)
which matches a long between ?min and ?max, and binds ?min and ?max . Also, this pattern can yield long values in the range of ?min and ?max.

unbalanced 2020-09-24T19:27:32.078400Z

mhm. Without looking at internals, I suppose converting that to a lazy sequence would be quite difficult?

unbalanced 2020-09-24T19:27:45.078900Z

because then an infinite sequence is no problem

noprompt 2020-09-24T19:29:41.080900Z

Not especially. If we know either or both of ?min and ?max matching is pretty easy. If we know neither it’s also not so bad: we simply produce the space which is all possible combinations of long values such that (<= ?min TARGET ?max) where TARGET is the value being matched.

unbalanced 2020-09-24T19:31:40.082600Z

Well that's certainly an acceptable option as well

noprompt 2020-09-24T19:31:46.082800Z

The concepts Meander is based on are very similar to the ones found in something like core.logic but not quite.

noprompt 2020-09-24T19:32:42.084600Z

Essentially, matching/searching is unification with the exclusions that variables cannot be unified. IOW, variables must always be unified against a ground value.

unbalanced 2020-09-24T19:32:44.084800Z

It's all some dark magic to me 😄 I'm still reworking through the end of SICP section 4 where they wire up that stuff from scratch

unbalanced 2020-09-24T19:34:19.086900Z

What I'm not understanding is (and possible it's just b/c lacking theory) how is it possible to stop at an arbitrary number and not pause/continue the computation to make it lazy

noprompt 2020-09-24T19:34:21.087Z

Another difference, is that Meander has been evolving (I think) a very different concept of a variable that is much more like a Clojure var than how LP normally thinks of them.

noprompt 2020-09-24T19:37:37.088300Z

The idea is that binding is essentially a reduction which can fail.

noprompt 2020-09-24T19:38:00.089Z

And unbinding or yielding is unfolding which can also fail.

noprompt 2020-09-24T19:39:08.089800Z

This process is independent from whatever it’s name happens to be.

unbalanced 2020-09-24T19:39:51.091100Z

Interesting... I have much to learn. Well I think an easy low hanging fruit would be to target things that for sure terminate, like m/match

noprompt 2020-09-24T19:40:32.092500Z

It’s too bad this is the free Slack. I’m sure many of the long time members in this channel are like, “yeah, we’ve heard this before…” 😛

unbalanced 2020-09-24T19:40:42.092900Z

:rolling_on_the_floor_laughing:

noprompt 2020-09-24T19:40:57.093300Z

But, I’m always happy to share my thoughts if helps/confuses/inspires. 🙂

unbalanced 2020-09-24T19:41:18.093600Z

Super inspired! I love being the new guy haha.

unbalanced 2020-09-24T19:41:54.094100Z

Where did you get the idea for this, anyway? I can't find a single precedent like it. Closest I can see is datalog and core.logic

unbalanced 2020-09-24T19:43:18.094600Z

I mean I've seen other data transformation languages but none with the declarative/data-literal syntax

jimmy 2020-09-24T19:46:16.096500Z

Replied on the card. Hopefully there is something helpful in there.

👍 1
noprompt 2020-09-24T19:49:04.099Z

A couple things pushed me in this direction: • Pattern matchers • Logic Programming • Math notation • Data literals • Regular expression But I think the biggest one was noticing a lot of asymmetry in how we manipulate data. We take data apart differently from how we put it together.

noprompt 2020-09-24T19:51:45.100500Z

I thought it would be cool to see if I could make something that really emphasized symmetry and played to our mutual strengths to recognize patterns.

👍 1
noprompt 2020-09-24T19:55:27.103900Z

The other thing I noticed is that, after programming in Clojure for almost 10 years, I’m not getting better at reading arbitrary Clojure programs. I still have to slow down and mentally unpack even modestly complex code.

👍 1
noprompt 2020-09-24T19:56:31.105700Z

So, yeah, I sort of feel like I’m always haunted by this question of “shouldn’t we be able to skip all this these days and more or less draw a picture of what we want?”

unbalanced 2020-09-24T19:56:32.105800Z

I saw @jimmy’s talk and it made me realize I'm not getting better at it either. The only thing that's increased is my code reading "pain tolerance" hahaha

noprompt 2020-09-24T19:56:44.106200Z

It’s true!

unbalanced 2020-09-24T19:57:37.107300Z

the data literal syntax is very powerful because it plugs into our visual cortex system, which is infinitely more powerful than whatever systems process functional abstractions

unbalanced 2020-09-24T19:58:04.107800Z

I was actually really surprised that I didn't find a fully defined language based on this

unbalanced 2020-09-24T19:58:49.108900Z

Pattern matching, yes... but data transformation, no

unbalanced 2020-09-24T19:59:25.110Z

I actually think there is a lot of excellent opportunity for serious academic work here, too

noprompt 2020-09-24T19:59:55.110900Z

I always use regular expression as a point here. If you master the language, you often do not have the problem of wondering what an arbitrary regular expression means. So I’m emphasizing semantics here. I’m not saying there aren’t exceptions to this point but, in general, I think it’s fare to say regular expression has this property of remarkably clear semantics.

noprompt 2020-09-24T20:01:07.112Z

We do have this trouble with code. And this makes sense because code is meant to encode semantics in a general way.

unbalanced 2020-09-24T20:01:30.112600Z

think, for instance, matrix operations. Trying to find a single matrix that is equivalent, for instance, to the multiplication of three matrices.

noprompt 2020-09-24T20:01:52.113500Z

Actually, there is a really cool Python matcher that can do that. ☝️

noprompt 2020-09-24T20:02:02.113900Z

I would love to steal that technology.

unbalanced 2020-09-24T20:02:40.114300Z

The same analogy could be extended to meander transformations. What single meander pattern could be compiled from 3 meander patterns?

unbalanced 2020-09-24T20:02:49.114500Z

seems like a pretty interesting study to me

unbalanced 2020-09-24T20:03:10.114600Z

what are you referring to? I can help steal/translate 🙂

noprompt 2020-09-24T20:08:22.114800Z

https://github.com/HPAC/matchpy

unbalanced 2020-09-24T20:15:10.115200Z

Do you have a contributor doc anywhere?

noprompt 2020-09-24T20:27:21.116700Z

I do not. Mostly that is due to the fact I have to make compromises about my time.

noprompt 2020-09-24T20:28:25.118500Z

But, also, if anyone wants to contribute I’m much better at an organic conversation that tells someone where everything is than I am translating that into a document.

noprompt 2020-09-24T20:28:40.119100Z

I need a spirit medium like @jimmy for that sort of thing. 🙂

unbalanced 2020-09-24T20:29:07.119500Z

Fine by me! I'm thinking as a rough mockup of a match-xfn something like this:

;; psuedocode
(defpsuedomacro matcher [{ex-handler :ex-handler alt-nil :alt-nil :as opts} prop-list]
 (comp 
   (map (fn [x]
          (try
            (m/match x ~@plist)
            (catch Exception e
              (if (some? ex-handler)
                (or (ex-handler e) alt-nil)
                alt-nil
                )))))
   cat
   (remove #{alt-nil})))

noprompt 2020-09-24T20:31:32.121100Z

I think the best thing to do if you have ideas is to share them on Github. @timothypratley and others have shared some fantastic ideas there.

unbalanced 2020-09-24T20:31:42.121400Z

Sweet! Will do

noprompt 2020-09-24T20:32:17.122100Z

Ideas generally take a lot of thinking about and it’s nice having several on the table to see if there are common threads among them to be addressed with a unifying solution.

noprompt 2020-09-24T20:32:58.122800Z

For example, we’ve had a lot back and forth about aggregation and that has led to the model of binding I mentioned earlier.

noprompt 2020-09-24T20:33:25.123500Z

It’s not ready yet but I think it would have taken longer to come up with a solution without people sharing their ideas.

noprompt 2020-09-24T20:34:33.124800Z

I think that’s is one of the things I’m most proud of being a member in this channel is the encouragement for people to discuss their ideas, criticisms, etc.

unbalanced 2020-09-24T22:10:57.125800Z

Awesome. Just need a clojure transliteration?

Lucy Wang 2020-09-24T23:00:36.126900Z

Am I misusing it, or it's cata doesn't work with & ?rest on a map?

(me/match {:a 1 :b 2}
  {:a ?a & (me/cata ?rest)}
  {:aa ?a :rest ?rest}

  {:b ?b}
  {:bb ?b}) ;; => stackoverflow!

jimmy 2020-09-24T23:01:32.128Z

Change it to (me/some ?a)

Lucy Wang 2020-09-24T23:01:38.128200Z

aaah!!

Lucy Wang 2020-09-24T23:02:12.129500Z

it works :bananadance:

😄 1
jimmy 2020-09-24T23:02:16.129700Z

It will still match with nil even if the key doesn't exist. This is based on some of clojures behavior and something we have considered changing in zeta.

👍 2
jimmy 2020-09-24T23:03:28.131500Z

We also have thought about detecting things like this in some sort of linting like mode to help people diagnose these problems.

Lucy Wang 2020-09-24T23:03:35.131700Z

is it worth to contribute this case to the doc? I think it's a pretty common

jimmy 2020-09-24T23:04:41.132300Z

Definitely :)

jimmy 2020-09-24T23:05:02.133Z

I've also found keeping my base case as the first match helps prevent me from doing this.

💯 2
Lucy Wang 2020-09-24T23:06:56.134200Z

emm ... maybe not, in my example above if I reorder the two clauses, the result would be short circuited by the {:b ?b} match and never gets to the other one

jimmy 2020-09-24T23:08:36.135300Z

Yeah not always the case. Just a general rule of thumb. In this case you definitely can't do it.

Lucy Wang 2020-09-24T23:10:03.136100Z

sgtm!

jimmy 2020-09-24T23:23:30.136200Z

In this case it does avoid the stack overflow and hints at the fact that the patterns are completely overlapping.