Arc Forumnew | comments | leaders | submitlogin
A follow-up on the pipe operator
2 points by akkartik 4040 days ago | 8 comments
3 weeks ago (http://arclanguage.org/item?id=18044) I said that arc can merge clojure's -> and ->> operators thanks to its [.. _ ..] syntax for simple functions. I just added this to wart without any reader changes:

  mac (x -> f)
    `(,f ,x)

  mac (x -> f) :case (mem? '_ f)
    `((fn(_) ,f) ,x)
Now I can do things like:

  3 -> _+1
The catch of course is that '_ is now special inside ->, and that only the pipe operator supports the simpler syntax for anonymous functions. Not as powerful, but still worthwhile given how nicely you can break down ssyntax chaining when there's a string or expression in between:

  a.b."foo".c.d  # illegal
  (a.b "foo") -> _.c.d  # legal
The interruption here has a proportionate effect; you can keep ssyntax before and after it. In arc you have to unwind the entire thing:

  (((a.b "foo") c) d)
Thoughts?


2 points by thaddeus 4040 days ago | link

I like it. Can this be added into anarki?

-----

2 points by akkartik 4040 days ago | link

Done! https://github.com/arclanguage/anarki/commit/afdfdf929a

I've also extracted the _ handling into a new function called (ugh) functionize. And I've fixed zap to show how you can make other functions "_ aware".

But like I said, this isn't as valuable in arc since you already have the [.. _ ..] syntax. All it permits is more widespread ssyntax use. You can say _.x instead of [_ x], and you can chain ssyntax around obstacles.

I found the idea interesting because it asks the question: do we need [.. _ ..] syntax, or can we eliminate the need for this reader macro just with a few simple changes to the functions where we most often use it?

-----

4 points by rocketnia 4040 days ago | link

This is a horrible change. I didn't respond to the original post because of "Thumper's rule," and I couldn't rebut thaddeus in time to stop you. :(

-

== Problems with functionize in general ==

The functionize-based utilities are discontinuous about the way they detect underscores: The body can use _ three times, or two times, or one time, but as soon as it uses _ zero times, it means something completely different. Thanks to this, I can break several layers of code by making a single local edit. But will I? Yes:

The 'treemem function detects occurrences of _ without regard for quoting or local scopes. So if I use an _ to activate one functionize-based utility, then I'll accidentally activate all the other functionize-based utilities which surround that one. If I want to avoid refactoring several layers of code each time I edit, I pretty much have two options:

- I can avoid putting an _ anywhere in my code, in which case this functionize feature won't be very useful to me.

- I can make sure to activate each and every functionize utility as soon as I use it, in which case they would have been better off as 'let variants. For instance, I might settle on the idiom (zap (do '_ ...) foo), but it would be more convenient to say (zaplet orig foo ...).

-

== Problems in Arc ==

Your Anarki commit is one of those things that is "guaranteed to break all your code." Personally, I like using the pattern (zap [map [...] _] args), which now breaks since the _ activates zap's automatic function wrapper. It seems you would want me to write (zap (map [...] _) args) instead, but for compatibility with Rainbow, Jarc, etc., I think I'll define a macro (itfn ...) and write (zap (itfn:map (itfn:...) it) args). Effectively, I'll be recreating the [...] functionality in the way I like.

Meanwhile, Arc already covers a lot of the functionality of Clojure's -> and ->> operators using (aand ...). If you still miss -> or ->>, I recommend just implementing an 'aand variant that doesn't short-circuit on nil. Call it 'ado or something.

Here's how to use 'aand/'ado with the examples from that thread you linked to (http://arclanguage.org/item?id=18044):

  (-> (range) [map [* _ _] _] [filter even? _] [take 10 _] [reduce + _])
  (-> 5 [+ _ 3] [/ 2 _] [- 1 _])
  
  (ado (range) (map [* _ _] it) (filter even? it) (take 10 it) (reduce + it))
  (aand 5 (+ it 3) (/ 2 it) (- 1 it))
-

== Considerations in Wart ==

Clojure's -> and ->> operators are not infix, and that might be good for their indentation. I'm not really sure.

  (-> (...)
    (...)
    (...
      ...)
    (...))
  
  (...) ->
    (...) ->
    (...
      ...) ->
    (...)
  
  (...)
    -> (...)
    -> (...
         ...)
    -> (...)
On the other hand, I like that you can potentially use infix to combine things like -> and ->> into one chain:

  (...)
    -> (...)
    ->> (...
          ...)
    -> (...)

-----

1 point by akkartik 4040 days ago | link

Just spotted your comment while I'm working on something else. Haven't digested it all, but judging from the first five words -- feel free to revert! It was definitely intended as an experiment, and I'm not attached to it. I may well do so myself later today if you don't get to it first.

-----

1 point by akkartik 4040 days ago | link

Ok, done reading now, and you're right, I'll revert it.

I can only defend myself against the wart section :) In wart the pipe operator can only take two args and is intended to be used in infix. I use a non-infix transform for more args, and for prefix mode in general: https://github.com/akkartik/wart/commit/ec0f9a38b8

My weak defense for the rest: functionize and the _ syntax was only intended for tiny expressions.

-----

2 points by rocketnia 4039 days ago | link

"I can only defend myself against the wart section :) In wart the pipe operator can only take two args and is intended to be used in infix. I use a non-infix transform for more args, and prefix mode in general"

Oh, so you're pursuing both options at once. I look forward to you figuring out what kind of indentation you prefer here. :) My "considerations about wart" section was only wishy-washy anyway.

---

"For the rest, my weak defense is that functionize and the _ syntax is only intended for tiny expressions."

In Penknife, when I used the a'b operator as sugar for (b a), I found I ended up with a few really long lines of a.b.c'd'e.f, so it kinda suffered from its own success. ^_^ My a'b is the same as your (a -> b._), and it exactly corresponds to your no-underscore special case, (a -> b), so I expect you to have the same issue.

I suspect these syntaxes actually have a special tendency to let sugar accumulate, driving them away from the ideal "tiny expressions" case. Specifically, they make it possible to inject new code without breaking apart the surrounding sugar first:

  a.b.f.c.d                  # before refactoring
  a.b."foo".c.d              # illegal
  a.b -> (_ "foo") -> _.c.d  # legal? (not quite the example you gave)
  a.b'[itfn:it s.foo].c.d    # Penknife code of similar generality
Fortunately for wart, its infix operators allow whitespace in between, which possibly means you can write these long expressions on multiple lines. (That wasn't the case in Penknife.)

-----

1 point by akkartik 4039 days ago | link

"I look forward to you figuring out what kind of indentation you prefer here. :)"

Here's my current thinking: https://github.com/akkartik/wart/blob/aec0af2676/082macex.wa...

"these syntaxes actually have a special tendency to let sugar accumulate, driving them away from the ideal "tiny expressions" case."

Definitely. The wart example you gave is indeed legal, but the actual _ expression is still simple, even though the chain is long.

It seems nested expressions often work fine because each macro only wraps its arg and leaves the rest to the inside.

  (zap (map car._ _) (list list.1 list.2)
  => (zap (fn(_) (map car._ _)) ..)
  => (zap (fn(_) (map (fn(_) car._) _)) ..)
But that's less readable than when you have the [] delimiting scopes.

Some other concrete examples I came up with of the problems you pointed out:

  (zap (cons? & (fn(_) ..)) ..)  # broken by nested scope
  (zap (cons? & (fn(x) (+ '_ car.x)) ..)  # broken by quoting

-----

1 point by akkartik 4039 days ago | link

It's out, and I'm not putting it back or anything, but FWIW:

  (mac functionize (expr)
    (if (and (treemem '_ ssexpand.expr)
             (~caris expr 'make-br-fn)
             (~caris expr 'fn))
      `(fn(_) ,expr)
      expr))

  arc> (= x '((1) (2) (3)))
  arc> (zap [map [car _] _] x)
  arc> x
  (1 2 3)
This idea was in the wart version from the start (https://github.com/akkartik/wart/commit/7e5d620b28#diff-1) but I didn't mention it for simplicity, and then totally forgot to carry it over into anarki.

It's still pretty hacky though, and has more in common with C's macros than with lisp's.

-----