Fork me on GitHub
#meander
<
2020-12-15
>
noprompt01:12:57

Some other things on the way that I will follow up with after the interpreter code • fresh which allows for fresh variables in the usual logic programming sense • project which allows for the current state of variables to be queried (actually way cooler than this) Example of fresh:

(m/match [1 2]
  [(m/fresh [?x]
     ?x)
   ?x]
  ?x)
;; => 2
Example of project:
;; (m/project <yield-pattern> <query-pattern> <value-pattern>)
;;
;; Attempts to yield an object described by yield-pattern using the
;; current bindings and match that object with query-pattern. If
;; successful, attempts to match the target object with value-pattern
;; if querying, or yield the object if yielding. Global bindings are
;; not altered when yielding the yield-pattern object.
(m/search 10
  ;; Capture two 10 in !x
  ;;   Bindings: {!x [10 10]}
  ;; Project the vector [!x !x] e.g. [10 10]
  ;; Match against [?x ?y]
  ;;   Bindings: {!x [10 10], ?x 10, ?y 10}
  ;; Match 10 against ?x
  (m/and !x !x (m/project [!x !x] [?x ?y] ?x))
  {:!x !x, :?x ?x, :?y ?y})
;; => ({:!x [10 10], :?x 10, :?y 10})

Lucy Wang14:12:32

So fresh is like a block scope, in that case would m/scoped be slightly better than m/fresh ?

noprompt16:12:52

I had a similar thought myself. 🙂 I’ve also been considering some other vocabulary words such as “local”, “ephemeral”, etc. to more accurately reflect the spirit. I will give this some more consideration!

jimmy19:12:01

I love the idea of not going with fresh. Always confused me. I think scoped, but local makes the most sense to me.

noprompt19:12:52

Lucy, what do you think of local? Also, sending to channel for feedback.

Lucy Wang09:12:08

+1 for local, even better than scoped!

👍 3
noprompt01:12:40

Both of these will be usable on both sides.

dgr22:12:52

I’m working on editing the Meander documentation. Can somebody give me more background on the differences between match and find? I understand the difference between match and search, but the only difference between match and findseems to be that I can use some pattern operations like scan in find, but not in match. Is that it? If so, why not just use find everywhere and do away with match? I assume that there is some sort of performance difference between the two, but it isn’t really mentioned. Is there anything else that find can do that match can’t, or vice versa?

👏 3
jimmy22:12:08

Match is unambiguous. We aren’t searching to find a way to satisfy the pattern. We are just matching very directly what was given. Given that we aren’t searching, match does have in general better performance characteristics. Find can do anything match can do. But match can’t use things that require searching to satisfy the pattern. Another way of putting it is that match allows there to only be exactly one match. Find allows there to be more than one, but selects the first.

jimmy22:12:37

Also, thanks for any edits you are doing. Always wonderful to have people help out with that 🙂

noprompt22:12:52

LOL just backspaced a bunch.

🙂 3
noprompt22:12:10

The semantics of find are basically (first (search ,,,)) but compiles to code specifically designed to find the first solution more efficiently.

dgr22:12:14

So, define “searching” in the way that Meander uses the concept. How is what match does not searching while what search/`find` do is searching.

dgr22:12:30

Yes, I get the diff between search and find. Totally makes sense.

dgr22:12:24

It’s the diff between match and find (and search) that has me scratching my head.

noprompt22:12:32

match is searching in the traditional sense of pattern matching found in virtually every language that has it. Patterns must be unambiguous and match throws when there is no default and no match; pretty much core.match style.

noprompt22:12:58

Both search and find allow for ambiguous patterns and neither complain when there is no solution found.

dgr22:12:18

What does “ambiguous” mean? Just that more than one pattern matches?

dgr22:12:48

OK, good point that neither search or find complain about nothing found. That’s important.

noprompt22:12:56

An example of an ambiguous pattern would be {?k ?v} or {?k ?v, ?v ?k} because there is no one solution given a satisfactory map of the right shape.

dgr22:12:52

Hm. So how does it deal with {?k ?v}? What are the variables bound to?

noprompt22:12:09

So a map like {:x 1 :y 1} in the first example could bind ?k to either :x or :y.

dgr22:12:30

Got it. So does it just choose one?

dgr22:12:38

The first?

noprompt22:12:47

find does, search returns both answers.

noprompt22:12:59

find just picks the first one blindly.

noprompt22:12:11

Depth first.

dgr22:12:24

OK, make sense. search returns both and find returns whatever search would find first.

noprompt22:12:59

Another example would be [_ … ?x . _ …] aka (m/scan ?x). Some ?x is in there.

noprompt22:12:13

But you don’t want to call first on search because and prefer find because find avoids making collections etc.

noprompt22:12:28

Or, at least, it tries to avoid them as often as possible.

👍 3
dgr22:12:50

OK, got it. So basically, match wants things to have a single possibility. search is willing to have multiple possibilities, which it can deal with because it returns all of them. And find is effectively an optimized version of (first (search …)).

👍 3
dgr22:12:58

Thanks for the help. Another question so another thread. It seems like search (and find) are almost Prolog-like in how they accomplish the search. It seems it’s performing backtracking in some sort of search tree across the whole pattern(s). Is that a good way to think about it?

dgr22:12:28

Does Meander return all results from search in a list instead of a lazy seq? In other words, does one ever have to worry about laziness, or is that a non-issue?

noprompt22:12:53

It’s lazy.

noprompt22:12:03

So, don’t wrap it in binding 🙂

noprompt22:12:44

find and match are, OTOH, eager.

dgr22:12:00

Is it? I see: (class (m/search 1 1 :one)) ;; => clojure.lang.PersistentList

noprompt22:12:13

That is probably not reliable.

noprompt22:12:28

(let [x {:a 1 :b 2 1 :a :d 4}]
  (class
   (m/search x
     {?k ?v ?v ?k}
     x)))
;; => clojure.lang.LazySeq

dgr22:12:29

Yep: (class (m/search [1 2 3 4] (m/scan ?x) ?x)) ;; => clojure.lang.LazySeq

👍 3
dgr22:12:16

So, what can we say about the return type? It conforms to sequential? ??

noprompt23:12:58

Yeah, I guess. Or that it matches the pattern (_ …).

noprompt23:12:20

This the spec

(s/fdef meander.match.epsilon/search
  :args (s/cat :expr any?
               :clauses :meander.match.epsilon.match/clauses)
  :ret (s/coll-of any? :kind sequential?))

dgr23:12:25

Seems like it returns: nil (no match), PersistentList, and LazySeq, depending on the case.

noprompt23:12:34

From meander.match.specs.epsilon

dgr23:12:48

OK, so it’s basically a sequential?.

noprompt23:12:15

BTW, specs are mostly in these “private” namespaces.

dgr23:12:26

Should it return () for no match, instead of nil?

dgr23:12:36

OK, I can take a look at the specs.

dgr23:12:36

(m/search 0 1 :one) ;; => nil

dgr23:12:34

and (sequential? nil) is false

noprompt23:12:59

Ah, well I guess then the spec is wrong. 🙂

dgr23:12:13

LOL. 🙂 I just gave you another unit test.

noprompt23:12:35

But, yeah, sure.

noprompt23:12:19

Heh, I’m pretty sure would could figure out why its not a list because, I would actually agree there is merit to it having a () output at minimum.

jimmy23:12:29

Yeah, but it is also a breaking change. Just imagine it in a when

jimmy23:12:20

nil seems fine. And probably best not to change it now.

jimmy23:12:46

We can fix the spec though

dgr23:12:50

You want me to file a low priority bug? I don’t know that it would cause a problem very often, but it seems like empty list is better than nil.

noprompt23:12:48

I agree. Yes you can file the ticket but here’s the thing: I really want to stop working on epsilon

dgr23:12:52

Basically, a user should be checking for no result with (empty? (m/search …), right?

dgr23:12:10

Understood. Do you have a way to tag it for zeta?

noprompt23:12:36

In fact, I would like to personally stop working on epsilon, take the stuff that I like to zeta, integrate it and start building there.

noprompt23:12:45

You can tag it as both.

noprompt23:12:56

Because I think it should be fixed.

noprompt23:12:04

I agree with your position. 🙂

dgr23:12:06

OK, I’ll file it.

noprompt23:12:32

Pointing out these flaws keeps me motivated.

dgr23:12:35

Corner cases. Gotta love ’em. :face_with_rolling_eyes:

dgr23:12:59

I’m a product manager for my day job, but I should have been a tester.

noprompt23:12:12

Seriously the best way to see commits on the project is to come here and point out a thing or two 😅

noprompt23:12:20

Semantics are important!

dgr23:12:29

BTW, did you see my question about Meander search being sort of Prolog like? Did that make sense?

noprompt23:12:06

Yes it does. search is basically like unify with a ground term.

dgr23:12:45

OK, cool. Then it wasn’t my imagination. 🙂

noprompt23:12:20

It’s kinda like conde 😎

dgr23:12:21

And scan is something close to member/3 in Prolog.

noprompt23:12:33

Lemme check

noprompt23:12:39

Not member/2? I just did a quick search and that’s all I see from the SWI docs.

dgr23:12:56

Sorry, yea, member/2.

noprompt23:12:14

Yes but it’s more like searching for a subsequence.

noprompt23:12:38

(m/scan ?x ?y ?z) ~= [_ ... ?x ?y ?z . _ ...]

dgr23:12:46

Right. It’s the backtracking that got me. So, even if you only have one pattern, you’re doing backtracking within that pattern if you’re using scan.

noprompt23:12:53

Or rather particular elements of a subsequence.

dgr23:12:26

And you’re calling the expression every time. Backtracking. Rebinding logic vars. Then calling the expression. And so on.

noprompt23:12:47

Of course, we try to find the best way to create the smallest search space to minimize that by exploiting what we know.

dgr23:12:24

BTW, I found something else while I was cooking up more examples for the docs.

noprompt23:12:27

Yes, but again, we try to minimize that.

dgr23:12:07

(m/search {:a [1 2] :z [:x :y]} {:a (m/scan ?a) :z (m/scan ?z)} [?a ?z]) ;; => ([1 :x] [1 :y] [2 :x] [2 :y]) (m/search {:a [1 2] :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10 :z [:x :y]} {:a (m/scan ?a) :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10 :z (m/scan ?z)} [?a ?z]) ;; => ([1 :x] [2 :x] [1 :y] [2 :y])

noprompt23:12:32

Both patterns are looking for all possible combinations of ?a in [1 2] at :a and ?z in [:x :y] at :z.

dgr23:12:32

Depending on the number of items in a map literal, Clojure will either put them in a PersistentArrayMap or a PersistentHashMap. If it chooses an array map, then the order is as the user writes them. If hash map, then it’s whatever the hash order is.

noprompt23:12:48

And there is no way around that unfortunately.

dgr23:12:53

That changes the order that Meander does the search and changes the order of the output.

dgr23:12:59

It’s not a problem.

noprompt23:12:01

It can, yes,

noprompt23:12:09

However, I have been thinking that is actually biased.

dgr23:12:13

I just point that out in the (new) docs as something to be aware of.

dgr23:12:20

It will affect find, for instance.

noprompt23:12:22

We have ticket for the problem

noprompt23:12:29

No solution yet.

dgr23:12:45

Oh, you already have a ticket. Cool. No worries then.

noprompt23:12:53

But I’ve been thinking a way to solve it would be to have a method of being able to tell the interpreter/compiler how to produce the space.

dgr23:12:42

Most of the time it shouldn’t matter. And as long as it’s documented, I think it should be OK. I added a short section to the docs that shows the issue and basically says “Don’t rely on the ordering.”

dgr23:12:26

I gotta run and get dinner on the table for my kids. TTYL. Thanks for the help. That was great. I’ll file the bug shortly and I’ll submit a PR with doc updates in a bit.