@timothypratley Here’s an equivalent example just using meander.epsilon/search
.
(m/search (fetch-as-hiccup company-directory-page)
(m/$ [:div {:class "directory-tables"}
. _ ...
[:h3 _ ?department & _]
_
[:table &
(m/$ [:tbody &
(m/scan [:tr
. _ ...
[:td {} & (m/scan [:a {} ?name & _])]
_
[:td {} ?title & _]
_
[:td {} _ [:a {:href ?mailto} & _] & _]
& _])])]
& _])
{:department ?department
:name ?name
:title ?title
:email (subs ?mailto 7)})
I think removing unnecessary whitespace that is left in with the parse could help make this less finicky/messy. I didn’t do that just to show how to do the search with the exact same input. I used &
a lot. (Which if it isn’t documented I will write up something tomorrow afternoon). One trick I did was to keep a & ?rest
around as I was writing it so I could see what was left for me to search for. I used $
to jump into the nested structure to the points I cared about.
If I were doing a lot of hiccup things I might use defsyntax to make something like tag
, but I think the key really would be just doing preprocessing and stripping out whitespace. Would make everything cleaner.oh wow, nice!!!
I noticed in the source code for epsilon there is a sibling syntax to $
called $*
what does that do?
If memory serves, the context argument is a function so you can apply the function to the context. But honestly not 100% sure
I took a shot at creating my own syntax for “find these things in order ignoring before, between and after”
And along the way I got confused by this behavior:
(m/search [:a [1 "a"] :b [2 "b"] :c [3 "c"] :d]
(m/scan [1 ?a] :b [2 ?b] :c [3 ?c])
[?a ?b ?c])
;;=> (["a" "b" "c"])
^^ so far so good, that just proves that m/scan
worksBut then:
(m/defsyntax scan'
[& patterns]
(case (:meander.syntax.epsilon/phase &env)
:meander/match
(clojure.core/let [inner `(~@'(_ ...) ~@patterns ~@'(. _ ...))]
`(seqable ~@inner))
;; else
&form))
(m/search [:a [1 "a"] :b [2 "b"] :c [3 "c"] :d]
(scan' [1 ?a] :b [2 ?b] :c [3 ?c])
[?a ?b ?c])
=> nil
^^ this is copying m/scan
definition to scan'
but now it fails to matchI know the syntax is being called because I added prn
debugging
Oh gosh ignore all that, it should be m/seqable
<-- didn’t realize that was a syntax
[meander/epsilon "0.0.340"]
Thanks @timothypratley and @jimmy! 👍
So here’s my implementation:
(m/defsyntax scan*
"Match patterns in order ignoring things around and between them.
_ ... p1 . _ ... p2 . _ ... ,,, pn . _ ...
where `p1` through `pn` are `patterns`."
[& patterns]
(m/rewrite patterns
;; convert patterns to memory variables
(!pattern ...)
;; substitute patterns interspersed with ignore anything
(`m/seqable .
'_ '... !pattern '. ...
'_ '...)))
@timothypratley There’s something like that currently called separated
.
but it produces: => ([“a” “b” “c”] [“a” “b” “c”] [“a” “b” “c”] [“a” “b” “c”])
https://github.com/noprompt/meander/blob/epsilon/src/meander/epsilon.clj#L355
(meander.epsilon/seqable _ ... [1 ?a] . _ ... [2 ?b] . _ ... [3 ?c] . _ ...)
oh right…
;; Prevent producing the same search results twice when ;; the target is a vector. Sounds relevant…
I think the implementation could simply rely on seqable
when the problem I mentioned about or
in the seqable
implementation goes away.
Hmmm same problem:
(m/search [:a [1 "a"] :b [2 "b"] :c [3 "c"] :d]
(m/separated [1 ?a] [2 ?b] [3 ?c])
[?a ?b ?c])
=> (["a" "b" "c"] ["a" "b" "c"] ["a" "b" "c"] ["a" "b" "c"])
The output sould be (["a" "b" "c"])
as there is only one way to match the pattern
Not exactly.
Well, hmm…
Yeah, thats probably a bug.
(m/search [1 2 3 4 5]
[1 . _ ... 2 . _ ... ?x & _]
?x)
=> (3 4 5 4 5 5)^^ this seems like a good example without any syntax stuff
want me to open an issue?
Yeah that makes sense though because the width of the last _ …
grows to the point where the & _
ends up nothing.
(m/search [1 2 3 4 5]
[1 . _ ... 2 . _ ... ?x & ?rest]
[?x ?rest])
;; =>
([3 [4 5]] [4 [5]] [5 []] [4 [5]] [5 []] [5 []])
the answer should be (3 4 5) surely?
And, yes, do open an issue on that
(m/search [1 2 3 4 5]
[1 . _ ... 2 . !part-a ... ?x . !part-b ...]
{:part-a !part-a
:x ?x
:part-b !part-b})
;; =>
({:part-a [], :x 3, :part-b [4 5]}
{:part-a [3], :x 4, :part-b [5]}
{:part-a [3 4], :x 5, :part-b []})
So the bug seems to be with _ …
yeah it probably treats them all the same
Yeah, they’re called “drops” :drp
nodes and there’s some handling for patterns that use a lot of them. I can take a look at it tomorrow.
(m/search [1 2 3 4 5]
[1 . !a ... 2 . !b ... ?x . !c ...]
?x)
=> (3 4 5)
Yeah.
Okay, open a ticket for that.
I can’t look at it tonight unfortunately.
:thumbsup: will do!
yeah no rush at all man
But it probably has something do with the final case of :prt
in match
namespace that handles variable length on both sides.
There’s a function in the syntax namespace called window
and a couple other that are connected to it.
Probably something off in there.
Thanks for flagging these!
I did a fair bit of pushing myself during the Clojurist Together sprint and made some mistakes along the way. 🙂
Just gotta get them ironed out now! 😄
My pleasure! Hahaha well I’m really liking the new stuff 🙂
Cool! I’m glad.
☝️ I think we might want to roll in map-of
/ set-of
soonish.
Chime in if you have any thoughts on that one.
Oh lol, hear me out on one thing!!!
(m/search [1 2 3 4 5]
[1 . !_ ... 2 . !_ ... ?x . !_ ...]
?x)
^^ this produces the right result
hahahaha well it doesn’t really solve anything
it was just interesting
(m/search (fetch-as-hiccup company-directory-page)
;; match
(m/$ [:div {:class "directory-tables"} &
(m/scan
;; match heading/table pairs
[:h3 {} ?department & _]
_
[:table &
(m/scan
[:tbody &
(m/scan [:tr &
(separated
[:td & (m/scan [:a {} ?name & _])]
[:td {} ?title & _]
[:td & (m/scan [:a {:href ?mailto} & _])])])])])])
;;=>
{:department ?department
:name ?name
:title ?title
:email (subs ?mailto 7)})
^^ this relies on the syntax separated
which is currently a little bit broken, instead I used my own definition:
(m/defsyntax separated
"Match patterns in order ignoring things around and between them.
_ ... p1 . _ ... p2 . _ ... ,,, pn . _ ...
where `p1` through `pn` are `patterns`."
[& patterns]
(m/rewrite patterns
;; convert patterns to memory variables
(!pattern ...)
;; substitute patterns interspersed with ignore anything
(`m/seqable .
'!_ '... !pattern '. ...
'!_ '...)))
How would I bind a variable on the LHS to an expression dependent on a sub-pattern?
I got there with let
and a single logic variable, but can't figure out how to do it for a sequence
(m/match 10
(m/and ?num
(m/let [?parity (if (even? ?num) :even :odd)]))
[?num ?parity])
;; => [10 :even]
(m/match [0 1 2 5 8]
[(m/and !num
(m/let [!parity ??]))
...]
[!num !parity])
Intuitively the (let)
form is in the place of a subpattern, so I feel like there should be a way to reference the value being matched in the binding vector
Yes I agree. Another idea:
(m/match [0 1 2 5 8]
[!num ...]
(let [!parity (mapv even? !num)]
(m/subst
[[!num !parity] ...])))
That's confusing because !parity is being used as a regular Clojure binding
not a Meander anaphoric-thing
(m/match [0 1 2 5 8]
[(m/and !num
(m/or
(m/pred even? (m/let [!parity :even]))
(m/pred odd? (m/let [!parity :odd]))))
...]
[!num !parity])
;; => [[0 1 2 5 8] [:even :odd :even :odd :even]]
This is a little closer in how m/pred
operates implicitly on the "matched subvalue", but of course it won't scale to more complicated mappingsperhaps I'll have to look into defsyntax
oh how about this:
(m/match [0 1 2 5 8]
[(m/and !num
(m/app even? !parity)) ...]
[!num !parity])
=> [[0 1 2 5 8] [true false true false true]]
whoa, that was unexpected
hahah yeah
The context of this was issue #714 and brainstorming what an ideal syntax/semantics for "group-by" would look like
maybe I’m looking in the wrong place I don’t see that issue (max is 96)
can you please link to it?
oops, #74
oh right!!!
Ideally, I would like to write something like
(m/rewrite data
(m/grouped-by !num (m/grouped-by !lang !word))
(m/grouped-by !lang (m/grouped-by !num !word)))
for that examplewill probably sleep on it and post something on that issue, thanks for the help!
FWIW I think it would be nice if we could write
(m/rewrite data
{!num {!lang !word}}
{!lang {!num !word}})
^^ I think there is enough structure in this to infer the relationships between the variables (of course it produces an answer that is not what I want right now)I think the mechanism has to be that nested sequences collect each symbol’s terms… ie: the above expression should not produce !num as an array, but rather ![num lang word] ie an array of tripples for substitution, and when substituting, the tripples then all align, but of course the map substitution would have to imply merge semantics (which I think is reasonable).
I guess 3 arrays of the same lenth is the same thing as an array of tripples so that detail doesn’t really matter
Consider:
(m/rewrite {1 {:en "one", :de "eins", :fr "un"}
2 {:en "two", :de "zwei", :fr "deux" :es "dos"}
3 {:en "three", :de "drei", :fr "trois"}
5 {:fr "cinq"}}
(map-of !num (map-of !lang !word))
[~(count !num) ~(count !lang) ~(count !word)])
=> [4 11 11]
The problem here is that the arrays are different lengths, and we have no information for how to assemble them taking the right number from eachIf the arrays were the same length, at least we would know the association between the values.
The current way that nested memory variables work obscures the associations.
I think a separate operator would be better than further overloading the existing map syntax
the pattern {!k !v}
already has an existing interpretation and I imagine it would cause ambiguity
The answer @timothypratley gave might seem weird, but I think if you think about it, it make sense.
(m/match [0 1 2 5 8]
[(m/and !num
(m/app even? !parity))
...]
[!num !parity])
Compare this to:
(m/match [0 1 2 5 8]
[(m/and !num !parity)
...]
[!num !parity])
And says that the element should match both patterns. In this case our pattern is a memory variable which matches anything and stores it. Then we just combine that with an app and we can store the transformed value.Does this help?
(m/search [0 1 2 5 8]
(m/scan (m/and ?num (m/let [?parity (if (even? ?num) :even :odd)])))
[?num ?parity])
=> ([0 :even] [1 :odd] [2 :even] [5 :odd] [8 :even])
Or do you want the output to be [[0 1 2 5 8] [:even :odd :even :odd :even]]
?hmm yes, search
+ scan
works in this case, but I'd be interested to know if it could be done with !memory vars
eg. if there is more than a single variable-length sequence in the overall pattern, using search and scan would produce all permutations of those sequences
(m/match {:foo [1 2 3 4]
:bar [5 6 7 8]}
{:foo [!n ...]
:bar [!m ...]}
[!n !m])
;; => [[1 2 3 4] [5 6 7 8]]
(m/search {:foo [1 2 3 4]
:bar [5 6 7 8]}
{:foo (m/scan ?n)
:bar (m/scan ?m)}
[?n ?m])
;; => ([1 5] [1 6] [1 7] [1 8] [2 5] [2 6] [2 7] [2 8] [3 5] [3 6] [3 7] [3 8] [4 5] [4 6] [4 7] [4 8])
Would this help?
(m/match [0 1 2 5 8]
(m/and [!num ...]
(m/let [?parity (map even? !num)]))
[!num ?parity])
^^ obviously this is kind of cheating hahahabut it makes me wonder if the problem here is on the pattern or on substitution
The more I think about it, memory variables seem unsound in general
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k !n] ...]
[[!k !n (m/app str !n)] ...])
;; => [[:a 1 "2"] [:b 3 ""]]
This makes total sense operationally if you know that !n is just collecting and popping values
but it seems at odds with the declarative nature of the library
Regarding “duplication”… I would have expected the solution in this case be be:
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k !n] ...]
[(m/let [?n !n]
[!k ?n (m/app str ?n)]) ...])
i.e. if I want to access !n
once and use it twice, let
seems like the tool. But … like most of my made up solutions… it doesn’t work.
Is there a way to do this?
We don't have a substitute version of let yet. You can do this, or duplicate the memory variable with (m/and !n !n2)
(m/match [[:a 1] [:b 2] [:c 3]]
[[!k !n] ...]
(let [?n !n]
(m/subst
[[!k ?n (m/app str ?n)] ...])))
Oh cool, I think I like (m/and !n !n2)
more than the external let, and that should work for rewrite
etc
Based on this line of thinking, I’m now confused by this:
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k (m/and !n !n2)] ...]
[[!k !n (m/app str !n2)] ...])
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k (m/and !n (m/app str !n2)) ...]]
[[!k !n !n2] ...])
The former works great, the latter does not
why?????!?!?!?!! 😕
FWIW I think the former is the better way to express it, as the transformation belongs in the target pattern, so this is a bit of a mooo point, just wanting to understand.
@timothypratley because the second version has the …
in the wrong place.
(= [[:a 1 "1"] [:b 2 "2"] [:c 3 "3"]]
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k (m/and !n !n2)] ...]
[[!k !n (m/app str !n2)] ...])
(m/rewrite [[:a 1] [:b 2] [:c 3]]
[[!k (m/and !n (m/app str !n2))] ...]
[[!k !n !n2] ...]))
oh right!!!
yeah thanks @noprompt
they both work 🙂
There are a lot of cases where you want that behavior. For example making tuples of things. Or inverting a matrix. I definitely understand how it can be counter intuitive. But I'm not sure what the semantics would be otherwise.
Yeah, I think it goes back to the "nesting" problem I was facing with the grouping construct - it's hard to differentiate where you want a variable to refer to a consistent value within a context, versus a sequence of things
there's also the matter of mixing the sequential nature of !memory vars with unordered hash-maps
(m/rewrite [1 2 3 4]
[!n ...]
{1 !n
2 !n
3 !n
4 !n})
;; => {1 1, 2 2, 3 3, 4 4}
but
(m/rewrite [1 2 3 4 5 6 7 8 9]
[!n ...]
{1 !n
2 !n
3 !n
4 !n
5 !n
6 !n
7 !n
8 !n
9 !n})
;; => {7 2, 1 6, 4 3, 6 9, 3 5, 2 1, 9 7, 5 8, 8 4}
(I realise that's not a very realistic example, just trying to pick at the edges of the system to see if anything falls apart theoretically)
Yeah there are a lot of interactions between these things. There are ways to accomplish that without having to worry about order. On my phone but can show later if needed.
I would argue that memory variables are not actually at odds with declarative though. Really, its Meanders pattern evaluation strategy which is left to right, top to bottom.
An alternative universe of what a memory variable means: [thread incomming]
OMG I’m such an idiot… the “lost information” is completely solved by the ..!n
That is the associative information
I can easily reconstruct it from that
That was the motivation for it. 🙃 I just forgot to mention it. 😂
Ok so good news and bad news, the good news is I have coded a way to construct the “inferred associativity of nested repeats” or whatever you want to call them, the bad news is now I’m going to start wanting to use these in substitutions. Here is my code:
(defn kvseq
"Recursively convert a map to key/value pairs."
[m]
(for [[k v] m]
[k (cond-> v (map? v) (kvseq))]))
(kvseq
{1 {:en "one", :de "eins", :fr "un"}
2 {:en "two", :de "zwei", :fr "deux" :es "dos"}
3 {:en "three", :de "drei", :fr "trois"}
5 {:fr "cinq"}})
#_#_=>
([1 ([:en "one"] [:de "eins"] [:fr "un"])]
[2 ([:en "two"] [:de "zwei"] [:fr "deux"] [:es "dos"])]
[3 ([:en "three"] [:de "drei"] [:fr "trois"])]
[5 ([:fr "cinq"])])
(defn associations*
"Not intended to be called directly, prefer using associations.
Takes a sequence of outer rows"
[result outer-rows inner-rows partitions]
(if (seq outer-rows)
(let [[outer-row & os] outer-rows
[n & ns] partitions
[inner-chunk is] (split-at n inner-rows)
rows (for [inner-row inner-chunk]
(into outer-row inner-row))]
(recur (concat result rows) os is ns))
result))
(defn transpose
"Given a sequence of columns, returns a sequence of rows, or vice versa."
[seq-of-seqs]
(apply map vector seq-of-seqs))
(defn seqable-of-seqables?
[x]
(and (seqable? x)
(every? seqable? x)))
(defn associations
"Takes sequences of outer values to be associated with sequences of inner values as rows by partitions.
Rows are vectors of associated values.
Given outer sequences ([1 2]),
inner sequences ([:one :un :two :deux]),
and partitions [2 2]
Produces ([:one 1] [:un 1] [:two 2] [:deux 2])
Suitable for converting memory variable notation into row notation.
[!xs [!ys ..!ns] ...]
(associations [!xs] [!ys] !ns)
Can take layers of inners and partitions.
[!xs [!ys [!zs ..!ms] ..!ns] ...]
(associations [!xs] [!ys] !ns [!zs] !ms)
=> ([x y z] [x y z] [x y z] ...)
By aligning inner and outer values, we define their relationship.
This is often useful for deriving other logically consistent nested forms,
For example {!xs... {!ys... !vs...}} -> rows -> {!ys... {!xs... !vs...}}
Note that in this case, !ys and !vs are at the same level:
(associations [!xs] [!ys !vs] !ns)
=> ([x y v] [x y v] [x y v] ...)"
[outers & inner-partitions]
{:pre [(even? (count inner-partitions))
(seqable-of-seqables? outers)]}
(loop [rows (transpose outers)
ips inner-partitions]
(if ips
(let [[is ps & m] ips]
(assert (seqable-of-seqables? is))
(assert (= (count ps) (count rows)))
(assert (seqable? ps))
(assert (every? int? ps))
(let [inner-rows (transpose is)]
(assert (= (reduce + ps) (count inner-rows)))
(recur (associations* () rows inner-rows ps)
m)))
rows)))
(associations [[1 2]] [[:one :un :two :deux]] [2 2])
#_#_=> ([1 :one] [1 :un] [2 :two] [2 :deux])
(associations [[1 2]]
[[:one :un :two :deux]] [2 2]
[["ae1" "be1" "af1" "bf1" "ae2" "be2" "af2" "bf2"]] [2 2 2 2])
#_#_=>
([1 :one "ae1"]
[1 :one "be1"]
[1 :un "af1"]
[1 :un "bf1"]
[2 :two "ae2"]
[2 :two "be2"]
[2 :deux "af2"]
[2 :deux "bf2"])
(def data {:en {1 "one", 2 "two", 3 "three"},
:de {1 "eins", 2 "zwei", 3 "drei"},
:fr {1 "un", 2 "deux", 3 "trois", 5 "cinq"},
:es {4 "cuatro"}})
(m/match (kvseq data)
([!lang ([!num !word] ..!n)] ...)
(associations [!lang] [!num !word] !n))
#_#_=>
([:en 1 "one"]
[:en 2 "two"]
[:en 3 "three"]
[:de 1 "eins"]
[:de 2 "zwei"]
[:de 3 "drei"]
[:fr 1 "un"]
[:fr 2 "deux"]
[:fr 3 "trois"]
[:fr 5 "cinq"]
[:es 4 "cuatro"])
(defn nest-by [order rows]
(reduce
(fn [acc ordered-row]
(assoc-in acc (butlast ordered-row) (last ordered-row)))
{}
(for [row rows]
(mapv row order))))
(nest-by [1 0 2]
[[:en 1 "one"]
[:en 2 "two"]
[:en 3 "three"]
[:de 1 "eins"]
[:de 2 "zwei"]
[:de 3 "drei"]
[:fr 1 "un"]
[:fr 2 "deux"]
[:fr 3 "trois"]
[:fr 5 "cinq"]
[:es 4 "cuatro"]])
#_#_=>
{1 {:en "one", :de "eins", :fr "un"},
2 {:en "two", :de "zwei", :fr "deux"},
3 {:en "three", :de "drei", :fr "trois"},
5 {:fr "cinq"},
4 {:es "cuatro"}}
(def data {:en {1 "one", 2 "two", 3 "three"},
:de {1 "eins", 2 "zwei", 3 "drei"},
:fr {1 "un", 2 "deux", 3 "trois", 5 "cinq"},
:es {4 "cuatro"}})
(m/match (kvseq data)
([!lang ([!num !word] ..!n)] ...)
(->> (associations [!lang] [!num !word] !n)
(nest-by [1 0 2])))
=>
{1 {:en "one", :de "eins", :fr "un"},
2 {:en "two", :de "zwei", :fr "deux"},
3 {:en "three", :de "drei", :fr "trois"},
5 {:fr "cinq"},
4 {:es "cuatro"}}
:party-corgi:
Main things this proves to me: 1. Memory variables can capture the implied associativity of nested repeats (or whatever it’s called). 2. I think the concept of rows helps as an intermediary substrate when transforming between different orders of nesting 3. Creating a new nesting for maps is just assoc-in choosing the path and value from the rows in a different order 4. This is a general approach that works for arbitrary nestings of arbitrary participation (not just maps)
But the main drawback is of course that I had to write a lot of non-meander helper code. I think it is worth a quick review of why I needed them:
a) kvseq
was needed because there is no ..!n
equivalent for maps
b) associations
is needed to convert memory variables and partitions to rows. I think this might be useful to meander users and hope you consider including something like it in meander. Is it actually as useful as I think it is? Maybe it doesn’t come up much, or maybe if we had substitution equivalents it would be unnecessary.
c) nest-by
is needed because meander doesn’t have a way of expressing reduction/aggregation.
It’s also very interesting to me that when you put the pattern and associations function side by side you can see clearly that one implies the other:
([!lang ([!num !word] ..!n)] ...)
(associations [!lang] [!num !word] !n)
Haven't had a chance to deep dive on this. But you can match maps with seqable. And seqable supports ..!n.
Comparing with the search
approach:
(m/search {1 {:en "one", :de "eins", :fr "un"}
2 {:en "two", :de "zwei", :fr "deux" :es "dos"}
3 {:en "three", :de "drei", :fr "trois"}
5 {:fr "cinq"}}
(m/scan [?num (m/scan [?lang ?word])])
[?lang ?num ?word])
^^ This finds all the rows without any helpers, has obvious ordering (which means nesting can be even simpler)… maybe the real answer here is you are better off not bothering with memory variables for associative transformations. There is a specific case where you have 2 repeats inside 1 repeat where you can’t use search/scan, but maybe that should be handled by first splitting then search/scanning each separately.hahaha check this out:
(m/match {1 {:en "one", :de "eins", :fr "un"}
2 {:en "two", :de "zwei", :fr "deux" :es "dos"}
3 {:en "three", :de "drei", :fr "trois"}
5 {:fr "cinq"}}
{& (m/seqable [*num {& (m/seqable (m/app #(conj % *num) !lang-word-num) ...)}] ...)}
!lang-word-num)
=>
[[:en "one" 1]
[:de "eins" 1]
[:fr "un" 1]
[:en "two" 2]
[:de "zwei" 2]
[:fr "deux" 2]
[:es "dos" 2]
[:en "three" 3]
[:de "drei" 3]
[:fr "trois" 3]
[:fr "cinq" 5]]
wow, mutable variables?
yeah! if the outer variables are mutable, you can collect them with the inner rows
hmm, I've always wondered what those mutable vars could be used for
wait, they don't have to be mutable - I replaced *num with ?num and it worked fine
oh… interesting 🙂
I guess that makes sense 🙂
I thought logic vars couldn't be used inside variable-length sequences
but it seems like this is allowed because ?num
doesn't "leak" outside the pattern?
well, in this case it’s good that you can hahaha
But the final expression is pretty … ugly!!!
it could be a little cleaner if we made a thing called m/conj
as shorthand for m/app conj
…
{& (m/seqable [?num {& (m/seqable (m/conj !row ?num) ...)}] ...)}
yeah, it struck me as a little too "brain-teasery"
maybe even:
(m/map-of [?num (m/map-of (m/conj !row ?num))])
(m/into)
would be more useful than (m/conj)
and could work too
Personally I like "map-of" to behave more closely to clojure.spec
how is that?
where you have a key-pattern and val-pattern for a homogeneous map
like the one which was previously proposed
right yes, in that case map-of
won’t work here because the k/v need to be collected together
yup, just a comment about the operator name
> it could be a little cleaner if we made a thing called m/conj
as shorthand for m/app conj
…
A more general shorthand might be nice too
(m/apply f !a ?b :c)
=>
(m/app #(f %1 %2 :c) !a ?b)
Oh yeah, I like that.
{& (m/seqable [?num {& (m/seqable [?lang (m/and ?word (m/let [!row [?lang ?num ?word]]))] ...)}] ...)}
yet another way!!!
This one might work with map-of
because it doesn’t rely on key-value
pairing
ah nope: When matching, map patterns may not contain variables in their keys that would make it so there is more than one match possible.
(map-kvs ?num (map-kvs ?lang (m/and ?word (m/let [!row [?lang ?num ?word]]))))
^^ this works! 🙂where
(m/defsyntax map-kvs [ks vs]
(cond (m/match-syntax? &env)
`{& (m/seqable [~ks ~vs] ...)}
(m/subst-syntax? &env)
`{& ([~ks ~vs] ...)}
:else
&form))
similar to map-of
but different in some way I can’t describe.
Oh interestingly the ?num trick does not work inside rewrite, but *num does… I have no idea why
O.K. one last riddiculous one:
(m/match {1 {:en "one", :de "eins", :fr "un"}
2 {:en "two", :de "zwei", :fr "deux" :es "dos"}
3 {:en "three", :de "drei", :fr "trois"}
5 {:fr "cinq"}}
(map-kvs *num (map-kvs *lang (m/and *word (m/app (fn [word] (assoc-in (last !acc) [*lang *num] word)) !acc))))
(last !acc))
=>
{:en {1 "one", 2 "two", 3 "three"},
:de {1 "eins", 2 "zwei", 3 "drei"},
:fr {1 "un", 2 "deux", 3 "trois", 5 "cinq"},
:es {2 "dos"}}
I’m abusing variables to perform a reduction while parsing… conceptually though this might not be crazy if there were a way to define an accumlator variable and something a little more slick than app for updating them.
I’m thinking of something like
(map-kvs *num (map-kvs *lang *word (m/collect **acc** assoc-in [*lang *num] *word)))
o_ODeclarative programming simply boils down to idea that the programmer need not worry about control flow.
Consider for a moment a magical f* function:
(defn f* [m]
(m/search m
(m/scan [?k1 (m/scan [?k2 ?v])])
[?k1 ?k2 ?v]))
Let’s use it:
(f* {:a {1 "a1"
2 "a2"}
:b {3 "b3"}})
=>
([:a 1 "a1"] [:a 2 "a2"] [:b 3 "b3"])
Wow!!! that’s useful 🙂
O.K. now get ready for even more awesome:
(defn f' [acc args]
(assoc-in acc (butlast args) (last args)))
What is this f prime you ask??? Let’s see what it does:
(let [!associations '([:a 1 "a1"] [:a 2 "a2"] [:b 3 "b3"])]
(reduce f' {} !associations))
=>
{:a {1 "a1", 2 "a2"}, :b {3 "b3"}}
❗What is going on here??
f*
collected “variables” in an associative way which preserved information that current “memory variables” throws away
Specifically, when you nest memory variables, the collect values into arrays of different sizes, this explicitly removes the association information.
If in an alternate reality memory variables just did not work like this, we would have scoping.
What about f'
?? What’s the dealio there?
If we have “variables” in associative arrays, it becomes obvious how to substitute them associatively into a map: It’s just assoc-in
!
Now there isn’t any meander syntax sugar for assoc-in
that I’m aware of right now, but in a world where we have “associative nested memory variables” this operator would be uber powerful.
I don’t find it hard to imagine
(m/rewrite {!k1 {!k2 !v}}
{!k2 {!k1 !v}})
meaning exactly what it looks like:
Collect k1 k2 v
associatively,
substitute as assoc-in k2 k1 v
That
is
some seriously nifty syntax!
I’ll take a different tact and say I don’t see this as non-declarative. Consider this example.
(m/rewrite [1 2 3 4 5 6]
[!xs ...]
[[!xs !xs] ...]
This can say “given a vector of zero or more elements, the result should be a vector with those elements in pairs”. I’m sure that @noprompt would probably say something about declarative being not 100% clear. Which is fine. But to me there is nothing undeclarative about memory variables.
I will say that they can be surprising and there are certain things that are easy to accomplish with them and other things that are not so easy to accomplish with them.But it doesn’t have to us that syntax.
The core concept is the issue:
The discomfort shown in the map examples are due to Clojure. There’s nothing a lot I can do about that. &
or m/and
are the best offerings we have currently to get around the map ordering issue.
(m/and {1 !n} ,,, {9 !n})
{1 !n & {2 !n & {,,,}}}
I should point out this is completely not declarative… and I think thats okay because control flow in this way does have its merits.How do you express collecting associative variables?
We could introduce a new concept “associative memory variables” maybe even a new symbol
If we say that [!n !n]
meant repeat the exact same value twice, how do we get to the next value? Do we need to always say [!n ..2]
or something like that? How does that work in the nested case? For example:
(m/rewrite [1 2 3 4 5 6]
[!xs ...]
{& [[!xs [!xs !xs]] ...]})
FWIW I’d rather memory variables just work the way I want them to hahahahahaa but of course that’s my opinion
For me, when I started thinking about those issues and how to solve them, the only answer I could come up with were explicitly imperative answers (like !xs.next).
That said, we do need good ways to to accomplish duplication just like we need good ways of doing grouping. We also (I believe) need good ways to generalize memory variables.
Call me a pedant but I think that variables are always declarative.
Memory variables were primarily driven to conception by regular expression things. I wouldn’t mind having an associative binding variable type, however.
They thing I want to discuss in this thread is that the nested memory variables drop information, useful information! If we had that information, we could do some cool stuff 🙂
I think we can generalize memory variables and do exactly what you are talking about. For a first approximation, memory variables are monoids. Why couldn’t we at least generalize that far?
But memory variables were not designed for nested things. They were designed for sequential things. We’re simply lacking a type for nested/hierarchical things.
I don’t think nesting is monoidal though.
You can also do {& [[1 !n] [2 !n] ,,, [9 !n]]}
I would imagine a monoidal map could serve many of those purposes, but I admit I’m just spit balling.
Any examples of symbolic and imperative programming combined? I think imperative is useful, but seems at odds with symbolic and transparent ways of doing things.
rewrite
is that example.
The structure of clauses are basically if then else.
I guess we have different definitions of imperative. I don’t think if statements are an imperative construct.
I don’t like the word imperative. Its not really helpful here. What I’m trying to say is there is utility in “ordering mattering” and “order not mattering”.
What would this look like? I’m having trouble coming up with an sexpr kind of thing.
IMO it is way more elementary than any of that, its just fundamentally instead of producing different sized arrays, produce equal sized arrays. Maybe I’m guilty of jumping to an implementation here, but it just seems perfect 😛
(m/rewrite [1 2]
(&scope-1 [?x ?y])
(&scope-1 ?x))
;; => 1
(m/rewrite [1 2]
(&scope-1 [?x ?y])
(&scope-1 ?y))
;; => 2
(m/rewrite [[1 2] [3 4]]
[[(&scope-1 [!x !y]) (&scope-2 [!x !y])] ...]
[(&scope-1 !x) (&scope-2 !x) . !x ...])
;; =>
[1 3 1 3]
Maybe I’m missing the thread but it seems like the desire is to have a notion of storing things in contexts.
It seems entirely possible and useful to me to have a storage mechanism which is keyed by variables and their values and, in particular, for memory variables to have a distinct drain that belongs to that storage.
In the third example above there is the global !xs
and !ys
and then there is the “scoped” !xs
and !ys
that belong to &scope-1
and &scope-2
.
(m/rewrite {!k1 {!k2 !v}}
{!k2 {!k1 !v}})
@timothypratley Are you imagining an implicit repeat with this?
(m/rewrite {:a {:b :c}}
{!k1 {!k2 !v}}
{!k2 {!k1 !v}})
That does exactly what I’d expect, but I’m assuming you are thinking of it generalizing to where there is more than one key value pair in the outer (and inner?) structure?I think you are presenting a (probably better) solution. I’m going to give a more direct example of what I’m pushing for specifically:
(m/match [[:a [[1 "a1"]
[2 "a2"]]]
[:b [[3 "b3"]]]]
[[!k1 [[!k2 !v] ...]] ...]
{:k1s !k1
:k2s !k2
:vs !v})
=>
{:k1s [:a :b], :k2s [1 2 3], :vs ["a1" "a2" "b3"]}
^^ This is how memory variables work, note that there are 2 memory variables gathered in !k1
But in Tim’s alternate universe:
the result would be :
{:k1s [:a :a :b], :k2s [1 2 3], :vs ["a1" "a2" "b3"]}
Note that !k1
had 3 values in it!!!
Just doing this one neat trick has preserved the associative information we crave, it defines the scopes implicitly.I’m happy to add map-of
and set-of
to epsilon
so that folks can get this semantic easily.
I’ll call them “tim variables” just to distinguish the behavior, but fundamentally its just about how the arrays get populated.
LOL
map-of
is a fine thing, but it doesn’t solve the information loss
Its not clear to me how !ks1
ends up with two :a
s.
Aha! That’s easy
Oh I see.
Yeah, I think the &scope
thing I mentioned gets at your problem.
when we add something “nested”, we just also add whatever te current parent is.
so one algorithm is: • don’t insert tim variables if there are subvariables. • when inserting a bottom level variable, insert all the parents
This might not be a great way, I’m just describing something that would mechanically make sense to me.
So what would the result of this be?
(m/match [[:a [[1 "a1"]
[2 "a2"]]]
[:b [[3 "b3"]]]]
[[!k1 [[_ !v] ...]] ...]
{:k1s !k1
:vs !v})
{!k1 ?x}
does not imply that ?x
is a subvariable of !k1
. Consider
#{[!k1 ?x]}
The idea of a subanything implies a super something.
[!k1 ?x :as ?super]
This would be a case where there is a sub/super relationship.@jimmy {:k1s [:a :a :b], :vs ["a1" "a2" "b3"]}
@noprompt I don’t follow your notation.
In my little world I’m only talking about sequences. Nested sequences are strictly identifiable. Map can always be represented as sequences. Maybe I confused things by eariler proposing a map syntax which isn’t really important to the question of how to keep context information. Also can you explain more what your $scope proposal is as I don’t understand the example.
Also just to brainstorm here, maybe we could use metadata
on values in memory variables as a low friction alternative
@timothypratley That’s what I figured. But I am a little confused by why. Is !k2
and !v
are subvariables of !k1
. But I am confused on the relationship between !k2
and !v
. They seem to be siblings, so they would both be bottom in my mind and cause double inserts. Is that not their relationship? Is !v
a bottom variable but not !k2
?
well “on values” is probably the wrong word there
Correct, they are both bottom siblings, and yes that implies that regardless of how many siblings exist, insertion into the parent stack should happen only once.
to explore a as metadata
style instead of equal array sizes
approach a bit more;
When inserting a memory variable, check to see if there is a parent, and just record that somewhere.
The question for that approach is how do you identify parents? By value? by index? and where do you store it? in a shadow array? as an array attached as metadata to the variable maybe? Seems entirely feasible.
The hypothetical (&scope <pattern>)
establishes a storage location &scope
which maps variables in <pattern>
to their values in <pattern>
. Memory variables matched in a scope would be associated only with what the collected in that scope.
(m/match [1 2 3]
[!xs (&scope !xs) !xs]
{:global-!xs !xs
:&scope-!xs (get &scope '!xs)})
;; =>
{:global-!xs [1 2 3]
:&scope-!xs [2]}
Of course we could’ve just written
[!xs (m/and !xs !ys) !xs]
and gotten the same affect.I’m really confused, there is no sequence …
and no nesting 😕 😱
Okay, lets back up here.
Lets go back to what I flagged earlier
[!xs !ys :as ?vector]
If we were going to imagine a super/sub relation ship here, we could say that !xs
and !ys
are subvariables of ?vector
in this example.
But thats kind of weird so lets just say that !xs
and !ys
are subvariables of that vector.
The !xs
and !ys
are siblings and thus one is not a subvariable of the other.
The same is true if we talk about
{!ks !vs}
!ks
is a sibling of !vs
Logically, they are members of a pair.
I’m taking about Subsequencevariables:
[!x [!y …] …]
Clearly for every x there can be 0-n y
That seems like a different concept to what you are describing
Right. And thats problematic if you want to keep things grouped.
So when we want to keep things grouped we need mechanism for establishing a group; an context sensitive association thing.
I claim that I’ve described two mechanical ways to preserve the context sensitive information purely on the pattern 😛
i.e. no syntax required to describe it.
I think what @noprompt means is that if we want to be able to choose between “to group or not to group” we need a way to distinguish these two situations.
Or if we want to group by something other than my parent
I don’t think we do need that; there are alternatives! 🙂 For example the metadata approach is just collecting more data… To chose to ignore it or use it happens later.
So you don’t strictly need to choose that in the pattern being matched.
You asked the right question earlier @timothypratley “how do you define what a parent is”?
Well taking the most basic case:
[!x [!y …] …]
Just by looking at it you can see that at the top level [_ _ …]
has no sequence context around it, therefore !x
has no “parent”. But when we look at !y
it clearly occurs inside a surrounding sequence, so it has a “parent” !x
.
Programatically I imagine this logic as “I have a stack! Everytime I enter a sequence, I put that on the stack.
You don’t even have to think of them as parents at all really, just levels.
You’re saying !x
is !y
s uncle.
Or aunt.
More simply I’m saying that sequences can be nested.
But how would you square that notion with something like
[!x [!y] !x [!z]]
?!x
exists in a sequence, !y
exists in a subsequence of that sequence.
there are no sequences in [!x [!y] !x [!z]]
so nothing to square with that my small mind can see
By sequences, you mean repeats? (ie: …, ..2, ..!n, ..?n)
What about
[!x [!y] !y [!x]]
?I mean repeats yeah, I guess sequence is a really bad word hahahaha. Loops
This is why I was saying earlier that the mechanism for expressing an binding scope is useful.
Right now there is only one scope: the global scope.
Every time there is a …
it implies a very useful scope 😛
(m/match [[:a [[1 "a1"]
[2 "a2"]]]
[:b [[3 "b3"]]]]
[[!k1 [[!k1.k2 !k1.v] ...]] ...]
{:k1s !k1})
;; =>
{:k1s [:a [{:k2 1 :v "a1"} {:k2 2 :v "a2"}]
:b [{:k2 3 :v "b3"}]]}
Similarish idea, but not implicit. Not saying we should adopt that syntax, but gets the point across.Yeah! that preserves the associative information in a very precise way 🙂
gosh I need to make a new language up just to talk about this stuff
There is a book on constraint language based on term rewriting that has a very similar construct. I have a book on it at home.
“nested repeat stack associativity information” how’s that sound lol
Ummm so where in the code would I look for where insertion into memory variables occurs?
Starting point https://github.com/noprompt/meander/blob/epsilon/src/meander/match/ir/epsilon.cljc#L1458
That change will be fairly difficult.
hahaha damnit!!!! well hopefully I’ll learn something.
Upon re-reading this thread I realize I never presented the KILLER FEATURE:
(defn f' [acc args]
(assoc-in acc (reverse (butlast args)) (last args)))
(let [!associations '([:a 1 "a1"] [:a 2 "a2"] [:b 3 "b3"])]
(reduce f' {} !associations))
=>
{1 {:a "a1"}, 2 {:a "a2"}, 3 {:b "b3"}}
Hopefully you’ll recognize this as a solution to @qythium’s language words transformation.You could possible make that work as a rewrite over the ir. That wouldn't require changing the internals.
LOL with all the discussion today I didn’t get a chance to get into fixing the bug reported in #96.
New thread in which ridiculous escapades are journaled:
(defmethod compile* :mvr-append
[ir fail kind]
(let [!symbol (:symbol ir)]
`(let [~!symbol (conj ~!symbol ~(compile* (:target ir) fail kind))
~'_ (prn "APPEND" ~!symbol)]
~(compile* (:then ir) fail kind))))
(defmethod compile* :mvr-bind
[ir fail kind]
`(let [~(:symbol ir) ~(compile* (:target ir) fail kind)
~'_ (prn "BIND" ~(:symbol ir))]
~(compile* (:then ir) fail kind)))
(do (prn "----------")
(meander.epsilon/match
[:x1 [:y1 [:z1]
:y2 [:z2 :z3]]
:x2 [:y2 [:z4 :z5 :z6]]]
[!x [!y [!z ...] ...] ...]
[!x !y !z]))
=>
"----------"
"APPEND" [:x1]
"APPEND" [:y1]
"BIND" [:z1]
"APPEND" [:y1 :y2]
"BIND" [:z1 :z2 :z3]
"APPEND" [:x1 :x2]
"APPEND" [:y1 :y2 :y2]
"BIND" [:z1 :z2 :z3 :z4 :z5 :z6]
=> [[:x1 :x2] [:y1 :y2 :y2] [:z1 :z2 :z3 :z4 :z5 :z6]]
what does append and bind mean? 😱 what things can I look up in ir
?
Buried in some junk code I have a helper for analyzing stuff. https://github.com/jimmyhmiller/PlayGround/blob/master/wander/src/wander/core10.clj#L272
:mvr-bind
is for binding a memory variable, usually to a []
but sometimes to (vec seq)
.
Not 100% accurate but useful.
:mvr-append
is for conj
ing on it.
Sorry its not documented. 🙂
@jimmy ooo nice thank you I’ll play around with that @noprompt I’m probably conflating something but it seems surprising (in a wierdly good way) that my output indicates that !x and !y are appended to, but !z is bound to… I’m obviously misinterpreting the output though.
Because z is just a literal vector. We can just bind it. It's basically an optimization.
ohic
Just caught up with all the threads, I'm happy to have sparked so much discussion 😅
I really like the !k1.k2
notation and the idea of explicitly declared scopes