Stream: ideas

Topic: static dispatch - pass_to alternative


view this post on Zulip Richard Feldman (Dec 25 2024 at 01:41):

what if instead of .pass_to(foo) we had just .(foo)? A really cool thing this would enable:

import Instant exposing [hours]

four_hours = 4.(hours)

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:42):

(as opposed to the currently-proposed 4.pass_to(hours))

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 01:42):

how do extra args work?

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:43):

same as .pass_to

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 01:43):

4.(sub(5))?

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 01:43):

4.(sub, 5)?

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:43):

I was thinking the latter, but wow does it look hilarious when you use it with Num.sub :joy:

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:43):

(either style)

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 01:44):

Yeah, I may have picked a particularly bad example function. The second one looks a lot like a tuple which is kinda strange.

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:44):

comparing to an earlier list of examples:

.pass_to()

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .pass_to(ul, [class("pagination")])

.()

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .(ul, [class("pagination")])

view this post on Zulip Richard Feldman (Dec 25 2024 at 01:45):

yeah looks strange there too

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 02:02):

Yeah, I think @Eli Dowling is probably right about the best alternatives to .pass_to() being some sort of extra symbol like .>

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .>ul([class("pagination")])

Will look a bit odd and take some time to get used to, but it is just a minor extra visual indicator to note the function is imported instead of being from the defining module of the type.

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:06):

I was literally coming here to once again say that an operator still seems cleaner than this suggestion! Thanks @Brendan Hansknecht :)

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:13):

I'm not totally married to .>but I do think starting with a . has a nice sense to it.

I think .>> looks more visually distinct and is essentially the same speed to type.

Some comparisons:

.>

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .>ul([class("pagination")])
        .do_final(page_thingy)

.>>

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .>>ul([class("pagination")])
        .do_final(page_thingy)

With a space:

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .> ul([class("pagination")])
        .do_final(page_thingy)

view this post on Zulip Richard Feldman (Dec 25 2024 at 02:16):

I'm open to the possibility, but I haven't seen any yet (including these) that seem worth the strangeness budget

view this post on Zulip Richard Feldman (Dec 25 2024 at 02:17):

these look like Haskell operators :sweat_smile:

view this post on Zulip Richard Feldman (Dec 25 2024 at 02:17):

(sorry if that was too harsh)

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 02:24):

At this point, my gut feeling is that if we want clarity it will be .pass_to and if we want brevity, it will be some sort of extra symbol.

view this post on Zulip Richard Feldman (Dec 25 2024 at 02:36):

going back to the original example, we could support this:

import Instant exposing [h]

in_24_hours! = || Instant.now! + 24h

I've wanted something like this for units in general, e.g. 42cm, 16px etc. It kinda reminds me of infix operators in that I think certain operations are easier for me to follow when they're written in this style.

if we did that, we'd probably want to change the syntax for typed number literals (probably to methods) to support that, e.g. 42u8 becoming 42.to_u8() but that might be a nice simplification in regardless

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:37):

I think given we originally had |> which people had to learn as pipe. Replacing it with .> Which people have to learn as pipe is not too bad at all.

I think you've just got Haskell trauma Richard :stuck_out_tongue_wink:

view this post on Zulip Richard Feldman (Dec 25 2024 at 02:39):

a lot of my aversion to unusual symbols is seeing other people bounce off of Haskell because of the symbols, without ever making it to the semantics

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:43):

I would argue the issue with symbols in Haskell is mostly when they combine together.
I can read normal Haskell code fine, spattering of symbols, all good.
But when operator precedence rules start coming in and the symbols get dense I feel like I have zero intuitive understanding of what's going on.
(I don't know Haskell but read bits occasionally)

I don't think this has that issue, mostly because the precedence is clear and we don't have any other "function application adjustment" type symbols.

view this post on Zulip Brendan Hansknecht (Dec 25 2024 at 02:48):

I think most beginners wouldn't notice or care about this any more than |>

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:51):

@Richard Feldman I don't know if I missed something, but what is || doing in that example??

view this post on Zulip Eli Dowling (Dec 25 2024 at 02:52):

Oh wait, it's a function declaration. My bad

view this post on Zulip Oskar Hahn (Dec 25 2024 at 08:43):

For the question, if we should use ? or try, the idea was to have both for the moment and experiment with both ways, but that we should remove one of them before a 1.0. (See for example here )

Could we do the same for .pass_to and .>? Have both in the language when static dispatch gets implemented, experiment with both of them, and restart the discussion, somewhere before a 1.0?

view this post on Zulip Eli Dowling (Dec 25 2024 at 09:11):

I like this idea a lot, and was going to suggest it but didn't like the idea of "two ways to do one thing".
Having this experiment also end at the same time as the try ? experiment makes perfect sense and solves that problem.

Big +1 from me

view this post on Zulip Anthony Bullard (Dec 25 2024 at 11:59):

I still dont see static dispatch and the existing |> operator as at odds with one another, .> is fine but not that much more understandable for someone who hasn’t seen pipelines before and eats weirdness budget for those who have.

view this post on Zulip Eli Dowling (Dec 25 2024 at 12:06):

I think the visuals when in a pipeline are worse

view this post on Zulip Georges Boris (Dec 26 2024 at 16:45):

Just a wild thought here, but what if we could use . as a pipeline? All use cases could be achieved by both inline static_dispatch and the pipeline one, but sometimes the pipeline one could be used for legibility sake:

# lets assume both modules have an `hour` function
import TimeRange
import TimeInterval

# module function with same name
hour : Int -> String
hour = |x| Str.fromInt x

# Inline

timeRange = Now!.to_unix.(TimeRange.hour)
timeInterval = Now!.to_unix.(TimeInterval.hour)
timeString = Now!.to_unix.hour

# Pipeline

timeRange =
  Now!
  . to_unix
  . TimeRange.hour

# Mixing both

timeRange =
  Now!.to_unix
  . TimeRange.hour

# If for some reason the "Num" module also had
# an `hour` function we could just disambiguate at call site

# hour is defined on module, so it is more specific
# and should take precedence over Num.hour?
timeString = Now!.to_unix.hour
timeFromNum = Now!.to_unix.(Num.hour)

view this post on Zulip Sam Mohr (Dec 26 2024 at 16:57):

First, a reminder that we're mostly (if not fully) aligned that we'll be using parens for function calls

view this post on Zulip Anthony Bullard (Dec 26 2024 at 16:59):

Yeah, I think PNC _might_ stay around even if Static Dispatch doesn't work out. But others call me out if I read that wrong

view this post on Zulip Sam Mohr (Dec 26 2024 at 16:59):

And also, that syntax currently means "give me an anonymous function that gets the field abc from a record", and this would make a single space the difference between calling a local function and defining a field getter. That makes it pretty ambiguous in my eyes and I think we should prefer a syntax that's visually unambiguous for someone reading Roc.

view this post on Zulip Sam Mohr (Dec 26 2024 at 17:01):

Anthony Bullard said:

Yeah, I think PNC _might_ stay around even if Static Dispatch doesn't work out. But others call me out if I read that wrong

You're right it'll probably stay even if SD is rejected because of the phenomenon that people seemed to like Gleam more just for PNC even if everything else was the same

view this post on Zulip Georges Boris (Dec 26 2024 at 17:05):

oh - I haven't thought about record accessors syntax. that is indeed confusing with other usages of .xxx .

But to clarify - what do you mean by using parens for function calls ? Oh - you mean that in my example Now!.to_unix.(xxx) the .(xxx) seems like it is the parans of a function call itself?

Or do you mean that all function calls should be using parens, even when there are no extra arguments in the pipeline? Now!.to_unix().(TimeRange.hour()) ?

view this post on Zulip Anthony Bullard (Dec 26 2024 at 17:06):

I think if we are doing this, I'd rather @Eli Dowling 's suggestion (or just keeping Pizza around)

view this post on Zulip Georges Boris (Dec 26 2024 at 17:15):

Folks - sorry if this is the wrong thread but I want to understand this.

The whole idea of dynamic dispatch is that by using .someFunction that would be inferred from the module of the current type I'm acting on, right?

And pass_to would be used when the pipelined function is not from that module? So something like:

# I'm ignoring `Num` for the example

(12.12).floor.to_string.pass_to(Parser.parseInt)

# Float
# Float.floor
# Int.to_string
# Parser.parseInt

Or did I get it wrong? (Because I'm thinking about issues like I showcased above when there are multiple conflicting modules that act on the same type with functions named the same)

If my reasoning is correct, wouldn't the pass_to with whatever syntax be at least as common as the inferred dispatch?

view this post on Zulip Sam Mohr (Dec 26 2024 at 17:19):

Even if a function has zero args, it should have parens

view this post on Zulip Sam Mohr (Dec 26 2024 at 17:20):

So it wouldn't be Now!.floor.to_string, it'd be Now!().floor().to_string()

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:50):

Georges Boris said:

The whole idea of dynamic dispatch is that by using .someFunction that would be inferred from the module of the current type I'm acting on, right?

"Static" dispatch and yes.

And pass_to would be used when the pipelined function is not from that module?

Also, yes. It is for any local or imported function

As mentioned by others, only slightly related to static dispatch, roc is also planning to switch to parens and commas calling syntax. So your code would be

(12.12).floor().to_string().pass_to(Parser.parseInt)

(Because I'm thinking about issues like I showcased above when there are multiple conflicting modules that act on the same type with functions named the same)

So conflicts are essentially the same as today. You have to import something to use it with .pass_to. that means you have the standard conflicts of any other import.

For static dispatch, it is always from the defining module of the type. So it can never have conflicts. There is only ever a single defining module.

view this post on Zulip Brendan Hansknecht (Dec 26 2024 at 22:56):

Just to show this in the current planned syntax

# lets assume both modules have an `hour` function
import TimeRange
import TimeInterval

# module function with same name
hour : Int -> Str
hour = |x| Str.fromInt(x)

# current accepted

timeRange = Now!().to_unix().pass_to(TimeRange.hour)
timeInterval = Now!().to_unix().pass_to(TimeInterval.hour)
timeString = Now!().to_unix().pass_to(hour)

# Syntax proposed at the top of the thread `.()` instead of pass_to

timeRange = Now!().to_unix().(TimeRange.hour)

# another example syntax

timeRange = Now!().to_unix().TimeRange::hour()

Note, I'm not actually sure how this would work with the .> syntax. How does that syntax deal with module qualifiers?

view this post on Zulip Sam Mohr (Dec 26 2024 at 23:10):

Pretty sure it'd just be standard dot-qualified module calls

view this post on Zulip Sam Mohr (Dec 26 2024 at 23:12):

timeRange = Now!().to_unix().>TimeRange.hour()

view this post on Zulip Anthony Bullard (Dec 26 2024 at 23:27):

I thought of .> as just being a Binop like pizza/pipe is today. Like it just being a small change (that I think is a little silly) aesthetically and zero change to structure or semantics.

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 01:01):

Hmm....I guess it could be qualified with dot....just looks off.

view this post on Zulip Sam Mohr (Dec 27 2024 at 02:01):

I think it looks close to normal with newlines

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:18):

Yes

view this post on Zulip Brendan Hansknecht (Dec 27 2024 at 02:18):

Just inline that is odd to read

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:36):

going back to the original idea in this thread, although 4.(sub(5)) looks very silly to me, this actually doesn't:

4.(sub)(5)

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:36):

revisiting the an earlier list of examples:

.pass_to()

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .pass_to(ul, [class("pagination")])

.()

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .(ul)([class("pagination")])

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:38):

Oh yeah .(name)(...) would be fine

view this post on Zulip Anton (Dec 28 2024 at 17:39):

I still prefer pass_to

view this post on Zulip Sam Mohr (Dec 28 2024 at 17:42):

It looks slightly nicer than .> to me at this point...

view this post on Zulip Sam Mohr (Dec 28 2024 at 17:43):

I'd prefer pass_to if it was one word

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:49):

yeah the thing I like about it over .> is that it doesn't look alien to me :big_smile:

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:49):

no mainstream languages have have .( or .> but . and parens appear next to each other all the time, whereas . and angle brackets basically never do

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:50):

Also, I think I have seen a similar syntax before....not recalling where though. For using dot method syntax on a function return by another function it would be x.(some_fn())(args)

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:50):

yeah we already have (foo.bar)(baz) for getting a field off a record which happens to contain a function, and then calling that function

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:50):

Where some_fn is returning a function that takes an x and args)

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:50):

and now that you mention it, this is a similar situation :thinking:

view this post on Zulip Richard Feldman (Dec 28 2024 at 17:56):

I still think .pass_to is more self-descriptive, but I have to admit, whenever I've run into wanting this, I have been annoyed by how verbose .pass_to looks and I try to find some other way to write it :sweat_smile:

view this post on Zulip Anton (Dec 28 2024 at 17:57):

Probably because I'm used to space separated Roc, lots of ()easily feel noisy and messy to me

view this post on Zulip Isaac Van Doren (Dec 28 2024 at 17:57):

.() looks stranger to me than .> but I’d take either over pass_to

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 17:58):

I think the context of calling a lambda in parens like seen with records makes .(some_fn)(args) make way more sense. Makes it easier to explain

view this post on Zulip Richard Feldman (Dec 28 2024 at 18:01):

Anton said:

Probably because I'm used to space separated Roc, lots of ()easily feel noisy and messy to me

this might sound strange, but I actually feel a lot differently about the noisiness of parens depending on how they're syntax highlighted.

specifically if they're sort of faded out and lower contrast, they don't look as noisy to me. So that's the highlighting scheme I used in the static dispatch doc.

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:02):

As I said before, for beginners (and possibly for general readability, though not sold on this), I think .pass_to is probably the clearest.

For general use, and getting out of the way while still being delineated from local imports, I think symbols are best here. I think that .> has no explanation except that it kinda looks like |> (which any go away). I think that .() is closer to calling lambda in a record so at least has a bit of precedence.

I think that symbols get out of the way, but add clarity for when you need it. As such, I am pro trying symbols, but I ultimately think it depends on what our goals are for the total beginner experience.

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:04):

Essentially anyone no matter the language background will figure out/understand .pass_to. with symbols, this will be much more of a guess as to why the symbols are needed and what they mean.

view this post on Zulip Anton (Dec 28 2024 at 18:09):

I think Richard said before that pass_to usage does not come up that often, in that case it seems best to go for something slightly more verbose and beginner friendly

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:11):

It really depends on what percentage of types are nominal types. Also depends on what percentage of types the author has control over. I think that it would come up frequently with Str and List U8

view this post on Zulip Sam Mohr (Dec 28 2024 at 18:11):

If pass_to is basically treated as a keyword, then migrating to another syntax would be really easy. It seems like an easy thing to start with.

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:12):

Whenever I write buffer code, I naturally write custom methods that would never be able to use static dispatch. At the same time, they aren't generally plentiful enough to make it worth defining my own nominal buffer type and module that wraps all the list methods I need

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:12):

So I think there will be plenty of cases where it is common.

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:13):

Anton said:

I think Richard said before that pass_to usage does not come up that often, in that case it seems best to go for something slightly more verbose and beginner friendly

My understanding was that this thread was started cause it was actually hit pretty commonly in practice when working on the real-world example with future roc syntax

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 18:15):

Anyway, I feel like we have collectively spent a ton of time debating these alternative syntaxes. I personally feel we should just pick one and try it out. It should be pretty trivial to change later.

view this post on Zulip Richard Feldman (Dec 28 2024 at 18:35):

if we're going to try one out, I'd like to try .(foo)(bar, baz) because that way we can get data on how confusing it is (or isn't) to beginners. If it's a problem in practice, we'll find out! :big_smile:

view this post on Zulip Jasper Woudenberg (Dec 28 2024 at 21:44):

How would the .(foo)(bar, baz) option look if you wanted to put a lambda in the pipeline? Something like this?

.(|x, y| x + y)(4)

Sorry to add another option to the mix, but I wonder what you think of this option:

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .|links| ul([class("pagination")], links)

So instead of piping directly into a function, you define a lambda that takes the current pipeline argument and then you can write a function body.

My sense is this would be reasonably intuitive to beginners, works well in a broader set of cases (i.e. when the arguments of the passed-to function aren't pipeline-perfect), but it's a bit more verbose when you want to pass to a function that does take the pipeline parameter as the first argument.

view this post on Zulip Georges Boris (Dec 28 2024 at 21:58):

Jasper Woudenberg said:

How would the .(foo)(bar, baz) option look if you wanted to put a lambda in the pipeline? Something like this?

.(|x, y| x + y)(4)

fwiw this seems pretty readable to me! though passing extra variables as arguments to lambdas seem a bit off. So maybe we could omit the 0-arg () when passing a lambda? .(|x| x + 4)

view this post on Zulip Sam Mohr (Dec 28 2024 at 22:08):

I think it's quite understandable as to what's happening, but I think it's not always desirable to put the entire body of a function into a single pipeline. My experience with Rust pipelines has made me feel that most usages of this would be better as an assignment to a variable that gets passed to the intermediate function. The cases where long pipelines are still good are things like iterators, which are lazy. Those tend to not need this syntax. Eagerly-evaluated pipelines should be broken up IMO.

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:14):

I tried out .(foo)() as opposed to .pass_to(foo) and one thing I didn't like about it was the specific scenario of having to use () at the end when it's only taking one argument

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:15):

it seems like if we just said .(foo) and .(foo)() are equivalent, it's fine?

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:16):

obviously it would logcally follow that if val.(foo)(bar, baz) and val.(foo)(bar) work, then val.(foo)() should work too, but it just feels unnecessarily noisy to have to put the extra () on the end

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:16):

It shouldn't conflict with 1-tuples for custom types

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:16):

when val.(foo) can't really meany anything else anyway

view this post on Zulip Ayaz Hafiz (Jan 02 2025 at 17:16):

would this work? .(foo(1, 2)) or .(foo 1 2) as foo(1, 2). Sorry if this has already been discussed

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:18):

I feel like .foo() and .(foo)() matching makes a lot of sense. So I much perfer that. It is consistent and simple as a rule. We don't need magic for everything to remove every level of noise.

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:18):

we did - it felt weird because theoretically you should be able to put any expression inside the parens

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:18):

I'd rather start with requiring the trailing () but would be open to experimenting with desugaring val.(foo) as val.(foo)()

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:18):

so like .(|x| x + 1) (or whatever

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:18):

so then .(foo(1,2)) would be actually calling the expression foo(1, 2)

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:19):

I think Ayaz is suggesting abc.(foo(1, 2)) would go to foo(abc, 1, 2)

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:19):

right

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:19):

but if we do that, then abc.(|x| x + 1) no longer makes sense because we're not treating what's inside the parens as a normal expression

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:19):

it's something special

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:19):

Which assumes that the abc.(<expr>) isn't allowed

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:20):

We currently do that for record builders

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:20):

example from the realworld code base that made me think of this:

Auth.authenticate(req, now!()).and_then!(handle!).pass_to(to_resp)
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)

intellectually, I understand that the middle one is the simplest and most consistent, but I like reading it the least of these three :sweat_smile:

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:20):

Maybe we shouldn't?

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:20):

I think making it be a normal expr makes the most sense, and is also more useful

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:21):

sometimes in pipelines today we do |> into a lambda, and .(...) allowing an expression would mean you could still do that naturally

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:21):

So otherwise would be:

Auth.authenticate(req, now!()).and_then!(handle!) |> to_resp

or

to_resp(Auth.authenticate(req, now!()).and_then!(handle!))

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:22):

yeah

view this post on Zulip Ayaz Hafiz (Jan 02 2025 at 17:23):

what if we just disallow chaining like this? require assigning to an intermediate variable and see how painful is. personally i try to avoid a lot of chaining anyway because hard for me to understand what the intermediate outputs are, and i think it's sometimes a signal that things need to be factored out

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:24):

I strongly agree. I think my Rust code is better because I can't do this.

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:24):

it's certainly possible in the static dispatch world...this is actually the only place in the whole code base where I ended up wanting to use it :big_smile:

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:24):

Imagine chaining one more function:

Auth.authenticate(req, now!()).and_then!(handle!).pass_to(to_resp).exit_code()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)().exit_code()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp).exit_code()
to_resp(Auth.authenticate(req, now!()).and_then!(handle!)).exit_code()
(Auth.authenticate(req, now!()).and_then!(handle!) |> to_resp).exit_code()

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:24):

Though I wouldn't be surprised if structurally-typed values made a pass_to equivalent necessary

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:25):

I do think it's a fair point to say "let's intentionally hold off on implementing anything for it, and see what use cases come up"

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:25):

like we can always add it later, and then have more examples of how different syntaxes could look

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:25):

obviously this is a thing we know we can do, and we have a few different ideas about how it could look

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:26):

and the easiest way to evaluate how worthwhile it is would be to just try living without it and see how much organic demand there is for it in practice

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:26):

It's just sugar, it wouldn't complicate canonicalization or anything else

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:26):

right

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:26):

Especially if we leave in |> for a bit. Any uses of |> that say around would be cases that might match better with one of these syntaxes

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:27):

Otherwise, it will just be cases of more complicated parens or splitting into many local vars in a way that feels unnecesary

view this post on Zulip Romain Lepert (Jan 06 2025 at 12:52):

Another alternative could be

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        .ul(_, [class("pagination")])

view this post on Zulip Sam Mohr (Jan 06 2025 at 16:01):

Something that I've seen in common between this discussion and the purity inference one is a bias for "forward-reading" features. Both val.>func(args) and val.(func)(args) are visually distinct from the method call syntax at the front of the function call. This makes it really easy to figure out whether a method is being called without having to "gather context" first.

As much as this feature has seen good success in Gleam, and I like the aesthetic of it, I think it hurts readability when you have bulky arguments.

view this post on Zulip Ian McLerran (Jan 07 2025 at 02:11):

Oh wow, I hadn’t seen the val.>func(args) suggestion. I know this wasn’t your point Sam, but I just gotta say I love the way that reads. Very clear what it is doing, and to me I like the uniformity of keeping the parens around the args immediately following the func name, as opposed to being interrupted by the parens around the func. Plus it keeps some of those roc/elm aesthetics I love…

view this post on Zulip Sam Mohr (Jan 07 2025 at 02:12):

Hey man, there are a lot of different opinions here, none of them better or worse

view this post on Zulip Sam Mohr (Jan 07 2025 at 02:13):

I think we're planning on starting with val.(func)(args) because enough people like it including Richard to warrant a test drive

view this post on Zulip Sam Mohr (Jan 07 2025 at 02:13):

But it's a pretty simple change to try something else

view this post on Zulip Sam Mohr (Jan 07 2025 at 02:13):

Secret option C: we implement both syntaxes

view this post on Zulip Ian McLerran (Jan 07 2025 at 02:35):

Haha yep, we’re all just here for the love of :roc:!

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:13):

I was talking with José Valim about this topic (and a bunch of others) on the podcast recently, and one thing that came up was that if we went with the maximally concise version of this, you could do:

yesterday = 1.(day).ago!()

later = now!() + 8.(hours)

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:13):

after importing the day and hours functions unqualified

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:15):

that assumes that if you have a 1-arg function you can call it with arg.(fn) and not arg.(fn)()

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:15):

otherwise it would be:

yesterday = 1.(day)().ago!()

later = now!() + 8.(hours)()

view this post on Zulip Anton (Jan 10 2025 at 19:17):

with the maximally concise version of this

What would this look like for functions with multiple arguments?

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:18):

:thinking: I just thought of an interesting argument for putting extra args inside the parens, like this:

arg1.(fn, arg2, arg3)

...is that you could think of it as "in this syntax, you swap the thing before the . with the first thing in the parens, so this desugars to

fn(arg1, arg2, arg3)

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:18):

I'm imagining an animation where they just swap places and the . disappears, and in my head it makes it really easy to understand :big_smile:

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:20):

another point in favor of that syntax is that it is exactly as concise as method calling style

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:20):

the . is just in a different position

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:21):

for context, we were talking about how people like to "add methods to existing types" (to use Rust terminology, but also applies to languages with classes)

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:22):

and a downside of allowing anything like that (which I don't think we should support) is that it makes it harder to look at code and understand where the implementation is coming from

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:22):

but an upside is that it makes the code look more stylistically consistent

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:23):

a nice thing about .( is that the difference is very small, so it's different enough to tell that it's not static dispatch but not so different that it looks like a radical departure from normal style

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:29):

actually considering ago! would be in the platform's Clock module, not the platform-agnostic Duration module, it would be:

import Clock exposing [ago!]
import Duration exposing [hours, day]

yesterday = 1.(day).(ago!)

later = Clock.now!() + 8.(hours)

view this post on Zulip Sam Mohr (Jan 10 2025 at 19:40):

What about the formatting of multiline calls with this?

chain =
    item
    .call(
        with,
        lots,
        of("abc"),
        if args then 123 else 456,
    )
    .(
        local_func,
        other,
        set,
        of(args),
    )

view this post on Zulip Sam Mohr (Jan 10 2025 at 19:41):

We'd maybe want

chain =
    item
    .call(
        with,
        lots,
        of("abc"),
        if args then 123 else 456,
    )
    .(local_func,
        other,
        set,
        of(args),
    )

view this post on Zulip Richard Feldman (Jan 10 2025 at 19:43):

both seem reasonable, although I like the second one better

view this post on Zulip Sam Mohr (Jan 10 2025 at 19:45):

I'd still prefer

chain =
    item
    .call(
        with,
        lots,
        of("abc"),
        if args then 123 else 456,
    )
    .(local_func)(
        other,
        set,
        of(args),
    )

because it looks like a function call, and I think it's totally fine to desugar

x = 123.(func)()

from

x = 123.(func)

view this post on Zulip Sam Mohr (Jan 10 2025 at 19:45):

But I'm okay with "the second one" above

view this post on Zulip Ian McLerran (Jan 10 2025 at 19:46):

Personally, I find the .( a little unintuitive. The notation is so concise and similar to accessing a static dispatch method that 1.(day) reads to me like 1.day. When you mix in extra args, with the first "argument" in the parens actually being the function, it gets really unintuitive.

I think this may just be my ADD which makes me have to really slow down and process that, and maybe I would get used to it, but I feel like it would drastically reduce the speed I can scan and comprehend code. It also seems like it could be a bit confusing for newcomers to remember that the first element in the parens is actually the function.

I still find item.>fn(arg1, arg2) much easier to scan and model mentally than item.(fn, arg1, arg2).

view this post on Zulip Luke Boswell (Jan 10 2025 at 20:00):

yesterday = 1.>day().ago!()

later = now!() + 8.>hours()

Would this work?

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:16):

I really can't stand .> :sweat_smile:

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:16):

every time I see it I have this visceral "no way, never" reaction

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:17):

I really try to be open minded about ideas where I have a negative first reaction because I've come around to a lot of them over the years, but I have zero percent come around on this one :sweat_smile:

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:23):

Do we need to reinvent the wheel? What about |>? Maybe there's a blocker I'm not thinking of, but it actually seems like this should work fine with SD/PNC... Sorry if I'm rehashing conversations which have already happened.

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:24):

I guess the .( was for the sake of easy auto complete...

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:25):

the problem with |> is that you can't keep chaining things after it

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:25):

really only works at the end of a chain

view this post on Zulip Brendan Hansknecht (Jan 10 2025 at 20:30):

I definitely prefer arg1.(fn)(arg2, arg3) over arg1.(fn, arg2, arg3). I think it is much clearer to read and much closer to a standard method call. The other still looks like messed up tuple syntax to me.

view this post on Zulip Brendan Hansknecht (Jan 10 2025 at 20:30):

I would be fine with arg1.(fn) as a sugar for arg1.(fn)().

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:30):

Shouldn't type inference with static dispatch be able to infer all the following types?

res = 123.to_str
    |> to_utf8
    .map(|n| -> "${a}")
    |> join_with(", ")

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:31):

yeah but try putting that on one line :big_smile:

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:32):

Touché

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:32):

yeah that's the problem case

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:33):

res = 123.to_str() |> to_utf8().map(|n| -> "${a}") |> join_with(", ")

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:35):

Should be solved by adding the parens, right?

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:38):

to me, to_utf8().map(...) reads like its own standalone thing

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:39):

we could have the rule be that it doesn't, but at that point we have strangeness with either |> or .(

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:39):

That's fair...

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:40):

I'll admit that is pretty weird when I look at it.

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:40):

I will say that I'm a big fan of 4.(hours) specifically

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:40):

I might be alone on that in this thread, but I know a lot of people who have done a bunch of Ruby and love that (and miss it in other languages)

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:41):

like specifically in the conversation with José he brought it up out of nowhere as a motivating example of wanting to "add methods to types after the fact" because it's such a beloved notation

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:42):

for durations in particular, although I also like it for some units of measure (although it has limitations compared to an actual first-hand units of measure system like what F# has)

view this post on Zulip Jasper Woudenberg (Jan 10 2025 at 20:44):

I remember finding it very confusing in my early Ruby days. Like, I knew that if I tried I'd probably be able to reverse-engineer where I would need to put ., _, , () in the quasi-english invocations to get it to work, but instead I would just throw some options to the wall and hope one sticked.

I still have that occasionally, wondering whether it's 4.hours.from.now or 4.hours.from_now for instance.

It's probably better with types though.

view this post on Zulip Sam Mohr (Jan 10 2025 at 20:45):

Ruby looks great, but is the Wild West

view this post on Zulip Sam Mohr (Jan 10 2025 at 20:45):

The types are definitely the difference

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:46):

Richard Feldman said:

I will say that I'm a big fan of 4.(hours) specifically

I might be alone on that in this thread, but I know a lot of people who have done a bunch of Ruby and love that (and miss it in other languages)

like specifically in the conversation with José he brought it up out of nowhere as a motivating example of wanting to "add methods to types after the fact" because it's such a beloved notation

That makes sense. I can appreciate that. I think the val.(fn, arg1, arg2) is where I struggle. And honestly, even there, I can appreciate the conciseness. It currently feels difficult for me to quickly parse that now (especially in a long chain), but maybe with time it would become second nature.

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:48):

As I think about it, with a good LSP that could highlight fn differently from arg, this wouldn't be bad at all. I'm more concerned with the reading than the writing, but the LSP can help there...

view this post on Zulip Brendan Hansknecht (Jan 10 2025 at 20:50):

Is val.(fn)(arg1, arg2) easier to parse for you? I find it way easier to parse.

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:51):

Yeah, in a world sans .>, val.(fn)(arg1, arg2) is definitely my preference.

view this post on Zulip Richard Feldman (Jan 10 2025 at 20:51):

yeah I also prefer that syntax for multi arg

view this post on Zulip Ian McLerran (Jan 10 2025 at 20:56):

As far as single arg function goes val.(fn) does look a lot cleaner than val.(fn)()...

view this post on Zulip Anthony Bullard (Jan 10 2025 at 21:21):

Richard Feldman said:

after importing the day and hours functions unqualified

This sound a lot like a proposal I made recently....

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:22):

similar for sure!

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:23):

but with the significant difference of there being a syntactically visible way to tell whether it's resolving based on static dispatch or local scope

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:25):

something I realized that I haven't seen come up yet, but which is definitely a case where something along these lines would be nice, is where you're converting from one type to another, e.g.

my_list
.map(|arg| (arg, foo(arg)))
.(Dict.from_list)

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:25):

(assume there might be a bunch more steps in that pipeline)

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:25):

sometimes you'll happen to be able to do .to_dict() (or whatever) but other times the thing you're converting from doesn't know about the thing you're converting to (such as creating a dictionary from a list)

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:26):

at which point a way to pass what you've been building up into a plain function becomes nice

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:26):

I guess another thing that's actually really nice here is being able to do .(Ok)

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:26):

at the end of a bunch of operations

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:27):

in the realworld code base one of the things I was trying out was allowing .Ok()

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:27):

which was nice in a couple of places

view this post on Zulip Richard Feldman (Jan 10 2025 at 23:27):

but if we had .(Ok) it would be equally concise and one less concept

view this post on Zulip Niclas Ahden (Jan 11 2025 at 23:01):

val.(fn)(arg1, arg2) wasn't obvious to me at first, but with context I get it.

Is val.(fn(arg1, arg2)) reasonable? Perhaps it'd be easier for a newcomer to understand. It could be extended to allow a lambda in there if we wanted val.(|argX| fn(arg1, argX, arg2)). Sorry if this is out of touch, I'm not fully up-to-date with the direction.

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:03):

it's an option, although my default assumption is that anything inside parens is an expression that gets evaluated on its own

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:08):

so I'd assume val.(fn(arg1, arg2)) would evaluate fn(arg1, arg2) on its own

view this post on Zulip Niclas Ahden (Jan 11 2025 at 23:11):

I agree, that's confusing. I think both options are a bit confusing, though, but val.(fn)(arg1, arg2) has the benefit that when I read it I go "whoah, something funky is going on with that (fn)".

What do you think of a Ruby-like solution such as val.(fn(_, arg1, arg2))?

view this post on Zulip Niclas Ahden (Jan 11 2025 at 23:29):

For non-Rubyists, Ruby would say val.then { fn _1, arg1, arg2 } or you could use it instead of _1 as per their latest release. We'd use _ instead to mirror a type hole (type hole/variable hole).

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:42):

I think it's a can of worms I'd like to avoid opening right now :sweat_smile:

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:42):

having _ as a placeholder value in expressions (as opposed to patterns) can be taken really far

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:43):

like I think in Scala you can do stuff like (the equivalent of) when _ is which would make the entire when expression become a lambda which takes one argument and puts it where the _ is

view this post on Zulip Richard Feldman (Jan 11 2025 at 23:43):

so I've been trying to avoid using _ for things in that direction because of the "where does it end?" question

view this post on Zulip Niclas Ahden (Jan 12 2025 at 13:14):

Indeed, that's totally fair! I would personally probably like val.(fn)(arg1, arg2) and it'd be good to try.

Some seem to prefer pass_to. I don't have strong feelings. Here are one-word alternatives which I didn't see while reading up on the conversations:

-- to
yesterday = 1.to(day).ago!()
later = now!() + 8.to(hours)
Auth.authenticate(req, now!()).and_then!(handle!).to(to_resp)
blocks = bytes.len().to(List.with_capacity)

-- pipe
yesterday = 1.pipe(day).ago!()
later = now!() + 8.pipe(hours)
Auth.authenticate(req, now!()).and_then!(handle!).pipe(to_resp)
blocks = bytes.len().pipe(List.with_capacity)

-- then
yesterday = 1.then(day).ago!()
later = now!() + 8.then(hours)
Auth.authenticate(req, now!()).and_then!(handle!).then(to_resp) -- API conflict, one of these usages has to give
blocks = bytes.len().then(List.with_capacity)

Sorry if these were considered already, I just didn't see them, even with searching. No need for reasoning etc. if they're not applicable, just thought I'd share them in case it helps!

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:42):

something I'm noticing is that the conciseness of .( makes it useful in more situations, e.g. in the revised tutorial I have in progress for the static dispatch world, I originally wrote this:

get_input! = ||
    echo!("Enter the number of birds:")
    birds = input!().to_i64()?

    echo!("Enter the number of iguanas:")
    iguanas = input!().to_i64()?

    { birds, iguanas }

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:42):

I realized I was missing an Ok on the return value, so I changed it to this:

get_input! = ||
    echo!("Enter the number of birds:")
    birds = input!().to_i64()?

    echo!("Enter the number of iguanas:")
    iguanas = input!().to_i64()?

    Ok({ birds, iguanas })

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:43):

this is correct, but I was a little sad about how the original focused on "here's the thing we're returning" whereas the correct one focuses on "we're returning Ok" which is the least-interesting part about that last line

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:44):

but then I tried it like this:

get_input! = ||
    echo!("Enter the number of birds:")
    birds = input!().to_i64()?

    echo!("Enter the number of iguanas:")
    iguanas = input!().to_i64()?

    { birds, iguanas }.(Ok)

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:44):

I like this! It makes the Ok sort of an afterthought (a parenthetical afterthought, in fact!) - which I think is appropriate given how this function is written

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:45):

I never used to do |> Ok like this because it introduced more symbols and made it longer, but the property of .( that it has the same number of symbols as a direct call means I'm more open to something like this

view this post on Zulip Richard Feldman (Jan 12 2025 at 13:45):

notably, .pass_to( doesn't have that property

view this post on Zulip Richard Feldman (Jan 12 2025 at 16:06):

random thought we haven't discussed: since passing exactly 1 arg seems to be the most common case, and all the designs for passing multiple args seem awkward in different ways, one possible design would be to only have syntax for 1 arg, and then if you want multiples you use a lambda:

str.to_i64()
.(|result| foo(result, arg2))

view this post on Zulip Richard Feldman (Jan 12 2025 at 16:08):

as opposed to:

str.to_i64()
.(foo)(arg2)

view this post on Zulip Richard Feldman (Jan 12 2025 at 16:08):

or

str.to_i64()
.(foo, arg2)

view this post on Zulip Richard Feldman (Jan 12 2025 at 16:11):

it's more verbose but doesn't require separate syntax

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 17:34):

I found the afterthought tag most useful with nested wrapping. Happens pretty often with things like walkUntil, but also comes up in other locations.

val
    .transform()
    .(Ok)
    .(Step)

view this post on Zulip Notification Bot (Jan 13 2025 at 04:26):

126 messages were moved from this topic to #ideas > static dispatch - partial application syntax by Richard Feldman.

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:25):

Richard Feldman said:

random thought we haven't discussed: since passing exactly 1 arg seems to be the most common case, and all the designs for passing multiple args seem awkward in different ways, one possible design would be to only have syntax for 1 arg, and then if you want multiples you use a lambda:

str.to_i64()
.(|result| foo(result, arg2))

related to this, I am legit struggling to come up with a non-contrived example of when this comes up in the static dispatch world :sweat_smile:

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:25):

can anyone think of one?

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:26):

that is, where we're calling a function that takes 2+ arguments and it would make more sense to use .( than a method call

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:28):

arg reordering

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:28):

:thinking: for example?

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:31):

Oh, re-reading this, I guess you're asking for an example of when you'd need to calculate a function?

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:31):

yeah like basically what's an example that could plausibly come up in real-world Roc code where this syntax would come up

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:31):

for using .( with anything other than a function that takes exactly 1 argument

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:31):

I need to clarify the syntax first

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:32):

another way to put it: what's a situation where today we'd do arg1 |> fn arg2 (or even more args than 2)

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:32):

and we wouldn't just use method-style calling syntax in the static dispatch world

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:32):

Oh

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:33):

data =
    Http.get!("api.com")
    .(decode_with)(Json.utf8)

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:33):

Is this not what you're talking about?

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:33):

It's all blending together at this point

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:34):

Not sure if I can think of an example of what you're looking for

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:36):

Http.get! accept the decoding format as its second argument :thinking:

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:36):

our api does, yes

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:36):

actually that works as an example if you're building up the URL

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:36):

foo
.bar()
.to_url()
.(Http.get!)(Json.utf8)

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:37):

Ideally, all types would have the right methods. pass_to is the stopgap for holes in APIs in my eyes. AKA imagine if HTTP didn't have the right type

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:37):

and if we didn't have separate syntax for it, this would be:

foo
.bar()
.to_url()
.(|url| Http.get!(url, Json.utf8))

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:39):

I think the argument for that being the best design is that if this comes up super rarely, both arg1.(fn, arg2, arg3) and arg1.(fn)(arg2, arg3) are more surprising to read than arg1.(|arg2, arg3| fn(arg2, arg3))

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:39):

because if you're used to seeing the 1-arg form on a regular basis (e.g. I think .(Ok) will come up often)

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:39):

then you read that syntax and immediately understand it

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:40):

it would be annoying if it came up all the time, but if it comes up very rarely, then maybe it's best to optimize for the syntax not being surprising as opposed to being more concise

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:42):

Like Ayaz, my worry is that this will be used often at all. I can't think of an example that is improved by this instead of just having smaller pipelines and multiple assignments. It's good to try stuff, but seeing foo.(|arg| bar(arg, 123)) makes me think a Roc with any usage of this will always be worse than a Roc without it at all

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:42):

I guess a variation of that idea could be that .pass_to exists, since I don't think arg1.pass_to(fn, arg2, arg3) is surprising, but then we have arg1.(fn) as a shorthand for arg1.pass_to(fn) because it comes up so often

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:45):

I actually think that I'd be close to wanting it even if it was just for 4.(hours) and .(Ok) on their own :big_smile:

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:46):

when doing the realworld app I independently made up two other syntaxes, namely .Ok() and 4h, just because I wanted to be able to do something along those lines in those places

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:46):

obviously lots of languages get away with not having something like this, so it wouldn't be the end of the world if we didn't have it

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:46):

The single arg version makes sense for the reasons you say

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:47):

Anything more complex is the problem

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:47):

yeah

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:47):

so maybe we just try it with single-arg being the only thing that's allowed, and see how it goes

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:47):

That would be great!

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:47):

We already do that for record builders

view this post on Zulip Sam Mohr (Jan 13 2025 at 06:47):

Seems good there so far

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:58):

ha, we could refer to .( as "the empty method"

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:58):

because it's like .foo(bar) with the foo deleted

view this post on Zulip Richard Feldman (Jan 13 2025 at 06:59):

and in this design it's very easy to explain: a.(b) means "pass a to b"

view this post on Zulip Luke Boswell (Jan 13 2025 at 07:18):

Lambdas would still work with this?

view this post on Zulip Luke Boswell (Jan 13 2025 at 07:28):

a.(|b| fn(b))?

view this post on Zulip Sam Mohr (Jan 13 2025 at 07:36):

I'm voting that they shouldn't

view this post on Zulip Kilian Vounckx (Jan 13 2025 at 07:52):

@Sam Mohr I don't see why they shouldn't?
If we have a lint in the lsp or something saying it is unnecessary, I don't see the problem

view this post on Zulip Luke Boswell (Jan 13 2025 at 07:54):

Luke Boswell said:

a.(|b| fn(b))?

Let me expand that, I was thinking a.(|b| fn(b, c, d))?

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:05):

@Kilian Vounckx the goal is to disincentivize Roc devs to write code that is hard to read for others. If I come into a new codebase and see this, especially without knowing Roc, I'll be pretty confused.

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:06):

In general, the longer a chain of methods is, the harder it is to tell what's happening in the middle of it. This feels like a way to allow method chains to get even longer than they already would

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:06):

I wonder if other people have experience with that, or disagree that long method chains are usually an anti-pattern

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:08):

I'm thinking of this chain I read today (source):

    let mut captured_symbols: Vec<_> = new_output
        .references
        .value_lookups()
        .copied()
        // filter out the closure's name itself
        .filter(|s| *s != symbol)
        // symbols bound either in this pattern or deeper down are not captured!
        .filter(|s| !new_output.references.bound_symbols().any(|x| x == s))
        .filter(|s| bound_by_argument_patterns.iter().all(|(k, _)| s != k))
        // filter out top-level symbols those will be globally available, and don't need to be captured
        .filter(|s| {
            let is_top_level = env.top_level_symbols.contains(s);
            references_top_level = references_top_level || is_top_level;
            !is_top_level
        })
        // filter out imported symbols those will be globally available, and don't need to be captured
        .filter(|s| s.module_id() == env.home)
        // filter out functions that don't close over anything
        .filter(|s| !new_output.non_closures.contains(s))
        .filter(|s| !output.non_closures.contains(s))
        .map(|s| (s, var_store.fresh()))
        .collect();

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:09):

This would be really hard to read without comments, since all methods are just .filter(...)

view this post on Zulip Sam Mohr (Jan 13 2025 at 08:10):

If there were intermittent variables, this wouldn't need all of these comments IMO

view this post on Zulip Kilian Vounckx (Jan 13 2025 at 08:13):

I think I would write this with a single filter using a longer closure. Using var names within the closure. But I would still need that syntax even for a single filter

view this post on Zulip Anthony Bullard (Jan 13 2025 at 11:23):

Yeah, that filter chain should be replaced with a single filter. I could not trust even Rust's compiler to inline all those functions even if an Iter is a "zero cost" abstraction.

view this post on Zulip Dawid Danieluk (Jan 13 2025 at 16:53):

I think this readability issue is caused by explicit types, because cost of extracting filters into separate functions is higher as you need to annotate every single function.
With implicit types and pseudo-roc syntax it could have been

captured_symbols = new_output
        .value_lookups()
        .discard_if(|s| s == symbol)
        .discard_if(.(boundHereOrDeeper)(new_output, bound_by_argument_patterns))
        .discard_if(.(isImportedOrTopLevelSymbol)(env))
        .discard_if(|s| new_output.non_closures.contains(s))
        .discard_if(|s| output.non_closures.contains(s))
        .map(|s| (s, var_store.fresh()));

.....
boundHereOrDeeper = .....
isImportedOrTopLevelSymbol = .....

I might have gotten some syntax wrong here, but You get my point. I think that having ability to implicitly deduce types encourages small descriptive functions (instead of inline code that requires comments).
Personally that's how I'd code it, if something is not obvious at first look then extract it into separate 'private' function with descriptive name.
I've used discard_if here as I think some examples in Roc used keep_if, I'm not sure if there's supposed to be counterpart, but that makes it cleaner as I don't need to revert all booleans in filters.

Even if the code you gave wasn't using many separate filters it would still be more readable to me if some parts would be extracted into separate functions so I don't think that it's chaining that's the issue here, but I guess that comes down to preference.

view this post on Zulip Richard Feldman (Jan 13 2025 at 18:12):

Luke Boswell said:

Lambdas would still work with this?
a.(|b| fn(b))?

It would be super weird to be like "you put an expression in here...as long as it's not a lambda! That one type of expression is banned in only this place."

Everywhere else in the language, anywhere you can put an expression, you can put a lambda, and I don't think we should change that.

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:07):

Personally the full syntax is much more interesting to me than only the single arg version

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:08):

I regular hit code in other languages where I am wasting time with nesting and parens that would be resolved by this syntax

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:09):

Bad code (like infinite filter chains) can be written in any language or syntax

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:09):

I don't think that is a good example of where the x.(fn)(y, z) fails

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:10):

It literally isn't using the sytac at all. It is using standard method dispatch

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:12):

Just standard method syntax also makes sense to me. I think I'm mostly worried by lambdas

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:12):

I have written plenty of transformation pipelines on lists that would not work with static dispatch, but currently work with |> and would work with .(fn). I guess we could keep static dispatch and |> but that feels less nice than just committing to the new syntax.

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:12):

x.(fn)(y, z) is the exact same thing as a method call, but with a local/imported function

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:13):

If we allow it so long as fn is just an (optionally Module-qualified) variable, it makes sense to me (e.g. exactly what we do for record builders)

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:13):

Once you do x.(|n| fn(foo, n))(y, z)...

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:15):

I always hate when I have to do that in pipelines. It is the ugliest, but I sometimes have to do it today

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:15):

We shouldn't allow it IMO

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 23:15):

But I think blocking the syntax for a single use that is the worst use and is probably pretty unlikely to be used is a mistake

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:16):

I think we can allow the syntax, but only for named functions

view this post on Zulip Sam Mohr (Jan 13 2025 at 23:16):

It seems like a pretty safe compromise

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:11):

I very strongly think that lambdas should be allowed in there

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:11):

I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:12):

that said, I appreciate the pushback about it (not) being a reasonable way to address the situation where you want to apply multiple arguments

view this post on Zulip Sam Mohr (Jan 14 2025 at 00:13):

I guess I'll have to hope that anyone that puts a lambda in there thinks it looks as awkward as I think it does, and that pressure makes this usage of the syntax rare

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:13):

to be fair, lambdas in pipelines are already rare today

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:15):

edit: ahh I tried to fix a typo on mobile and hit delete instead of edit, and there's no confirmation or undo button 🤦🏼‍♂️

but thankfully, Sam rewrote it below! :point_down:

view this post on Zulip Sam Mohr (Jan 14 2025 at 00:16):

Option 1 looks closest to a function call

view this post on Zulip Anthony Bullard (Jan 14 2025 at 00:16):

Option 1

view this post on Zulip Sam Mohr (Jan 14 2025 at 00:27):

Now that partial application is off the table and .> looks bad enough to enough people to not be an option, these are the remaining options for this method-style local function call syntax:

  1. foo.get_bar().(local_func)(arg1, arg2)
  2. foo.get_bar().(local_func, arg1, arg2)
  3. foo.get_bar().pass_to(local_func, arg1, arg2) (and maybe we allow .(...) as well as a shorthand?)

view this post on Zulip Richard Feldman (Jan 14 2025 at 00:29):

are there any other options we haven't talked about that could be worth considering?

view this post on Zulip Brendan Hansknecht (Jan 14 2025 at 00:29):

Sam Mohr said:

I guess I'll have to hope that anyone that puts a lambda in there thinks it looks as awkward as I think it does, and that pressure makes this usage of the syntax rare

At a minimum if people use lambdas, I would generally expect them to be single arg lambdas. The rest would be able to be captures of the function directly. So no need for other args. I expect using lambdas and adding extra arg to be exceptionally rare. Cause you can always be less verbose by using captures instead.

view this post on Zulip Sam Mohr (Jan 14 2025 at 00:41):

  1. foo.get_bar()..local_func(arg1, arg2)

Probably a non starter since it doesn't take lambdas, and also conflicts with list spreads, but it's been mentioned before

view this post on Zulip Anthony Bullard (Jan 14 2025 at 01:01):

That's interesting. The only language I know with that syntax is Dart - which uses it for a very different purpose.

view this post on Zulip Anthony Bullard (Jan 14 2025 at 01:05):

Which is "call a void method on an instance, but return the instance"

view this post on Zulip Sam Mohr (Jan 14 2025 at 01:33):

Not really a thing in FP, yeah

view this post on Zulip Oskar Hahn (Jan 14 2025 at 07:34):

I think long chains of functions are a good fit for Roc, because it fits with ref counting. In the example from @Sam Mohr I can see without reading the code, that the argument, that gets passed around, has an ref count of 1. (maybe there is a copy on the first method call, but not on any other).

If the code would use more intermittent variables, I would need to read the code very carefully to see, if they are used more then once.

Since Roc makes the bet, that most of the time, variables have a refcount of one, the syntax should make it clear, when this bet is off.

view this post on Zulip Oskar Hahn (Jan 14 2025 at 07:37):

I think, I am wrong:

["long_string1", "long_string2]
.map(|e| (e, e))

both strings will get a ref count of 2.

view this post on Zulip Jafar Husain (Jan 14 2025 at 10:33):

Instead of providing an alternate syntax for invoking methods of a module and methods intended to operate on a module, why not offer the ability to specify methods should be invoked using member syntax at defn time (sometimes called “extension methods"). This creates a pit of success by leading developers to use chaining for when it is most appropriate: a series of parameterized transformations on a single subject. However as soon as an operation involves multiple subjects of equal importance, the developer is forced to break out of the chain and pass them as arguments. 

# imagine map/zip was not already defined in List module.
map = |self, fn| …
zip = |left, right| …

# use of chaining where appropriate
let x = [1,2,3].map(|elem| elem + 1)
let y = [4,5,6].map(|elem| elem - 1)

# zip does not favor one list or another, and so we don't try to chain.
let z = zip(x, y, |left, right| left + right)

Extension methods would not have access to hidden members of opaque types. They could also be exported, making it easy to provide methods for modules which were not considered by the owner of the module (ex. think itertools). For prior art, you can find use of extension methods in both C# and Kotlin. IMO they have gently encouraged a more functional style while allowing code to appear OO.

view this post on Zulip Jafar Husain (Jan 14 2025 at 10:36):

BTW love the proposed "..." syntax. I was just abbreviating before I realized that the example might actually be valid Roc code in the future :laughing:

view this post on Zulip Sam Mohr (Jan 14 2025 at 10:49):

My worry with extension methods is that they are syntactically identical to functions at the call site. The benefit of val.(local_func)(arg1, arg2) is that it's distinct from a method call without knowing even its type signature

view this post on Zulip Sam Mohr (Jan 14 2025 at 10:50):

If we could find a way to make extension methods syntactically distinct at the call site, then they could be an option here.

view this post on Zulip Sam Mohr (Jan 14 2025 at 10:50):

Though I can definitely see a Roc where now everything is desired to be written as either an extension method or custom type method, except for maybe constructors.

view this post on Zulip Sam Mohr (Jan 14 2025 at 10:52):

Meaning, y'know, if we can make normal functions Just Work like extension methods via val.(func)(...), what's the need for extensions methods?

view this post on Zulip Brendan Hansknecht (Jan 14 2025 at 16:00):

Oskar Hahn said:

I think long chains of functions are a good fit for Roc, because it fits with ref counting. In the example from Sam Mohr I can see without reading the code, that the argument, that gets passed around, has an ref count of 1. (maybe there is a copy on the first method call, but not on any other).

While not guaranteed, I do think this will be true most of the time

view this post on Zulip Joshua Warner (Jan 14 2025 at 18:02):

One thing I like about val.(func)(args) is that auto-complete could automatically populate (func) after you type ., as long as it's a function in scope that has the right first arg type - and that auto-complete works the same way as it would for a static-dispatch function. That's not really the case for .(func(args)) or .pass_to(func, args) since in both of those cases the auto-complete should also be inserting the close-paren after your cursor

view this post on Zulip Sky Rose (Jan 15 2025 at 00:54):

Sam Mohr said:

  1. foo.get_bar()..local_func(arg1, arg2)

Probably a non starter since it doesn't take lambdas, and also conflicts with list spreads, but it's been mentioned before

Maybe it could take lambdas if you wrap it in parens (as normal order of operation parens, not any special function syntax):

foo.get_bar()..local_func(second_arg)
foo.get_bar()..(|result, second_arg| expression)(second_arg)
foo.get_bar()..(higher_ordered_function(f))(second_arg)

In cases where you do need parens, it's only marginally clunkier than the leading option .(arbitrary function expression)(), and it doesn't require any new special case uses of parens, and it make the common case ..local_func() much simpler.

view this post on Zulip Sky Rose (Jan 15 2025 at 00:56):

Are there cases where it's ambiguous with list spreading? Wouldn't that always have a space or comma before the .., like [a, ..b]?

view this post on Zulip Sam Mohr (Jan 15 2025 at 00:56):

I think it's more subtle than foo.(func)(args), but foo..func(args) has fewer parens, and I think anything we can do to avoid our Giles Corey crushing to death by parens is a good thing

view this post on Zulip Sam Mohr (Jan 15 2025 at 00:57):

Yeah, it'd be fine for list spreads, I was thinking of ranges, a la 0..10 which would make an iterator over integers

view this post on Zulip Sam Mohr (Jan 15 2025 at 00:57):

And it looks better than foo.>func(args)

view this post on Zulip Brendan Hansknecht (Jan 15 2025 at 02:01):

and I think anything we can do to avoid our Giles Corey crushing to death by parens is a good thing

Maybe... It might be a good thing that it is parens heavy. It really shouldn't be used everywhere, but it is nice every once and a while. So having some natural avoidance due to parens aesthetic may be a good thing.

view this post on Zulip jan kili (Jan 15 2025 at 03:57):

@Brendan Hansknecht Why shouldn't it be used everywhere? I currently do a lot of |> foo |> bar chains.

view this post on Zulip Brendan Hansknecht (Jan 15 2025 at 04:53):

I'm actually pretty ok with it being used everywhere. I think it is preferable to anchor to static dispatch though and use this more sparingly

view this post on Zulip Brendan Hansknecht (Jan 15 2025 at 04:54):

That said, I also add pipes all over the place, so maybe this would be everywhere

view this post on Zulip jan kili (Jan 15 2025 at 14:49):

Richard Feldman said:

are there any other options we haven't talked about that could be worth considering?

Did we rule out .$ at some point after here?

  1. foo.get_bar().$local_func(arg1, arg2)

view this post on Zulip jan kili (Jan 15 2025 at 14:53):

(I tried to link to my message that says "I believe with $s it would look like dot-dollar-with-nonrepeated-qualifiers" with the :money_face:s under it)

view this post on Zulip jan kili (Jan 15 2025 at 14:58):

I slightly prefer this over Option 1, as it's consistent with our use of $ as an expression injector for strings. We could even allow expressions/lambdas with added parens when it's more than an existing def:

foo.get_bar().$(make a func)(arg1, arg2)

view this post on Zulip jan kili (Jan 15 2025 at 15:03):

Sorry, not injector - interpolator. pass_to does feel like "method interpolation".

view this post on Zulip Kiryl Dziamura (Feb 12 2025 at 23:23):

Should only one argument in passed lambda be allowed? So the other potential args have to come from the scope straight to the lambda body. To reduce indirection. In other words, should it be allowed to have another pair of parens immediately after the passed lambda?

a.(|x, y| y.div(x))(b)

vs

a.(|x| b.div(x))

So if you want to be abstract - go implement a local function, otherwise only straightforward lambdas are allowed. Although lambda syntax for the special case feels a bit redundant with such a constraint

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:24):

Richard Feldman said:

I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:

I suggested similar earlier, but we thought it was a premature restriction

view this post on Zulip jan kili (Feb 12 2025 at 23:32):

JanCVanB said:

Richard Feldman said:

are there any other options we haven't talked about that could be worth considering?

Did we rule out .$ at some point after here?

  1. foo.get_bar().$local_func(arg1, arg2)

Bump for my re-proposal of "method interpolation" syntax

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:33):

I'm a fan because less parens

view this post on Zulip Richard Feldman (Feb 12 2025 at 23:36):

maybe it's my Perl experience with identifiers that start with $ but when I look at that I see a method call to something named $local_func :sweat_smile:

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:48):

I've basically never used Perl, I take it as a blessing

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 00:50):

foo.(local_fn)

Is one (:wait_one_second:) character less than this

foo.$local_fn()

:grinning_face_with_smiling_eyes:

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 00:52):

But
foo.(local_fn)(bar)
is 1 more character than
foo.$local_fn(bar)

So it may balance out

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 00:52):

That said, I still prefer foo.(local_fn)(bar)

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 00:52):

That said, I feel like I really need to type out a project with both syntax to see how it feels in practice

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 01:04):

I like to think about .() as of basic syntax for piping. So you can expand the brackets in foo.(Foo.method)(bar) and get foo.method(bar).

view this post on Zulip Sky Rose (Feb 13 2025 at 04:43):

If we're talking about .$, I'll keep plugging my favorite alternative ... foo..local_fn() is also one more character than foo.(local_fn), but I think it will be even easier to type and to read. It also still supports foo..(Module.fn)() or other complex expressions if needed.

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:00):

hm, so instead of this:

time = 4.(hours).(ago!)

...it would be this?

time = 4..hours()..ago!()

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:01):

and then instead of:

foo
.bar()
.to_url()
.(Http.get!)(Json.utf8)

...it would be:

foo
.bar()
.to_url()
..Http.get!(Json.utf8)

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:02):

I'm trying to think of a tie-in between this use of .. and the use of it in records and tag unions

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:02):

something like it means "merge these" maybe?

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:02):

or like "combine these" or "concatenate these" or something

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:04):

in this example, I want to call it the "dramatic pause" operator :big_smile:

time = 4..hours()..ago!()

view this post on Zulip jan kili (Feb 13 2025 at 05:05):

Just for side-by-side... (not disparaging)

time = 4.$hours().$ago!()
foo
.bar()
.to_url()
.$Http.get!(Json.utf8)

view this post on Zulip jan kili (Feb 13 2025 at 05:08):

main! = |_|
    "./input.txt"
    .(Path.from_str)
    .read_bytes!()?
    .(Foo.from_bytes)?
    .(transform)(2, Much)
    .to_bytes()?
    .(Path.write_bytes!)(Path.from_str("./output.txt"))

    Stdout.line!("🥳 See ./output.txt")
main! = |_|
    "./input.txt"
    ..Path.from_str()
    .read_bytes!()?
    ..Foo.from_bytes()?
    ..transform(2, Much)
    .to_bytes()?
    ..Path.write_bytes!(Path.from_str("./output.txt"))

    Stdout.line!("🥳 See ./output.txt")
main! = |_|
    "./input.txt"
    .$Path.from_str()
    .read_bytes!()?
    .$Foo.from_bytes()?
    .$transform(2, Much)
    .to_bytes()?
    .$Path.write_bytes!(Path.from_str("./output.txt"))

    Stdout.line!("🥳 See ./output.txt")

view this post on Zulip jan kili (Feb 13 2025 at 05:09):

(note: we would color $ however we want) (also I only want editor color themes that fade parentheses to like gray)

view this post on Zulip jan kili (Feb 13 2025 at 05:12):

I really look forward to the days post-syntax-rework when we can just say '''roc and have it look great every time.

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:13):

I think I've come around to preferring ..

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:14):

I very much like that it's obvious how the multi-arg case works

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:15):

and even once you know what the syntax is, both ..foo() and ..foo(bar) look equivalently nice, whereas .(foo) looks way better than .(foo)(bar)

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:15):

(I'm just not a fan of .$ - sorry Jan!)

view this post on Zulip jan kili (Feb 13 2025 at 05:15):

Do we use ... anywhere? Should that be our spread operator instead?

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:16):

we plan to: #ideas > Adding an ellipsis `...` keyword

view this post on Zulip jan kili (Feb 13 2025 at 05:16):

Ohhh right nvm

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:16):

I think there's some symmetry to having them all use .. though

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:16):

I haven't figured out quite how to express it

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:18):

but like, there's some commonality in [a, b, ..c, d] -> and a..b(c, d) in that we're specifying how certain ingredients get combined together to form the final thing we want (a list in the first case and a function call in the second case)

view this post on Zulip jan kili (Feb 13 2025 at 05:18):

I wonder if we should revisit consensus building on #ideas > static dispatch - dispatch on return types because it could influence topics like this (unless we achieved consensus - I'll re-read it) :check: thanks Richard for summarizing it!

view this post on Zulip jan kili (Feb 13 2025 at 05:19):

Richard Feldman said:

I haven't figured out quite how to express it

"stitching"?

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:22):

something like that

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:23):

I guess really it's that although the uses aren't quite as similar as they are in (for example) lists, records, and tag unions, there's more similarity than I had originally noticed

view this post on Zulip Sam Mohr (Feb 13 2025 at 05:31):

Extraction

view this post on Zulip Sam Mohr (Feb 13 2025 at 05:31):

Nah

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:40):

hmm. I much prefer .(Path.write_bytes) to ..Path.write_bytes. I think it feels much more semantically correct

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:43):

I think that .(Path.write_bytes) makes much more sense because Path.write_bytes is an expression.

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:45):

Also I wait for the bugfix: (probably would happen with a common function name like map or read)

main! = |_|
    "./input.txt"
    ..Path.from_str()
    .read_bytes!()?
    ..Foo.from_bytes()?
-   ..transform(2, Much)
+   .transform(2, Much)
    .to_bytes()?
    ..Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:48):

Brendan Hansknecht said:

I think that .(Path.write_bytes) makes much more sense because Path.write_bytes is an expression.

I can appreciate that, but at the same time (as we've discussed previously) it's also inconsistent that .(foo)(bar) works, but if I take off the (bar) I don't get back a function that accepts bar

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:49):

I don't think so

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:49):

It is the same as static dispatch

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:49):

Or I guess that means I agree, but it exists in the language anyway

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:50):

hm I don't think so

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:50):

well, more concretely

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:50):

x.foo(bar) works, but if I take off the (bar) I don't get back a function

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:51):

x.(foo)(bar) works, but if I take off the (bar) I don't get back a function

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:51):

if I have ______(bar) and _____ is a valid expression on its own if I remove (bar), then everywhere else in the language, we can infer that the expression ____ on its own will be a function that accepts bar

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:51):

ah, I see

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:51):

yeah I guess x.foo is a valid expression on its own :thumbs_up:

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:51):

that's a fair point

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:52):

that said, it's definitely a problem that if you learn .(foo) you are very unlikely to realize that .(foo)(bar) is the way to add an argument :sweat_smile:

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:53):

Yeah, and it may be valid too, which is scary actually

X := {
    foo : Str
}

foo : X -> I64
foo = |x: X| ...

# usage
x.foo   # (valid, is a Str)
x.foo() # (valid, is an I64)

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:54):

I think the only variations we discussed earlier were:

x = arg1.(fn)(arg2, arg3)
x = arg1.(fn, arg2, arg3)
x = arg1.(fn(arg2, arg3))

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:54):

they all have problems

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:54):

it's definitely a problem that if you learn .(foo) you are very unlikely to realize that .(foo)(bar) is the way to add an argument

I think it is easier to just teach in reverse

x.(foo)(bar) then x.(foo)() then shorthand x.(foo)

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:55):

yeah, or even x.(foo)(bar, baz) because if you only know x.(foo)(bar) you might guess the way to add another argument is to follow that pattern and go x.(foo)(bar)(baz) :sweat_smile:

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 05:56):

sure, but I don't think it would specifically be hard to learn

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:56):

it's definitely a viable option, but it also feels unsatisfying to settle on a design that doesn't feel like it quite fits together right

view this post on Zulip Elias Mulhall (Feb 13 2025 at 05:59):

Forgive me, but why can't the syntax be 4.days().ago!() / x.foo(bar) and have the compiler figure out if it's static dispatch or piping?

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:59):

there are a few problems with that

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:00):

so let's say 4.days() is changed to mean:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:01):

now any time a module adds a function, that is a breaking change

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:01):

because it can break someone's build; they were relying on the local fallback, but now it doesn't fall back because a new function was added that uses the name they were relying on having been absent from that module's API

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:01):

worse, it can not break their build

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:02):

and instead silently change the behavior, because the types happen to line up but it does something different

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:02):

and they have no way of ever knowing other than finding out the hard way :sweat_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:02):

so yeah, it's technically feasible but I think it's best if we don't do it :big_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:03):

a variation on that design is to flip it around and say the local scope gets priority

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:04):

there are related problems with that, for example adding something to a particular scope (e.g. the top level) because you think it's only going to affect the couple of places in that scope you're thinking about at the time

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:04):

but then it turns out it either affects other code you didn't realize was also calling that, or else someone adds new code later on and calls a method they're totally used to calling, not realizing it's being silently overridden in this case

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:04):

so it has some similar downsides to monkeypatching

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:05):

convenient in some scenarios, but definitely also a footgun :sweat_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:05):

so I don't think we should do that either

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:07):

Richard Feldman said:

I think the only variations we discussed earlier were:

x = arg1.(fn)(arg2, arg3)
x = arg1.(fn, arg2, arg3)

x = arg1.(fn(arg2, arg3))

I forgot one: the "only support 1 argument, and use a lambda if you want more" design

x = arg1.(|a| fn(a, arg2, arg3))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:08):

(like the others, that one has a pretty glaring downside too)

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:09):

I still don't really see a glaring downside of

arg1.(fn)(arg2, arg3)

It has the same downside a vanilla static dispatch (but less bad due to 100% of the time being invalid if you pass the wrong number of args)

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:14):

well, compared to (for example) arg1..fn(arg2, arg3) the downsides are:

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:17):

ok

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:17):

I guess the only 2-character options we've discussed are .. and .$ and I think maybe .> at some point, but maybe there are other ones that might seem better if we tried them? e.g.

time = 4./hours()./ago!()

main! = |_|
    "./input.txt"
    ./Path.from_str()
    .read_bytes!()?
    ./Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    ./Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:18):

(I don't think that one is better than either .() or ..)

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:19):

for comparison:

time = 4..hours()..ago!()

main! = |_|
    "./input.txt"
    ..Path.from_str()
    .read_bytes!()?
    ..Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    ..Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:19):

and also:

time = 4.(hours).(ago!)

main! = |_|
    "./input.txt"
    .(Path.from_str)
    .read_bytes!()?
    .(Foo.from_bytes)?
    .transform(2, Much)
    .to_bytes()?
    .(Path.write_bytes!)(Path.from_str("./output.txt"))

or the comma version: (only last line is different)

time = 4.(hours).(ago!)

main! = |_|
    "./input.txt"
    .(Path.from_str)
    .read_bytes!()?
    .(Foo.from_bytes)?
    .transform(2, Much)
    .to_bytes()?
    .(Path.write_bytes!, Path.from_str("./output.txt"))

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:22):

Yeah, I think something about not encapsulation (SomeModule.Path.fn) as a single expression really bugs my brain.

I think so far .$ is the best 2 character example for making it still feel distinct.

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:25):

for further comparison:

time = 4.$hours().$ago!()

main! = |_|
    "./input.txt"
    .$Path.from_str()
    .read_bytes!()?
    .$Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    .$Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:26):

I don't like this, but an option we haven't discussed:

time = 4::hours()::ago!()

main! = |_|
    "./input.txt"
    ::Path.from_str()
    .read_bytes!()?
    ::Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    ::Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:27):

I dislike that this is almost backwards from what :: means in other languages :sweat_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:29):

also I'd argue that this is more internally consistent and easier to learn, but I also don't like how it looks:

time = 4.(hours)().(ago!)()

main! = |_|
    "./input.txt"
    .(Path.from_str)()
    .read_bytes!()?
    .(Foo.from_bytes)()?
    .transform(2, Much)
    .to_bytes()?
    .(Path.write_bytes!)(Path.from_str("./output.txt"))

view this post on Zulip Luke Boswell (Feb 13 2025 at 06:32):

I still like .> :sweat_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:33):

oh yeah, I also forgot the one in the title of this thread:

time = 4.pass_to(hours).pass_to(ago!)

main! = |_|
    "./input.txt"
    .pass_to(Path.from_str)
    .read_bytes!()?
    .pass_to(Foo.from_bytes)?
    .transform(2, Much)
    .to_bytes()?
    .pass_to(Path.write_bytes!, Path.from_str("./output.txt"))

very clear what it's doing, but a bit magical because it looks like a normal method-style call but is actually syntax sugar, and the verbosity makes cases like the first line totally unappealing

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:34):

comparing with .>:

time = 4.>hours().>ago!()

main! = |_|
    "./input.txt"
    .>Path.from_str()
    .read_bytes!()?
    .>Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    .>Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:34):

I think I just don't like how . followed by > looks haha

view this post on Zulip jan kili (Feb 13 2025 at 06:34):

If |> is fully removed (likely), .>'s primary connotation might be a terminal prompt, which is also apt.

view this post on Zulip Luke Boswell (Feb 13 2025 at 06:36):

I like it more than .. because I think the extra dot could be harder to see, and its clearer its coming from the local scope and not associated with the previous thing. Its like we use the dot to say hey continue, then the arrow is like a pipe into the next thing.

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:39):

:: is surprisingly jarring to look at...too significant in my opinion

view this post on Zulip Richard Feldman (Feb 13 2025 at 06:42):

full string interpolation metaphor mode :stuck_out_tongue:

time = 4.${hours}().${ago!}()

main! = |_|
    "./input.txt"
    .${Path.from_str}()
    .read_bytes!()?
    .${Foo.from_bytes}()?
    .transform(2, Much)
    .to_bytes()?
    .${Path.write_bytes!}(Path.from_str("./output.txt"))

view this post on Zulip jan kili (Feb 13 2025 at 06:44):

our old friend is unemployed now :stuck_out_tongue_closed_eyes:

time = 4.\hours().\ago!()

main! = |_|
    "./input.txt"
    .\Path.from_str()
    .read_bytes!()?
    .\Foo.from_bytes()?
    .\transform(2, Much)
    .to_bytes()?
    .\Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip jan kili (Feb 13 2025 at 06:45):

btw @Richard Feldman I intended transform as the one local function

view this post on Zulip jan kili (Feb 13 2025 at 06:48):

arguably .\ is the closest you can approximate the lambda symbol... (I'm surprised that I'm only 90% 80% 70% joking lol)

view this post on Zulip jan kili (Feb 13 2025 at 07:02):

Just for keyboard thoroughness, I think the remaining free real estate is

view this post on Zulip Alex K (Feb 13 2025 at 07:44):

Richard Feldman said:

I think I just don't like how . followed by > looks haha

Has :> been considered? If the vertical alignment matches, > could look more balanced with : than ..

view this post on Zulip Sam Mohr (Feb 13 2025 at 08:32):

We're trying to consider options that start with . so you can just type a period and get autocomplete

view this post on Zulip Sam Mohr (Feb 13 2025 at 08:32):

Otherwise, you'd need a different character for local functions

view this post on Zulip Alex K (Feb 13 2025 at 09:02):

Ok. From the :: version, I thought it needn't start with a period.

view this post on Zulip Sam Mohr (Feb 13 2025 at 09:03):

That's part of why we don't like it

view this post on Zulip Sam Mohr (Feb 13 2025 at 09:03):

It doesn't have to, but it'd have to be pretty good to overcome that benefit

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 11:12):

someone said "turbofish"? :grinning_face_with_smiling_eyes:

view this post on Zulip Elias Mulhall (Feb 13 2025 at 12:58):

I suspect this one is a no, but Reason uses ->
https://reasonml.github.io/docs/en/pipe-first

4->days->ago!

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:01):

time = 4->hours()->ago!()

main! = |_|
    "./input.txt"
    ->Path.from_str()
    .read_bytes!()?
    ->Foo.from_bytes()?
    ->transform(2, Much)
    .to_bytes()?
    ->Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:02):

huh, I expected to find that confusing based on past experience with C having that mean something else, but it actually looks fine to me? :thinking:

view this post on Zulip Eli Dowling (Feb 13 2025 at 13:04):

I really like this one! Big fan.
I use it all the time in php, it's easy to type. It's clear

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:04):

and actually maybe that's a feature and not a bug, because it makes me expect the same precedence as . (which addresses the problem |> would have)

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:04):

plus it looks normal without spaces around it

view this post on Zulip Elias Mulhall (Feb 13 2025 at 13:06):

My concern was that -> is already used in type signature and we just went through a lot of effort to remove it from lambdas

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:06):

it does have the downside of not starting with .

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:07):

yeah, it definitely has the downside of being function-related and yet not distinguishing between -> and => like function types do

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:08):

but maybe that's okay because it would literally always be ->my_fn! so the ! immediately after it would mean there's no ambiguity as to whether you're calling an effectful function

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:08):

(unlike when defining an anonymous function)

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:09):

does anyone know how it has worked out for Reason?

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:10):

always nice to draw on experience when other languages have directly tried something!

view this post on Zulip Elias Mulhall (Feb 13 2025 at 13:11):

That's kind of a complicated question :sweat_smile:

In part because from my perspective Reason had a very small push in popularity at some point, but has generally stayed pretty obscure.

view this post on Zulip Elias Mulhall (Feb 13 2025 at 13:14):

In this particular case Reason has |> as the pipe last operator, which is used for OCaml code. The pipe first operator -> is mostly meant for interop with JavaScript code, where the data structure is frequently the first arg.

view this post on Zulip Eli Dowling (Feb 13 2025 at 13:18):

Worth noting the reason stdlib is also pipe first so -> is the "main one".

view this post on Zulip Elias Mulhall (Feb 13 2025 at 13:23):

Yeah reading some more I think I got that a bit backwards. I remember when I was trying reason out pipe first was new and having both was pretty confusing. Not really an issue for roc though.

I think they were trying to go for something that looked like calling a method, but didn't conflict with record fields. I remember finding is pretty ok in practice.

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:24):

very interesting!

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:26):

I like that it works naturally with multiple args, and that there's precedent for the syntax at least (even if it has different semantics in mainstream languages) so there's a strangeness cost advantage in terms of how the code looks visually

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:27):

it stands out a lot more in a pipeline of calls than .. does, so the concern of mistakenly having an extra dot goes away

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:27):

it has a lot going for it!

view this post on Zulip Eli Dowling (Feb 13 2025 at 13:30):

I think it is also a big win for "grokability".
Being able to say to a beginner "it's an arrow because it puts this thing into that thing", is really helpful. A lot of the overhead of learning a language is remembering what things do and having simple associations helps a lot IMO

view this post on Zulip Richard Feldman (Feb 13 2025 at 13:31):

"arrow calls" is a natural way to describe them

view this post on Zulip Eli Dowling (Feb 13 2025 at 13:32):

Plus it's immediately recognisable to existing FP users.
It looks close enough to |> that a curious Ocaml or Haskel user glancing at roc code could definitely Intuit what's going on.

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 13:36):

foo->(|x| bar / x).baz()

Looks kinda good even with a simple lambda

view this post on Zulip Elias Mulhall (Feb 13 2025 at 14:12):

The reason the arrow looks good is -- and please understand this is a very technical explanation -- the reason is that there's a pointy middly bit on both sides

view this post on Zulip Jasper Woudenberg (Feb 13 2025 at 14:15):

I remember in the early static dispatch discussions, having a single symbol for triggering autocomplete, and autocomplete potentially offering both "methods" and regular functions were pretty important for folks. Is that still the case? I do really like the idea of seeing all "fitting" functions in a single auto-complete window.

view this post on Zulip Richard Feldman (Feb 13 2025 at 14:18):

yeah I think that's a significant downside of ->

view this post on Zulip Richard Feldman (Feb 13 2025 at 14:18):

but it might be the least bad downside :sweat_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 14:19):

among the options

view this post on Zulip Niclas Ahden (Feb 13 2025 at 14:43):

"only allow one arg and use a lambda for more" is surprisingly nice. The . aligns perfectly, is consistent, and easy to teach/understand. With pizza gone: if I want to pass a value to a function I use ., regardless of what function I want to pass it to (from the type's module, my module, another type's module, or just a lambda).

"Does the language have a pipe operator?"
"yup, it's .!"

or

"ew, is Roc OOP? I see dots everywhere!"
"No, young padawan, . is pipe and life is good!"

Sorry, I'm half-drunk on coffee :sweat_smile:

time = 4.(hours).(ago!)

main! = |_|
    "./input.txt"
    .(Path.from_str)
    .read_bytes!()?
    .(Foo.from_bytes)?
    .transform(2, Much)
    .to_bytes()?
    .(|bytes| Path.write_bytes!(bytes, Path.from_str("./output.txt"))

The case where you need to pass multiple arguments is of course less ergonomic, but it is extremely clear. There's also no difference if what you're passing in isn't the first arg -- you'll use a lambda either way.

view this post on Zulip Niclas Ahden (Feb 13 2025 at 14:56):

"Doesn't Roc have pizza? I thought it was a descendant of Elm!"
"Yes, it does, and it's easier to type, half the size, and even more powerful:.!"

Though I guess it kinds of puts a lot on .. All kinds of access as well as piping... Could be nice, because everything's . and easy, or perhaps it's overloaded and bloated :shrug:

view this post on Zulip Artur Domurad (Feb 13 2025 at 14:59):

There is also .|:

time = 4.|hours().|ago!()

main! = |_|
    "./input.txt"
    .|Path.from_str()
    .read_bytes!()?
    .|Foo.from_bytes()?
    .|transform(2, Much)
    .to_bytes()?
    .|Path.write_bytes!(Path.from_str("./output.txt"))

And the | is already connected to functions in roc, and to piping in bash.
But I think it might be confusing.

view this post on Zulip Richard Feldman (Feb 13 2025 at 15:59):

side by side:

time = 4.|hours().|ago!()

main! = |_|
    "./input.txt"
    .|Path.from_str()
    .read_bytes!()?
    .|Foo.from_bytes()?
    .|transform(2, Much)
    .to_bytes()?
    .|Path.write_bytes!(Path.from_str("./output.txt"))

...compared to arrow:

time = 4->hours()->ago!()

main! = |_|
    "./input.txt"
    ->Path.from_str()
    .read_bytes!()?
    ->Foo.from_bytes()?
    ->transform(2, Much)
    .to_bytes()?
    ->Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 16:00):

visually I prefer ->

view this post on Zulip Richard Feldman (Feb 13 2025 at 16:00):

although I do like that .| starts with .

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 16:09):

I like how -> suggests flow, but .| looks like a dam to me. I think the main difference with |> is that the latter looks like a triangle, so the direction of the flow is clearer. But that's subjective understanding of aesthetics obviously

view this post on Zulip Elias Mulhall (Feb 13 2025 at 16:13):

.| kinda looks like the start of a lambda in an interesting way, but otherwise I don't love it esthetically

view this post on Zulip Dawid Danieluk (Feb 13 2025 at 16:39):

I've mentioned this one in the past, but TS functional library - lodash uses thru for that purpose.
Personally I'm used to it, it's shorter than pass_to.

time = 4.>hours().>ago!()

main! = |_|
    "./input.txt"
    .thru(Path.from_str)
    .read_bytes!()?
    .thru(Foo.from_bytes)?
    .thru(|x| transform(x, 2, Much))
    .to_bytes()?
    .thru(|str| Path.write_bytes!(str, Path.from_str("./output.txt"))

I think that's roughly how that would look like using roc/lodash syntax.
I don't love new symbol combinations that you'd have to know what they mean first (.. in my mind is spread/range and other symbol combinations also look weird although I could get used to them. Surprisingly the one that hurts my eyes the least is ->, probably because dash is centered in the middle of the > and other combinations look really disbalanced vertically .$ .| .>).

view this post on Zulip Dawid Danieluk (Feb 13 2025 at 16:58):

That's how I felt about pass_to vs thru.
image.png
I thought that it's just silly abbreviation but I just found out thru was historically used before through so I guess it's not as silly and playful word as I thought.

view this post on Zulip Richard Feldman (Feb 13 2025 at 17:08):

time = 4.thru(hours).thru(ago!)

this has the same downsides of pass_to (too verbose for this use case, looking like a method call but actually being syntax sugar) while not having pass_to's whole selling point of being obviously self-descriptive, so I think we should rule that one out. :big_smile:

view this post on Zulip Dawid Danieluk (Feb 13 2025 at 17:28):

If that's the case then my second most liked option were arrows.

time = 4->hours->ago!

In your example you didn't omit () parens, but I think that's legal then there's a single argument right?
To me it looks great.

view this post on Zulip Jasper Woudenberg (Feb 13 2025 at 17:28):

I think all the options that have been discussed so far look pipeline-y to me, and give some general intuition that a value is being piped through a list of operations.

Beyond that, I don't believe any operator or keyword is going to give the reader a detailed intuitive understanding that the syntax will perform function application passing the pipeline-value as a first argument, and so will all require some learning. Unless the chosen syntax happens to be same as the one used in a language the learner is already familiar with, but I don't think any options currently under consideration meet that criterium.

I guess I'm a bit sceptical that the choice of keyword or operator will have much of an impact on the experience of learning Roc or it's day to day use. And so the auto-complete benefits of any of the options starting with a . feel way more tangible to me than the aesthetic benefits of an option like ->.

view this post on Zulip Richard Feldman (Feb 13 2025 at 17:30):

I cannot overstate how much I absolutely cannot stand looking at .>

view this post on Zulip Richard Feldman (Feb 13 2025 at 17:31):

I really want to like it because of its objective properties but I just have this extremely negative reaction to the way it looks that I can't get over :sweat_smile:

view this post on Zulip Niclas Ahden (Feb 13 2025 at 17:31):

Is the “single arg or use a lambda” reasonable?

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:40):

Thought: when autocomplete isn't an option, arrow is totally fine

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:40):

So can autocomplete replace the dot with an arrow on selection?

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:41):

That would make it pretty clearly a great option

view this post on Zulip jan kili (Feb 13 2025 at 17:42):

I've hoped that autocomplete desires would eliminate ->, but if that's not guaranteed then I wanna say - I don't fully know why but I strongly dislike -> here. (As a |> :pizza: lover, I'm shocked!)

view this post on Zulip Dawid Danieluk (Feb 13 2025 at 17:42):

My only preference here would be to avoid .. as when I'm looking at it I feel like I'm drowning in dots.

time = 4->hours()->ago!()

main! = |_|
    "./input.txt"
    ->Path.from_str()
    .read_bytes!()?
    ->Foo.from_bytes()?
    ->transform(2, Much)
    .to_bytes()?
    ->Path.write_bytes!(Path.from_str("./output.txt"))

vs

time = 4..hours()..ago!()

main! = |_|
    "./input.txt"
    ..Path.from_str()
    .read_bytes!()?
    ..Foo.from_bytes()?
    ..transform(2, Much)
    .to_bytes()?
    ..Path.write_bytes!(Path.from_str("./output.txt"))

I guess other syntaxes have the benefit of clearly separating piping and method chaining.
Even worse if there were some lambdas using ranges (Roc doesn't have ranges yet right?) and ellipsis somewhere

time = 4..hours()..ago!()

read_lines = |range| ...

main! = |_|
    "./input.txt"
    ..Path.from_str()
    ..read_lines(1..10)
    ..transform(2, Much)
    .to_bytes()?
    ..Path.write_bytes!(Path.from_str("./output.txt"))

Super dot heavy code (and there are more dots in destructuring assignment, open records and probably many other places).
I like the idea of autocomplete replacing dot with -> depending on picked option.

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:51):

JanCVanB said:

I've hoped that autocomplete desires would eliminate ->, but if that's not guaranteed then I wanna say - I don't fully know why but I strongly dislike -> here. (As a |> :pizza: lover, I'm shocked!)

Would love to hear this articulated if possible

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:55):

Arrow seems like it has the benefits of .> without the ickyness:

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:57):

Sam Mohr said:

So can autocomplete replace the dot with an arrow on selection?

The answer seems to be that the LSP can do this! https://docs.rs/lsp-types/0.97.0/lsp_types/struct.CompletionItem.html#structfield.text_edit

view this post on Zulip jan kili (Feb 13 2025 at 17:57):

Without articulating my cons, I don't see why it isn't superior to add a space to the right and rotate the - to preserve the existing glory of |> . If the method interpolation syntax doesn't look like a method, why have it?

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:58):

The goal is to make long and readable chains

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:58):

I don't think |> is glorious unless you're used to it, or it's used only by itself

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:59):

Because when the pipes line up :ok: :ok:

view this post on Zulip jan kili (Feb 13 2025 at 17:59):

One big con for me is that I see foo.bar()->baz() as a function that takes a foo.bar() and returns a baz()... or is that intentional?

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:59):

That is a concern if it's not obvious where that's an expression or a type

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:00):

But types always start with name : ...

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:02):

the space is actually the biggest problem imo

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:02):

that's what makes the precedence confusing

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:02):

What if we remove the space?

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:02):

time = 4|>hours()|>ago!()

main! = |_|
    "./input.txt"
    |>Path.from_str()
    .read_bytes!()?
    |>Foo.from_bytes()?
    |>transform(2, Much)
    .to_bytes()?
    |>Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:03):

the problem is, |> in other languages is always formatted with spaces around it

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:03):

so it just looks like a mistake

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:03):

The pipes are much taller than the dots

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:03):

that's a big advantage of -> I guess, it's always formatted without spaces when it's used like this

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:03):

So they look like Goliaths

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:03):

and it always has the same precedence as .

view this post on Zulip jan kili (Feb 13 2025 at 18:04):

With plenty of good .X options, I'd be surprised if we went with anything that doesn't look like a method, let alone to overload an existing symbol like ->.

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:05):

Since autocomplete isn't a problem with the LSP being able to replace the dot with whatever we want

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:05):

We could make > work

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:05):

So long as there's no space

view this post on Zulip jan kili (Feb 13 2025 at 18:05):

But I love exploring weird options! Bump for .\ :grinning_face_with_smiling_eyes:

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:05):

time = 4>hours()>ago!()

main! = |_|
    "./input.txt"
    >Path.from_str()
    .read_bytes!()?
    >Foo.from_bytes()?
    >transform(2, Much)
    .to_bytes()?
    >Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:07):

What's the best option with a single character? I really like how everything lines up here

view this post on Zulip sammi watt (Feb 13 2025 at 18:08):

Lua uses : for its method call syntax. Has that been discussed? Its just, like, another dot on top

view this post on Zulip sammi watt (Feb 13 2025 at 18:08):

looks like this

time = 4:hours():ago!()
#// or 4:hours:ago! ??

main! = |_|
    "./input.txt"
    :Path.from_str()
    .read_bytes!()?
    :Foo.from_bytes()?
    :transform(2, Much)
    .to_bytes()?
    :Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:08):

This syntax highlighter makes them different colors (and therefore visually distinct)

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:09):

If they were the same color, maybe it wouldn't be as good

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:09):

We'd want to preserve that maybe?

view this post on Zulip jan kili (Feb 13 2025 at 18:10):

Other way around, in my opinion - in the same way that I want parens and braces the same color, I want method symbol and method-ish symbol the same color.

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:11):

How important do you think it is to be able to tell which is a method and which is a local function at a glance?

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:12):

JanCVanB said:

With plenty of good .X options

the problem is I wouldn't call any of those options "good" - I've been using words like "viable" and "plausible" because I think we could ship them, but I don't think any of them are actually good :sweat_smile:

view this post on Zulip jan kili (Feb 13 2025 at 18:13):

Sam Mohr said:

What's the best option with a single character? I really like how everything lines up here

In my opinion ~ \ : $ > | (sorted most-to-least favorite, with some ties)

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:15):

> is definitely taken :big_smile:

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:15):

| isn't taken at the expression level though, which somehow I never realized

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:15):

I agree that > is weird looking, but we already have - work differently with or without a space

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:15):

yeah but people write x>y all the time

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:16):

Yep

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:16):

I've seen programmers who never put spaces around their math operators

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:16):

Let's not consider it

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:16):

I've seen them too :frown:

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:16):

time = 4|hours()|ago!()

# or possibly:

time = 4|hours|ago!

main! = |_|
    "./input.txt"
    |Path.from_str()
    .read_bytes!()?
    |Foo.from_bytes()?
    |transform(2, Much)
    .to_bytes()?
    |Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:16):

Morse code?

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:17):

it's definitely interesting, I'm not sure how I feel about it

view this post on Zulip jan kili (Feb 13 2025 at 18:18):

time = 4~hours()~ago!()

main! = |_|
    "./input.txt"
    ~Path.from_str()
    .read_bytes!()?
    ~Foo.from_bytes()?
    ~transform(2, Much)
    .to_bytes()?
    ~Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:18):

it is definitely cool that the meaning lines up so well with the meaning of | in Bash

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:18):

I don't like ~ for this because I always think of that as meaning "approximate"

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:18):

It visually works

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:19):

If only - wasn't taken

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:19):

it's only slightly taken

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:19):

who uses that one, honestly

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:19):

Stop being so negative!

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:20):

| is definitely used with spaces around it in Bash though, and when it is used in programming languages (e.g. for bitwise operations, or in Roc patterns) it has spaces around it too

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:21):

Yep

view this post on Zulip jan kili (Feb 13 2025 at 18:21):

We do want to give off Lua2 (2ua) vibes, so : gets a bump from that.

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:21):

Hawk

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:21):

I'm down for :

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:22):

Two minor downsides

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:22):

Lack of direction, and tricky to distinguish in multiline chains

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:22):

also : already means 2 other things in Roc, namely types and record fields

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:23):

I couldn't think of any situations where this would be ambiguous as long as we require parens

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:23):

but if we allowed omitting parens (e.g. 4:hours:ago!) then { x:y } would become ambiguously a record vs not

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:23):

oh wait, nm it's ambiguous regardless

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:24):

{ x:y() } could be a record with one field, or a block with one expression in it

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:24):

before anyone asks, let's not revisit block syntax for the sake of : :stuck_out_tongue:

view this post on Zulip jan kili (Feb 13 2025 at 18:24):

Well if we like | and we're really in blue sky mode... we did just drop an ASCII character that could stand alone

time = 4\hours\ago!

main! = |_|
    "./input.txt"
    \Path.from_str()
    .read_bytes!()?
    \Foo.from_bytes()?
    \transform(2, Much)
    .to_bytes()?
    \Path.write_bytes!(Path.from_str("./output.txt"))

and we could revive our "hey it's a lambda" justification

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:26):

It's weird but very readable

view this post on Zulip Niclas Ahden (Feb 13 2025 at 18:26):

\ looks like a slope that funnels the arg from the line above into the next function :joy:

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:28):

it's a slide, whee! :slide:

view this post on Zulip jan kili (Feb 13 2025 at 18:29):

#Slidechaining #MusicProductionJoke

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:31):

Daft Punk's favorite operator

view this post on Zulip Niclas Ahden (Feb 13 2025 at 18:31):

I still prefer . single arg+lambda because it’s consistent “whenever I want to pipe, I .”. However, I’m down for the slide :P

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:32):

@Niclas Ahden isn't that concern assuaged by the LSP letting you type a dot and replacing it with something else?

view this post on Zulip Niclas Ahden (Feb 13 2025 at 18:33):

Partly? Isn’t it kind of nice that the .s line up like that though? Looks super clean and easy :) No “so one is a dot, what is that other thing?” yknow?

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:33):

Yeah, I agree

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:34):

But I also want to see when something different is happening, which is the case for everything else in Roc (see purity inference)

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:35):

the top 3 contenders in my mind right now are:

and they all have a bunch of different tradeoffs :sweat_smile:

view this post on Zulip Niclas Ahden (Feb 13 2025 at 18:36):

BAM! :exploding_head:

time = 4🛝hours🛝ago!

main! = |_|
    "./input.txt"
    🛝Path.from_str()
    .read_bytes!()?
    🛝Foo.from_bytes()?
    🛝transform(2, Much)
    .to_bytes()?
    🛝Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:41):

I think if I had to pick one right now I'd pick ->

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:41):

because I think the only downside compared to .. and .( is that it doesn't start with . - whereas those have (in my mind) more serious drawbacks

view this post on Zulip Niclas Ahden (Feb 13 2025 at 18:42):

My top two:

-> looks like PHP :upside_down:
.. looks like a range and doesn't line up, but I guess it's pretty neat too
: may be good? Guess it depends on the highlighting

Time for dinner. Home-made hamburgers here we gooo!

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:42):

enjoy!

view this post on Zulip Niclas Ahden (Feb 13 2025 at 19:49):

Sam Mohr said:

But I also want to see when something different is happening, which is the case for everything else in Roc (see purity inference)

My view (which might be flawed?) is that broadly speaking something different isn't happening. I understand that it's technically different, but when teaching or thinking about it I can go "I'm just piping" and that's true. It's just a matter of what I'm piping to, but whatever the receiver is, I'm just piping. Right?

I do think it goes haywire when it's like .(foo)(baz) because, to me, it's weird that one argument is coming from the left and the other is coming from the right, but both are applied to foo. I know it makes sense technically, but visually it's a bit jarring, and I have to think about it every time I look at it like "ah, yeah, I'm piping something in from the left and that is partially applied, then I'm applying baz to the partially-applied function".

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 20:33):

Richard Feldman said:

I think if I had to pick one right now I'd pick ->

I would put -> and .( as relatively even. I think other options are worse in my view. While I personally lean .(, I think as a community leaning -> probably makes more sense.

view this post on Zulip Niclas Ahden (Feb 13 2025 at 20:48):

time = 4\hours\ago!would we call this the "shovel operator"? Shoveling the 4 into hour etc.

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 22:35):

We do have .accessor as a valid function shorthand on structural records that can be combined with the current pipe |> operator.

If I can do,

getX = .x
{x:2} |> getX

For consistency, I'd expect this to work

{x:2} |> .x

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 22:35):

It is debatable whether it's good style to use piping like this, when you could just { x: 2 }.x, but my main point is that we should consider it. The shorthand should stay, since it's really valuable in higher order functions, like my_list.map(.my_field), but it feels bad to make them a "second class citizen" in piping.

unique_crops = farm
  .harvest()
  ->.crops # Looks great IMO.
  ->Set.from_list()

unique_crops = farm
  .harvest()
  ...crops # definitely a mistake, especially with the (`...` operator)
  ..Set.from_list()

unique_crops = farm
  .harvest()
  .(.crops) # Works, and If I pause on it, I even get what's happening, but I'd rather never see this
  .(Set.from_list())

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 22:39):

Why would you use that at all? Just access the record directly

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 22:40):

x.foo().field1.bar()

view this post on Zulip Luke Boswell (Feb 13 2025 at 22:42):

One concern I have with -> is that it is also used in our types to represent a function.

view this post on Zulip Luke Boswell (Feb 13 2025 at 22:43):

calculate : I64, I64 -> I64
calculate = |a,b|
    a -> calc_help(b)

view this post on Zulip Luke Boswell (Feb 13 2025 at 22:44):

Not a major concern... just wanted to flag it

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 22:45):

Weirdly enough, seeing .fn() and ->fn() together is less taxing to my brain than
.fn() and .(fn)together. I've gotten used to .fn() having a direction, but not .(fn) having one.
->fn() naturally have it.

view this post on Zulip Sky Rose (Feb 13 2025 at 22:46):

\ seems fine when piping into .accessor

unique_crops = farm
  .harvest()
  \.crops
  \Set.from_list()

(Edited: removed : example cuz : was rejected for causing ambiguities)

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 22:46):

Brendan Hansknecht said:

Why would you use that at all? Just access the record directly

Agreed. But a syntax like ..fn() would create an edge case for them, whereas the other options don't

view this post on Zulip Isaac Van Doren (Feb 13 2025 at 23:05):

What about .:?

time = 4.:hours().:ago()!

main! = |_|
    "./input.txt"
    .:Path.from_str()
    .read_bytes!()?
    .:Foo.from_bytes()?
    .:transform(2, Much)
    .to_bytes()?
    .:Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 23:05):

I find ->fn very pleasing to look at, but would also take .(fn) with the restriction that Niclas has tried out, allowing only 1 argument, not .(fn)(b ,c).
I'd rather see occasional stuff.(|s| fn(s,b)) -s, than stuff.(fn)(b) -s, mainly, because (b) feels disconnected from fn

view this post on Zulip Isaac Van Doren (Feb 13 2025 at 23:07):

Of the ones discussed so far .. is by far my top choice. I really dislike the way .(function) looks.

I like that .: allows you to start with a . and it seems to distinguish local function calls slightly better than .. does

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 23:07):

Norbert Hajagos said:

Brendan Hansknecht said:

Why would you use that at all? Just access the record directly

Agreed. But a syntax like ..fn() would create an edge case for them, whereas the other options don't

Sure, but we can just require parens in that case. So not really an edge case. Same as if you put a lambda there. Requires parens

view this post on Zulip Sam Mohr (Feb 13 2025 at 23:08):

+1 to .: being more distinct than :

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 23:11):

Brendan Hansknecht said:

Norbert Hajagos said:

Brendan Hansknecht said:

Why would you use that at all? Just access the record directly

Agreed. But a syntax like ..fn() would create an edge case for them, whereas the other options don't

Sure, but we can just require parens in that case. So not really an edge case. Same as if you put a lambda there. Requires parens

True. It is really a minor concern the more you talk about it.

view this post on Zulip Richard Feldman (Feb 13 2025 at 23:13):

with different syntax highlighting:

time = 4.:hours().:ago()!

main! = |_|
    "./input.txt"
    .:Path.from_str()
    .read_bytes!()?
    .:Foo.from_bytes()?
    .:transform(2, Much)
    .to_bytes()?
    .:Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Sam Mohr (Feb 13 2025 at 23:24):

Because it's all dots, this is a great option visually, and it has basically none of the discussed downsides IMO

view this post on Zulip Oskar Hahn (Feb 13 2025 at 23:29):

Could you explain once more, why they have to be different? Why is it not possible, to use the dot for both?

time = 4.hours().ago()!

main! = |_|
    "./input.txt"
    .Path.from_str()
    .read_bytes!()?
    .Foo.from_bytes()?
    .transform(2, Much)
    .to_bytes()?
    .Path.write_bytes!(Path.from_str("./output.txt"))

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 23:32):

Richard Feldman said:

there are a few problems with that

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 23:32):

Start reading from that message

view this post on Zulip Norbert Hajagos (Feb 13 2025 at 23:38):

Highlighting . and .: the same way really makes a difference for this "staircase operator". Not a huge fan of it's aesthetics (going up, rather than right), but it seems very practical. It starts with . and kinda like how :: is used in Rust or CPP to refer to Static methods, the : signals that the function is coming from somewhere else, not directly hanging off of what's on the left hand side.
Gotta go, have a good one!

view this post on Zulip Niclas Ahden (Feb 14 2025 at 00:16):

.: looks neat! Should we consider how each option looks with a lambda?

main! = |_|
    "./input.txt"
    .:Path.from_str()
    .read_bytes!()?
    .:(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
    "./input.txt"
    .(Path.from_str) # Is this viable, or does it have to be `.(Path.from_str)()`?
    .read_bytes!()?
    .(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
    "./input.txt"
    .|Path.from_str() # Teach `.|x| f(x)` first then explain that `.|` is a shorthand for that
    .read_bytes!()?
    .(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
    "./input.txt"
    \Path.from_str()
    .read_bytes!()?
    \(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
    "./input.txt"
    ->Path.from_str()
    .read_bytes!()?
    ->(|bytes| "Nice: ${bytes.to_utf8()}") # Here we're getting into -> and => territory

view this post on Zulip Sam Mohr (Feb 14 2025 at 00:17):

I expect we'd want to put lambdas in parens for all of these

view this post on Zulip Sam Mohr (Feb 14 2025 at 00:17):

Which has the detriment of .(func)(args) but should be a pretty rare operation

view this post on Zulip Sam Mohr (Feb 14 2025 at 00:18):

If not, then there can't be anything chained after the lambdas

view this post on Zulip Isaac Van Doren (Feb 14 2025 at 00:36):

I assumed that we wouldn’t be able to use lambdas with the proposed dot piping operators. I would prefer that they are not allowed to keep things simple. I usually find using a lambda within a pipeline like that to be an indication that the pipeline needs to be broken up.

view this post on Zulip Sam Mohr (Feb 14 2025 at 00:39):

Richard Feldman said:

I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:

Proposed and shot down, but I totally agree with you

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 00:46):

Once we have variables with foo_ I agree. Until then, pipelined lambdas are a great way to do inplace mutation without needing to rename constantly

view this post on Zulip Niclas Ahden (Feb 14 2025 at 00:58):

I think it’s neat for a quick transformation in a short pipeline :man_shrugging:

view this post on Zulip Sam Mohr (Feb 14 2025 at 01:01):

It can be pretty handy every once in a while, yes

view this post on Zulip Richard Feldman (Feb 14 2025 at 03:30):

I have really conflicting feelings about .: because:

view this post on Zulip Sam Mohr (Feb 14 2025 at 03:31):

We're exploring new ground!

view this post on Zulip Richard Feldman (Feb 14 2025 at 03:32):

objectively, I like that:

view this post on Zulip Joshua Warner (Feb 14 2025 at 03:34):

I don't think there's a precedence issue with _any_ of the options discussed is there?

view this post on Zulip Sam Mohr (Feb 14 2025 at 03:37):

name suggestion: method lookup syntax

view this post on Zulip Luke Boswell (Feb 14 2025 at 04:00):

The functionality it provides isn't in any other languages though right? -- like this really is completley new ground

view this post on Zulip Richard Feldman (Feb 14 2025 at 04:01):

@Joshua Warner the only precedence issue is with the ones that have (or look like they should have) spaces around them because they're infix operators

view this post on Zulip Richard Feldman (Feb 14 2025 at 04:01):

Luke Boswell said:

The functionality it provides isn't in any other languages though right? -- like this really is completley new ground

the ReasonML -> does exactly this

view this post on Zulip Richard Feldman (Feb 14 2025 at 04:02):

Sam Mohr said:

name suggestion: method lookup syntax

isn't this the one that isn't method-related though? :big_smile:

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:02):

The thing I'm trying to tie together is that we're using a method-like syntax for a non-method

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:02):

AKA a local function

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:03):

As long as we go with dot-something, we're emulating a method call

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:03):

But you're right, this doesn't say "non-method"

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:03):

That would get pretty wordy...

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:07):

They call it the "pipe first" operator in ReasonML: https://reasonml.github.io/docs/en/pipe-first

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:07):

Sounds like we should maybe just call it the pipe operator (if we go with ->)

view this post on Zulip Richard Feldman (Feb 14 2025 at 04:13):

yeah right now in my head .: feels like the "cool option" and -> feels like the "safe option" :big_smile:

view this post on Zulip Luke Boswell (Feb 14 2025 at 04:16):

.: could be the apply operator

view this post on Zulip Luke Boswell (Feb 14 2025 at 04:17):

Like maybe the .: is like an A

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 04:36):

I don't understand why something like .: but not .$

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 04:37):

Anyway, I pretty strongly lean -> over those currently.

view this post on Zulip Richard Feldman (Feb 14 2025 at 04:47):

I wrote up a draft of an entry in a future language reference about function calls:

https://docs.google.com/document/d/1Orw1Ti_If45rI2GQvkZO3iTIhb7PxScx6iDJIa4PumM/edit

(I happened to use -> at the time, but obviously other options could slot in there too.)

view this post on Zulip Sam Mohr (Feb 14 2025 at 04:59):

Arrow call just Makes Sense

view this post on Zulip Luke Boswell (Feb 14 2025 at 04:59):

It's nice how the three different ways of calling a function are all named "calls"

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:01):

Function application, Function piping, and Function dispatch

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:04):

yeah I tried to pick names I thought would line up with the names people would likely choose independently, with the exception of "dispatch calls"

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:04):

I'm calling those methods

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:05):

Function -- application, pipes, methods

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:05):

I chose that one over "method calls" because I've already seen confusion over the concept of whether Roc has a first-class thing called a method :sweat_smile:

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:05):

I've got my brainstorming hat on... just thinking about names

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:05):

Richard Feldman said:

I chose that one over "method calls" because I've already seen confusion over the concept of whether Roc has a first-class thing called a method :sweat_smile:

I understand, but I'm 99% sure people will just call it a method :laughing:

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:06):

Even zig has "methods"

// Structs can have methods
// Struct methods are not special, they are only namespaced

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:07):

yeah I agree, but I think it might be helpful to create a meme response of "Roc doesn't have methods"

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:07):

I'll avoid bike shaving this

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:07):

Richard Feldman said:

yeah I agree, but I think it might be helpful to create a meme response of "Roc doesn't have methods"

Why though... wouldn't this be a natural name that people can click with pretty easily if they want to know what it is?

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:07):

and if the official language reference uses the term methods to describe the calling style, it might make it harder to correct the mental model

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:08):

I'm onboard with memes, but here we're thinking about how these concepts hold together

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:09):

Maybe "pass to" call could be a good description

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:09):

the problem is that methods in every other language have different semantics

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:09):

like in some mainstream languages you can't just pass a method to a higher-order function

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:10):

because this is special

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:10):

I definitely think calling them something "dispatch" something is helpful

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:10):

(or self or whatever)

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:11):

I'm thinking about the "pass to" thing -> and what might be a meaningful name, as compared to "arrow" which doesn't quite feel right

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:11):

it's possible that this is the wrong direction for teaching though

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:12):

Yeah, we're building a bit of a strawman here (as in I don't expect these words to actually be used in the tutorial) -- but it's a helpful model for putting these features side-by-side and thinking about why they exist (I'm having fun thinking about it at least)

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 05:13):

Yeah, I definitely would lean far away from methods

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 05:13):

There is a reason that it is a pipeline and not a method chain in most functional languages

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 05:13):

Different model

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 05:13):

Better model

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 05:13):

More procedural model

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:14):

another option would be to formally define something like "in Roc, a 'method' refers to a function that's defined in the same module as the type of its first argument."

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:16):

the problem with that is, if you just start throwing around the term "method" in casual conversation, some people will be aware of this "official" (and unusual) definition, and others may not - they may be assuming a definition based on other languages they've used

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:16):

and there's no real way to tell until after the conversation has gotten confusing

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:17):

whereas "Roc doesn't have methods" is a quick way to get everyone in the conversation on the same page before that happens

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:18):

It's worth a try!

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:20):

In roc, there are three ways to pass arguments to a function; (1) applying, (2) piping, and (3) dispatching.

Here are examples of these three styles;
(1) Str.contains(url, "a")
(2) url->Str.contains("a")
(3) url.contains("a")

Applying arguments to a Function

You can apply a function in the normal way...

Piping arguments to a Function

You can also use a pipe operator -> ...

Dispatch to a Function

You can dispatch ...

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:21):

Should maybe put dispatch first (before piping) to imply its importance?

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:23):

I think it's easiest to learn them in this order because each explanation builds on the previous one

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:24):

Okay, sure

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:24):

like dispatch does the lookup part but also the "argument first" part

view this post on Zulip Sam Mohr (Feb 14 2025 at 05:24):

I guess the benefits of static dispatch should lead to its usage if its actually valuable

view this post on Zulip Richard Feldman (Feb 14 2025 at 05:25):

yeah I don't think its use will need to be promoted - it'll promote itself :big_smile:

view this post on Zulip Luke Boswell (Feb 14 2025 at 05:47):

Doc is looking pretty good. I can imagine this existing and it just feeling natural. Like "why doesn't every language have three ways to call a function..."

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 10:46):

“Local dispatch”? The idea is to name both “->fn” and “.fn” after a single concept (dispatch), but distinguish by kind. maybe, “dot/arrow dispatch”. Smth like that.

view this post on Zulip Niclas Ahden (Feb 14 2025 at 10:47):

I looked through the thread but didn't find & suggested:

time = 4&hours()&ago()! # Not great

main! = |_|
    "./input.txt"
    &Path.from_str()
    .read_bytes!()?
    &Foo.from_bytes()?
    &transform(2, Much)
    .to_bytes()?
    &Path.write_bytes!(Path.from_str("./output.txt"))

Precedent:
Pipe: https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Function.html#v:-38-
Functor pipe: https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Functor.html#v:-60--38--62-
Reminds of Ruby's artists.map(&:perform)

view this post on Zulip Niclas Ahden (Feb 14 2025 at 10:48):

I think most options struggle on a single line, esp. when meeting a ?:

.to_bytes()? |> Path.write_bytes()
.to_bytes()?->Path.write_bytes()
.to_bytes()?.:Path.write_bytes()
.to_bytes()?.(Path.write_bytes)
.to_bytes()?&Path.write_bytes()
.to_bytes()?\Path.write_bytes()
.to_bytes()?.|Path.write_bytes()

Pizza fares well by using spaces (cheating)

view this post on Zulip Sam Mohr (Feb 14 2025 at 10:50):

&& is technically not taken anymore since we're moving to and and or for logical operators, but we'll want to ideally parse && still and say "did you mean and?"

view this post on Zulip Sam Mohr (Feb 14 2025 at 10:52):

Anyway, I think & is a valid option!

view this post on Zulip Sam Mohr (Feb 14 2025 at 10:53):

Not sure how much people will like it, but it works

view this post on Zulip jan kili (Feb 14 2025 at 11:15):

Niclas Ahden said:

I think most options struggle on a single line, esp. when meeting a ?

For completeness:

.to_bytes()?.:Path.write_bytes()

view this post on Zulip jan kili (Feb 14 2025 at 11:24):

Wow I really like the aesthetics of that - I think it has the second-most transparent pixels without a whitespace character (second to ..), which helps visually break up the line. A Roc color theme that dims noncritical punctuation would help too, though that might even be more of a visual factor than pixels for aesthetics.

view this post on Zulip Niclas Ahden (Feb 14 2025 at 11:28):

Sorry, I thought I included that one! Added!

view this post on Zulip jan kili (Feb 14 2025 at 11:32):

As a modal text editor user, :foo slightly reminds me of executing a command. I suppose that's similar to local & imported functions being commands in the current namespace.

view this post on Zulip jan kili (Feb 14 2025 at 11:52):


Here's a third idea for how to talk about the three function application syntaxes: instead of it being "three ways to call a function" or "three ways to pass arguments", we could describe "three ways a function can get its first argument", since non-first arguments are always treated boringly and we want to scope the mental model very tightly.

  1. A function can apply/wrap/include its first arg.
  2. A first arg can be passed/applied to a function.
  3. A first arg can call/dispatch a function from its own module.

(I'm proposing phrasing, but idk which specific verb words are best, if any.)

These phrasings seem equally robust (as compared to the other nickname-based proposals) for the arbitrary sentence structures of casual conversation:

  1. "The first step is to call contains on the url."
  2. "Now pass it into the contains function."
  3. "Yeah, just call it from the url."

Potential confusion this avoids:

view this post on Zulip Richard Feldman (Feb 14 2025 at 12:44):

Sam Mohr said:

Sam Mohr said:

So can autocomplete replace the dot with an arrow on selection?

The answer seems to be that the LSP can do this! https://docs.rs/lsp-types/0.97.0/lsp_types/struct.CompletionItem.html#structfield.text_edit

I was thinking some more about this, and I think it narrows the gap but doesn't close it completely.

It's true that this means if I type . we can show autocompletions for both .: and -> in there, and then switch the operator accordingly if you select one of those.

However, if I type . and want to narrow it down to just the .: ones, I tap : and I'm there. For -> I have to start over.

view this post on Zulip Richard Feldman (Feb 14 2025 at 12:45):

that said, I guess we could show an autocomplete for -> options if you press - :thinking:

view this post on Zulip Richard Feldman (Feb 14 2025 at 12:45):

upside there would be that it filters after typing one character instead of 2, but on the flip side, it might be weird to see an autocomplete menu show up every time you're trying to do subtraction or negation

view this post on Zulip jan kili (Feb 14 2025 at 12:46):

(or if you type .-? eh that's unintuitive unless you were in this discussion, nvm, continue sorry :sweat_smile:)

view this post on Zulip Richard Feldman (Feb 14 2025 at 12:51):

I don't have any conclusions, just observing :big_smile:

view this post on Zulip Niclas Ahden (Feb 14 2025 at 12:58):

Agree that autocomplete is nice with .:. It also aligns quite well (not perfectly, but eh). I'd probably go so far as to use conceal in vim or ligatures to transform it to a single char that aligns perfectly: tadaa! :tada:

:: is usually used for traversing modules etc. so you could argue that adding : onto . is "reaching outside of the current type". Am I reaching? I'm reaching :sweat_smile:

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 13:39):

JanCVanB said:

(or if you type .-? eh that's unintuitive unless you were in this discussion, nvm, continue sorry :sweat_smile:)

I’d say the resolution of . to -> is not intuitive in itself. But once you learned about the feature, you’ll likely try to narrow it by .- because in the dropdown you’ll see the variants containing -

foo
bar
->foo
->bar

So the use of .- for autocompletion is as intuitive as .foo

view this post on Zulip witoldsz (Feb 14 2025 at 14:13):

For someone who did miss it "live" and try to catch up, is this all about not using |> because we want to start with a . instead? And this is because people are familiar with a dot . to kick-start an autocomplete?

If this is the case, then I would like to see this discussion happening in the (near) future when |> gets into JavaScript and suddenly whole world _LOVE_ |> and all the auto-completion tools recognize that and now we are in the Roc world with strange ..fn or .(fn) or .$fn and everybody else are living their happy lives with |>, but not us.

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 14:25):

I would like to see this discussion happening in the (near) future when |> gets into JavaScript

I remember the pipe operator for es proposal was introduced like 10 years ago. "near future" in this context is never future :D

I don't think it's about mainstream or hype but about consistency. The difference between chaining via static dispatch and pipe operator is that static dispatch is not an operator. So it has no complications with precedence. And having two similar features for chaining that work differently seems uncomfortable. But it's at least in my head

view this post on Zulip witoldsz (Feb 14 2025 at 14:35):

The difference between chaining via static dispatch and pipe operator is that static dispatch is not an operator.

OK, but on the other side, |> is already some kind of special-operator, because it applies left-side argument to the function call. So a dot . call is not that different… or maybe it is :thinking:

view this post on Zulip Dawid Danieluk (Feb 14 2025 at 14:43):

I'm wondering, would it be a big deal to keep separate autocompletes for methods vs functions? I kind of like the idea of pressing . and only seeing methods, pressing - and only seeing pipeable functions. It makes it easier to distinct APIs coming from some package and my own functions (also makes autocomplete list shorter, because I guess if everything would be shown the list could be reaaaaly long).

I think personally maybe I'd even disable changing . to -> automatically if LSP allowed that (hard to tell because I've never used anything like that so I don't have empirical experience if it would be nice to use).

Having separate first signs is nice in a way that it allows to experiment with UX a bit (or even allow users to set it in the settings).

EDIT. in other words

However, if I type . and want to narrow it down to just the .: ones, I tap : and I'm there. For -> I have to start over.

I think that narrowing might be nice both ways, not only from all to functions but also to only methods.
Not sure what's opinion of people here, maybe majority thinks that autocomplete should absolutely show everything, but I've feeling like you'd normally get grasp of what's method/function pretty quickly and wouldn't make many mistakes requiring to change the operator.

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 14:53):

it makes it easier to distinct APIs

I assume "native" methods are expected to be in the beginning of the list and foreign contain leading "->" so no real problem to distinguish them?

the list could be reaaaaly long

I think it will contain only suitable interfaces from the current scope? anyway, if the list is long, it will be long even if separated.


wanted also to note, that -> takes the same number of characters as .- for narrowing

view this post on Zulip witoldsz (Feb 14 2025 at 15:03):

I think it will contain only suitable interfaces from the current scope?

My hope is it would find the methods from all available scopes and import them, so I would not have to :)

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 15:04):

you mean also in not imported modules? sounds huge but why not :D

view this post on Zulip Dawid Danieluk (Feb 14 2025 at 15:51):

Kiryl Dziamura said:

you mean also in not imported modules? sounds huge but why not :D

I've assumed that it would suggest all available functions with matching signatures (not just imported ones) and autoimport them. I use it all the time in TS and didn't even think before that I'd have to manually find it first and import them into the scope before LSP would suggest it. If we had to do that I think that would severely limit usefulness of the feature.

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 16:01):

I probably just got used to write some things mostly manually to avoid wrong imports that provide similar interfaces to what I want. Like, if I want to import smth, first I want to ensure that the import location is correct

view this post on Zulip Kiryl Dziamura (Feb 14 2025 at 16:02):

So I forgot that autocompletion already works for the whole project

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 17:18):

I'm sure it is probably decision fatigue at this point from all the discussion, but a large part of me leans that we shouldn't do any of these for now. We should leave in |> and see how it feels. I totally understand that |> has precedence issues, but I would guess that this feature is less often used than normal static dispatch.

To be a bit more concrete, in Richard's real world app, this feature is used 7 times. Of those, there are exactly two unique ways it is being used. Fundamental diffs:

-            .(Ok)
+            |> Ok
-    expired_time = now + 1.(hour)
+    expired_time = now + (1 |> hour)

Looking at roccibird, most things would switch to static dispatch. Probably the biggest pain for rocci bird will actually be that static dispatch is at the module level and it define multiple custom records that would want to use it (they could instead use this syntax). I guess I they also could use static dispatch as long as they functions they call have unique names.
Anyway, roccibird has 6 calls in this odd may or may not be static dispatch. Otherwise, all pipes would turn to static dispatch except for 3. All of those three could work with the single arg version of .().
This is the only hairy location with pipes. It hits bad precedence:

pipes =
    prev.pipes->updatePipes().appendIfOk(pipe)

to

pipes =
    prev.pipes |> updatePipes() |> |l| l.appendIfOk(pipe)

or

pipes =
    (prev.pipes |> updatePipes()).appendIfOk(pipe)

view this post on Zulip Brendan Hansknecht (Feb 14 2025 at 17:20):

Also, this is the part that maybe could use static dispatch? Would require exposing 3 custom types from the same module. So names are less nice. That or moving all 3 types to separate modules, but that feels like overkill in this case. I guess that is where it would be nice to nest a module inside another.

In this code, each prev is a bespoke record for the stage (could be custom)

when model is
    TitleScreen prev ->
        prev
        |> updateFrameCount()
        |> runTitleScreen!()

    Game prev ->
        prev
        |> updateFrameCount()
        |> runGame!()

    GameOver prev ->
        prev
        |> updateFrameCount()
        |> runGameOver!()

view this post on Zulip Richard Feldman (Feb 14 2025 at 18:08):

well the new compiler has to implement something from scratch, and I'd prefer to try one of the new options out (-> if I had to pick one right now, since it still seems like the safe option) so we can at least see how it feels in practice.

view this post on Zulip Luke Boswell (Feb 14 2025 at 19:42):

I think I've missed something. Why can we make the precedence work for -> but not |>?

view this post on Zulip Luke Boswell (Feb 14 2025 at 19:43):

I also thought this whole search was for something starting with a . for autocomplete.

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:49):

I think . is a factor but not the only factor

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:51):

although I hadn't considered the point from earlier about maybe wanting two separate autocomplete namespaces to narrow down the search results - one for dispatch and one for functions in local scope. I'm trying to think how that experience might be, and I'm not sure which way I'd actually prefer if I could pick between the two options.

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:51):

the precedence problem is really just about "does this look like an infix operator or not"

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:52):

e.g. does it have spaces around it (or would you expect it to normally)

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:52):

a|>b I see as an infix operator (even if there are no spaces) because that's what it is in every language where I've seen that

view this post on Zulip Richard Feldman (Feb 14 2025 at 19:53):

a->b is not an infix operator in any language I've seen; it always works like . does in mainstream languages

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:34):

Which is.... it has to touch one of the two operands?

view this post on Zulip Richard Feldman (Feb 18 2025 at 15:43):

from a parsing perspective?

view this post on Zulip Richard Feldman (Feb 18 2025 at 15:45):

I haven't thought deeply about it, but I suspect the parser can get away with allowing whitespace on either side of a . or ->

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:49):

I didn't think that was common. Then I looked and it is. It's amazing the number of things that parsers allow that I would never even try to do

view this post on Zulip Isaac Van Doren (Feb 20 2025 at 15:25):

I just learned that ReScript has -> as their pipe operator

view this post on Zulip Brendan Hansknecht (Feb 20 2025 at 15:37):

So rescript and reasonml both

view this post on Zulip Richard Feldman (Feb 20 2025 at 15:37):

Elias Mulhall said:

I suspect this one is a no, but Reason uses ->
https://reasonml.github.io/docs/en/pipe-first

4->days->ago!

yeah, this became the frontrunner after it was suggested!

view this post on Zulip Richard Feldman (Feb 20 2025 at 15:42):

ReasonML is the syntax part of ReScript - it's kind of complicated :sweat_smile:

view this post on Zulip Brendan Hansknecht (Feb 20 2025 at 15:44):

I had a vague inkling of that, but wasn't confident enough to comment so

view this post on Zulip Isaac Van Doren (Feb 20 2025 at 15:58):

So confusing :sweat_smile:

view this post on Zulip jan kili (Feb 20 2025 at 17:06):

When we were considering .., there was some intuitive similarity with the existing spread/rest operator that would become overloaded. Is the same achievable with ->? How can we explain piping as similar to the separation of args from return value in an uneffectful function's type signature?

view this post on Zulip Norbert Hajagos (Feb 20 2025 at 18:25):

I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.

view this post on Zulip Anthony Bullard (Feb 20 2025 at 21:48):

The whole ReasohnML/ReScript/Buckscript thing is SO confusing and I really believe that's the reason it hasn't taken off at all

view this post on Zulip Anthony Bullard (Feb 20 2025 at 21:49):

Though someone from ReScript is on today's episode of Developer Voices, so I look forward to how he sells it and any updates on that ecosystem. The episode has a subhead of "A better Typescript?"....

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:16):

yeah I was initially surprised by Louis Pilfold's experience with the Gleam syntax change making a big difference because my previous takeaway from ReasonML was "if you take a good functional language like OCaml and put a familiar syntax on it, the result is not widespread adoption"

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:17):

at the time I thought it was a pretty narrow experiment, but in retrospect there were a lot of confounding variables besides just the syntax change

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:19):

I forgot to mention that to Louis - just recorded a software unscripted episode with him where we talked a bunch about Roc's design changes and how Gleam came up in several of our design discussions, influenced the design we ended up, etc.

view this post on Zulip Anthony Bullard (Feb 20 2025 at 22:20):

I hope he's doing well. He was very nice to me when I was a contributor in VERY early days of Gleam (late 2019/early 2020)

view this post on Zulip Anthony Bullard (Feb 20 2025 at 22:22):

But yeah, with ReasonML they had the tailwind of Facebook/Meta backing, but the headwind of OCaml/Bucklescript complexity in both the toolchains and the standard library - as well as confusing messaging made even worse IMO by ReScript

view this post on Zulip Anthony Bullard (Feb 20 2025 at 22:24):

Norbert Hajagos said:

I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.

I like -> because it makes me think of I think PHP (and C to a lesser extent). I could get used to it. I never thought that Lua's use of : for method invocation would make sense to me, but now it's second nature to me

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:35):

a thing I'd never thought about before but have been ruminating about based on all our super in-depth syntax design discussions is that:

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:35):

maybe all of that seems obvious, but they aren't things I'd put together in that way before

view this post on Zulip Richard Feldman (Feb 20 2025 at 22:38):

despite the fact that if you'd asked me about them one at a time, I think I would have given the same description of them :sweat_smile:

view this post on Zulip Luke Boswell (Feb 20 2025 at 22:40):

Anthony Bullard said:

Norbert Hajagos said:

I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.

I like -> because it makes me think of I think PHP (and C to a lesser extent). I could get used to it. I never thought that Lua's use of : for method invocation would make sense to me, but now it's second nature to me

If we remove -> from match statements, then it's kind of freed back up to be used for our pass_to alternative. It wouldn't be used anywhere else then except in our Types for functions List a -> Str etc

view this post on Zulip Norbert Hajagos (Feb 21 2025 at 07:57):

Oh yeah, good point Luke! Forgot -> was also in matches.

view this post on Zulip Joshua Warner (Feb 21 2025 at 17:57):

Is -> weird to use if the function you're dispatching to is impure?

view this post on Zulip Richard Feldman (Feb 21 2025 at 18:00):

I think it's kinda conceptually weird but I think in practice it's probably fine


Last updated: Jun 16 2026 at 16:19 UTC