~icefox/garnet#42: 
Method syntax vs. threading calls

Just thinking about design a little. Thought I'd written something about this already but can't find it. Inspired by https://brettrowberry.com/an-f-devs-perspective-on-clojure. In Rust it is very convenient and nice to thread method calls together like so:

foo.bar()
    .baz()
    .bop()

In functional languages you compose functions to do a similar thing while having data flow through them:

bar foo
  |> baz
  |> bop

BUT, the Rust form is a little more powerful because it namespaces the methods based on the type of the result, so really to write it in F# you'd have to do:

bar foo
  |> BarReturnType.baz
  |> BazReturnType.bop

However, Clojure and Fennel have a separate threading macro that basically does that for you, and is a bit more concise/easy to compose:

(-> "clojure"
    .toUpperCase
    .toCharArray
    first
    str)

And they have separate macros for "thread first arg" and "thread last arg", which is another weakness of the F#/Haskell style.

...Not sure that this is actually superior to the Rust approach though, at least for this language. Kinda tops out to "see what they must do to achieve a fraction of our power". Still, it's something I've pondered and the Clojure style is a new solution to me for what was previously an intractable issue.

Status
REPORTED
Submitter
~icefox
Assigned to
No-one
Submitted
2 years ago
Updated
7 months ago
Labels
T-DESIGN

~icefox 1 year, 11 months ago

Very interesting is https://srfi.schemers.org/srfi-197/srfi-197.html , which allows a placeholder for the value to chain:

(chain (alpha) (beta _) (gamma _) (delta _) (epsilon _) (zeta _) (eta _))
 
(chain bowl
       (add flour _)
       (add sugar _)
       (add eggs _)
       (mix _)
       (pour _)
       (bake _ (fahrenheit 350)))

This solves a big problem with this sort of syntax imo.

I tried to think of a way to incorporate pattern matching into this, but it didn't work. Go far enough with it and it just becomes "calling a function". Still!

Sooooo what would the Scheme-like chaining look like in Garnet? To start off with the Rust-y model with no placeholders it would have to be:

bowl
    .add(flour)
    .add(sugar)
    .add(eggs)
    .mix()
    .pour()
    .bake(farenheit(350))

That would literally translate into something like

chain bowl as
    add(_, flour)
    add(_, sugar)
    add(_, eggs)
    mix(_)
    pour(_)
    bake(_, farenheit(350))
end

There's two disconnected issues here: the variable positioning and the namespacing. This bit only considers the variable positioning. If you add namespacing to it, you could write it as above and have it compile to:

let _ = bowl.add(bowl, flour)
let _ = _.add(_, sugar)
let _ = _.add(_, eggs)
let _ = _.mix(_)
let _ = _.pour(_)
let _ = _.bake(_, farenheit(350))

Whether or not that is desirable, idk. Not sure how it would interact with the ergonomics of Garnet's modules in practice. But you could do the Clojure-ish approach of having a preceeding . to make the namespacing explicit, so:

chain bowl as
    .add(_, flour)
    .add(_, sugar)
    .add(_, eggs)
    mix(_)
    pour(_)
    bake(_, farenheit(350))
end

could mean:

let _ = bowl.add(bowl, flour)
let _ = _.add(_, sugar)
let _ = _.add(_, eggs)
let _ = mix(_)
let _ = pour(_)
let _ = bake(_, farenheit(350))

which seems like it would do everything we want it to...

Technomancy on the topic sez:

13:00 < technomancy> it's anaphoric unfortunately
13:00 < technomancy> once you solve the anaphoric problem it ends up looking a lot less clean
13:03 < Icefoz> I don't even know what you would call those operators, besides "compose" or maybe "expression threading" and those don't search well.
13:04 < Icefoz> aha!
13:04 < Icefoz> SRFI 197: pipeline operators
13:04 < Icefoz> whew
13:10 < technomancy> https://clojuredocs.org/clojure.core/as-%3E this is the clojure version of the same thing which does it without anaphora
13:11 < technomancy> but it also introduces new bindings without [] which is icky
13:11 < Icefoz> wait you were talking to me.  what the heck is "anaphoric" in this context?
13:11 < technomancy> it means it introduces a new identifier "out of thin air"
13:12 < Icefoz> hmmm, I see.
13:12 < technomancy> that's bad macro design; macros should always make it clear when new identifiers are introduced; typically by taking them as arguments. fennel's backtick is designed to prevent accidentally doing this, but it's possible to work around it.
13:14 < technomancy> I think clojure's as-> omits [] in order to make it work inside other arrows, but ... ugh
13:14 < technomancy> https://clojuredocs.org/clojure.core/as-%3E#example-568eeddae4b0f37b65a3c280 do not want

~icefox 1 year, 11 months ago

(The proposed Garnet syntax for this is kinda dumb, just consider it WIP.

~icefox referenced this from #48 1 year, 11 months ago

~icefox 1 year, 7 months ago*

Elixir is a decent case of a language where everything is module+function, kinda like Garnet, without Rust's &self-y thing. And it uses the |> pipe operator form and it works extremely well. It just makes it a magical operator that rewrites foo |> thing(x) to thing(foo, x).

~icefox 8 months ago

After using Elixir a lot more, it's not terrible. Definitely a little more work than method chaining, but having to specify which type to dispatch on instead of having it inferred feels more like it meshes with ML platypi vs typeclasses already.

It does get a bit icky if you don't always want the "receiver" to be the first arg in the function call. Elixir helps this with a shortcut lambda syntax but I don't like it very much, though possibly only 'cause Elixir lambdas are too complicated already. Having lots of Fancy Combinators that swap around args of functions like Haskell is even worse imo. Being able to specify a placeholder identifier like the Scheme example is an option, but as Technomancy says, that can get hairy. Basically it creates the hygenic macro problem, it appears. So not trying to make some clever way to nest pipeline/thread calls is probably the best choice.

For more discussion on that, see https://fennel-lang.org/reference#macro-gotchas and https://gist.github.com/nimaai/2f98cc421c9a51930e16#variable-capture

~icefox 7 months ago

At some point I just started assuming that we would just use pipe operators rather than method chaining. This is still worth a bit of real research and a proof of concept, though.

Register here or Log in to comment, or comment via email.