~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
1 year, 3 months ago
Updated
10 months ago
Labels
T-DESIGN

~icefox 1 year, 2 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, 2 months ago

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

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

~icefox 10 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).

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