Hey I have a question on the design of Meander for which I would like to know your take, @noprompt. It's not a suggestion on my part, it's more understanding why things work in a certain way. Let's take as a starting point an example I had last time:
You can see that, in the part that does the construction of the new pattern, I have to call meander/app
to signify my intent of applying the function, because (function args)
would have been read literally, as data. My question would be, why is meander/app
not the default meaning for applications in the RHS? You could still quote things explicitly when you want a list, and it seems to me that's the most common case. But I know from your interview on youtube that you thought about semantics quite a bit, hence my curiosity 🙂
@meditans m/app
is not the default on the RHS because the goal for substitution patterns is to be the dual of matching patterns and share the same syntax with them (though for epsilon
this is currently not the case but for zeta
it is). So if, for substitution, (p_fn & p_args)
is to mean apply substitution to p_fn
and p_args
to get f
and args
, then clojure.core/apply
f
to args
, there would need to be a suitable dual semantic for matching. I think there is one that could make “sense” and I’m going to ramble on about how m/app
works in zeta
, point out that m/app
in epsilon
is a little janky, some other thoughts on the topic, and hope for the best.
In zeta
, (m/app p_fn p_args p_object)
matching means yield objects of the pattern p_fn
and objects of the pattern p_args
to get fn
and args
respectively, then apply fn
to the object being matched and args
the return of which is matched against p_object
.
;; epsilon
(m/match 1 (m/app vector [?x]) ?x)
;; ❶ ❷
;; => 1
;; ❶ Function is a host expression without ~
;; ❷ Additional arguments must be supplied in host expression
;; zeta
(m/match 1 (m/app ~vector [] [?x]))
;; ❶ ❷
;; => 1
;; ❶ Function is a pattern
;; ❷ Additional arguments can be supplied with patterns
(m/app p_fn p_args p_object)
for substitution means yield objects of the pattern p_fn
and objects of the pattern p_args
to get fn
and args
respectively, then apply fn
to args
to get the object being yielded which must match the pattern p_object
.
;; epsilon
(let [?x 1]
(m/subst (m/app vector ?x ?x)))
;; ❸ ❶ ❷ ❷
;; => [1 1]
;; ❶ Function is a host expression without ~
;; ❷ Remaining pattern arguments assumed to the arguments to the
;; function which is completely different from match
;; where the remaining arguments are semantically equivalent to
;; `m/and`
;; ❸ Not possible to constrain return with a pattern
;; zeta
(m/subst (m/app ~vector [?x ?x] _) {'?x 1})
;; ❶ ❷ ❸
;; => [1 1]
;; ❶ Function is a pattern
;; ❷ Explicit arguments
;; ❸ Possible to constrain return with a pattern
As you can see from these examples, the syntax and semantics m/app
in epsilon
is a trashfire as it assumes the function is a host expression, arguments are passed to the function in different ways, and substitution lacks the ability to constrain the return. zeta
on the other hand has consistent syntax and the only semantic difference is that of matching/substitution.
Going back to your original question, I think it would be possible to have an m/app
everywhere style where patterns are of the form (p_fn p_args p_return)
retaining some of the primitive operators and desugaring to m/app
.
(m/rewrite (list 1 2 3)
(m/and (~sequential? [] true)
(~vec [] [?x ?y ?z]))
(~+ [?x ?y ?z] _))
;; => 6
Alternatively, if you have as primitives patterns which represent the duals of construction and deconstruction for each of the primitive data types which exist, you would have something that could serve this purpose for many things.
(m/rewrite (list 1 2 3)
(m/cons ?x (m/cata ?y))
(m/conj ?y ?x)
_ [])
;; => [3 2 1]
(m/rewrite [1 2 3]
(m/conj (m/cata ?x) ?y)
(m/cons ?y ?x)
_ ())
;; => (3 2 1)
thanks for the very detailed explanation @noprompt, can't wait to see zeta!
Remind me never to write a long reply in my editor then paste it into Slack thinking it’ll work out. 😂