Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syntax proposal: m/rec shorthand for m/with single case #116

Closed
timothypratley opened this issue Mar 5, 2020 · 13 comments
Closed

Syntax proposal: m/rec shorthand for m/with single case #116

timothypratley opened this issue Mar 5, 2020 · 13 comments

Comments

@timothypratley
Copy link
Collaborator

timothypratley commented Mar 5, 2020

{:as %r
 :m !m
 :r %r}

A more concise way to express

(m/with [%r {:m !m, :r %r}] %r)

@noprompt
Copy link
Owner

noprompt commented Mar 6, 2020

I think this is a cool idea but I want to give it a little thought. I think we could experiment with this on zeta and use the @ sigil for this instead of % (since the latter already has a definition and former does not). It could also work on epsilon too as meander.epsilon/@name i.e. m/@r (using clojure.core/deref is not an option as it is a breaking change to epsilon).

{:m !m, :r m/@r :as m/@r}

If I'm being totally honest, I don't really like the namespacing the like this. It's just noisy as hell. Plus, the reader doesn't parse something like foo/@x as (foo/deref x) which means there's no way to cut down the noise by referring deref from meander.<greek>. I would rather say that @r/(clojure.core/deref r) is treated as this self-recurring thing and say "if you want to match the form (clojure.core/deref x) you need to write it as ('clojure.core/deref x)". We've already stolen ~ and ~@.

{:m !m :r @r ::m/as @r}

looks much better to my eyes than the former example.


@SevereOverfl0w Had recommended we hold off on using the @ symbol in a previous discussion but I wonder what he thinks of this. Well, Dominic, what say you? 😄

@jimmyhmiller What do you think?

@noprompt noprompt added the zeta label Mar 6, 2020
@timothypratley
Copy link
Collaborator Author

timothypratley commented Mar 6, 2020

Ah, FWIW I'm highly suspicious of @ 😄
When you say % has a meaning, do you mean it's meaning within the m/with context? Does it have a meaning outside of that context?

Just brainstorming alternatives here:
%%r does double percent help?
{:as (m/pattern ?p) :x ?p}

Maybe we don't even need a special name?

{:as ?p, :x ?p} seems logical to me, but the compiler might not think so hahahaha
{m/recur ?p :x ?p} ???

@noprompt
Copy link
Owner

noprompt commented Mar 6, 2020

When you say % has a meaning, do you mean it's meaning within the m/with context?

Yes. m/with is more or less the equivalent of let with %name symbols instead of unsigiled symbols i.e. x. The reason for sigils, in general, is to make it easy, at a glance, to understand the precise role of a given symbol. Several languages do this. Clojure has ', :, #", etc. Ruby has @, @@, $, etc.

Does it have a meaning outside of that context?

Yeah, that's an unbound reference error. 😄

does double percent help?

?? has also come up but I don't think I want to go the double up route. If we could stick with a single sigil that'd be preferable.

I'm curious, what about @ has you suspicious?

@noprompt
Copy link
Owner

noprompt commented Mar 6, 2020

@timothypratley If you're looking to cut down a bit you could do something like this in the meantime:

(m/defsyntax rec [reference pattern]
  `(m/with [~reference ~pattern]
     ~reference))

(rec %r {:m !m :r (m/or %r {}})

@timothypratley
Copy link
Collaborator Author

timothypratley commented Mar 6, 2020

rec

Oh - that is really nice! I think that is better than adding a new sigil.

I'm curious, what about @ has you suspicious?

Well, you originally wanted to use it to match derefable things, so using it to "name a pattern" seems to conflict with that goal. @ has some baggage from Clojure and potential use cases for Meander in the future, and it just doesn't feel representative of "name a pattern".
I think it's a fine sigil, just better suited elsewhere.

rec covers the use cases I care about.

As a side note on sigils, is *p available? (not suggesting it for this purpose though, just curious)!!!!

@timothypratley
Copy link
Collaborator Author

*p is taken for mutable variable.

@yuhan0
Copy link
Contributor

yuhan0 commented Mar 11, 2020

I don't think it's a good idea to introduce new syntax for such a narrow use-case, especially when it's simply sugar for a more explicit form.

Besides, this forces one to put the base case nil inside the recursive map pattern itself

(m/with [%r {:m !m, :r (m/or nil %r)}]
        %r)

instead of the more sensible

(m/with [%r (m/or nil
                  {:m !m :r %r})]
        %r)

For instance if %r has 3 different cases this proposed syntax is not easily generalizable:

(m/match [1 2 [[3] 4 {:value 5 :rest [{:value 6 :rest []} 7]}]]
  (m/with [%r (m/or
                (m/and (m/pred number?) !n)
                (m/seqable %r ...)
                {:value !n :rest %r})]
    %r)
  !n)
;; => [1 2 3 4 5 6 7]

@timothypratley timothypratley changed the title Syntax proposal: :as %p implies m/with Syntax proposal: m/rec shorthand for m/with single case Mar 11, 2020
@timothypratley
Copy link
Collaborator Author

@yuhan0 @noprompt I agree that the original proposal has some limitations which make it less useful to rec but I do think rec is useful and doesn't steal a sigil... so can you clarify if you agree on that? Also I wonder if rec should contain an implied m/or

@yuhan0
Copy link
Contributor

yuhan0 commented Mar 11, 2020

I don't know, it's useful but feels like the sort of thing which the user should define for themselves with defsyntax rather than belonging in Meander core.
Currently there's already a little overlap between the use cases of with and cata, introducing another named operator in the same space seems a little cluttered and approaching the "map fatigue" territory of having multiple ways to express the same intent.

@yuhan0
Copy link
Contributor

yuhan0 commented Mar 11, 2020

That's of course just my personal opinion as a user of the library - with DSLs there's always a tradeoff between a small/restrained vs large/expressive vocabulary. I really like the way Meander is designed so far with a relatively small set of "primitives" and powerful ability to extend them via defsyntax, eg. I've been using my own map-of operator for months now without needing it to be officially sanctioned by the core syntax.
Maybe I just haven't been dealing with recursively nested data enough to find rec important, ultimately it's up to the maintainers to decide :)

@noprompt
Copy link
Owner

This is all fantastic discussion. I'm on the fence about adding it to core proper. What convinced me to proceed with adding map-of and submap-of was that it came up in a number of discussions and recently a friend of mine tried to express something like

(m/map-of (m/pred string?) _)

as

{(m/pred string?) _}

Of course, once you're skilled with the library, its easy to know how to write this down the latter with m/with, however, if you're not so skilled and only work with Meander here and there, well, that can be a source of confusion.

We can leave this issue open and sit on it. If it comes up enough, yeah, let's add it.

@SevereOverfl0w
Copy link
Contributor

I would probably match on 'clojure.core/deref for what I'm doing anyway 🤔. I think this is probably fine. As long as there's never any confusion about when I'm actually derefing.

@timothypratley
Copy link
Collaborator Author

timothypratley commented Mar 16, 2020

If Meander had a slick way to express recursion, it would open up a whole new class of expressions that could be written directly as patterns.

Meander already has m/with and m/cata, so the first question is "are they slick?"

Yes, I can express recursion with them. Here are 3 approaches to solving the same problem, one with m/with, one with m/cata and one with plain old recursion.

(m/rewrite {:resources {:r1 {:methods   {:m1 "m1"
                                         :m2 "m2"}
                             :resources {:r2 {:methods {:m3 "m3"}}}}}}
  (m/with [%p {:methods   (m/seqable [_ !methods] ...)
               :resources (m/seqable [_ %p] ...)}]
    %p)
  [!methods ...])

(m/rewrite {:resources {:r1 {:methods   {:m1 "m1"
                                         :m2 "m2"}
                             :resources {:r2 {:methods {:m3 "m3"}}}}}}
  {:methods   (m/seqable [_ !methods] ...)
   :resources (m/seqable [_ !resources] ...)}
  [!methods ... & (m/app #(apply concat %) ((m/cata !resources) ...))])

((def zzz
   (s/rewrite
     {:methods   (m/seqable [_ !methods] ...)
      :resources (m/seqable [_ !resources] ...)}
     [!methods ... & ~(mapcat zzz !resources)]))
 {:resources {:r1 {:methods   {:m1 "m1"
                               :m2 "m2"}
                   :resources {:r2 {:methods {:m3 "m3"}}}}}})

Observing from the three different approaches:

  • m/with may only appear in the match pattern
  • m/cata may only appear in the substitution pattern
  • m/with can define many patterns in bindings
  • m/with can occur within the context of another pattern, and can place sub-patterns in any context within its final pattern.
  • m/cata must always recur to the top-level pattern, really it just avoids naming the transform
  • Subjectively I prefer how m/with defines the pattern to match "up front"
  • m/with serves 2 purposes; (a) splitting patterns up (b) recursion
  • Subjectively I think "plain old recursion" wins in this showdown... m/with does too much, and m/cata needs a shoehorn to be made use of. TBH I'd probably advise anyone looking at a recursive situation to name their transform and use "plain old recursion" as m/with and m/cata feel kinda tricky to me.

The idea behind :as %p was to find a way to do less more compact. In hindsight :as was a bad idea, I should have proposed :with %p:

(m/rewrite {:resources {:r1 {:methods   {:m1 "m1"
                                         :m2 "m2"}
                             :resources {:r2 {:methods {:m3 "m3"}}}}}}
  {:with %p
   :methods   (m/seqable [_ !methods] ...)
   :resources (m/seqable [_ %p] ...)})
  [!methods ...])

So as to avoid collision with {:as ?x}. {:with %p} appeals to me because it is compact and the rule of use aligns with m/with (but does not allow quite as much as with). :with %p sigil usage would be consistent, as the sigil while being in a slightly different context from m/with has the same intent: to label a pattern, not create a variable.

(rec %p {...}) is pretty much the same thing as {:with %p} except I can defsyntax it. I do not think {:with %p} can be defsyntaxed. Subjectively I also think it is more explicit in a good way. It's interesting to think about what this should mean:

(m/and %p {:methods   (m/seqable [_ !methods] ...)
           :resources (m/seqable [_ %p] ...)})

%p is clearly meant to be a pattern not a variable... Could this be a way to express recursion with no new syntax at all??? 😄(m/and %p {...}) is analogous to (m/and ?x {...}) which is analogous to {:as ?x}. %p has meaning: a pattern. (m/and %p {...}) is analogous to (m/with [%p {...}] %p).

I agree that there should be a goal to keep the set of operators as small as possible, but I also think it is worth comparing notes on recursive expressions and searching out opportunities to apply Meander in recursive scenarios.

@noprompt noprompt closed this as completed Jul 1, 2021
Repository owner locked and limited conversation to collaborators Jul 1, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

4 participants