Stream: ideas

Topic: static dispatch - proposal


view this post on Zulip Richard Feldman (Nov 09 2024 at 02:08):

I wrote up a proposal...any feedback welcome!

https://docs.google.com/document/d/1OUd0f4PQjH8jb6i1vEJ5DOnfpVBJbGTjnCakpXAYeT8/edit

view this post on Zulip Luke Boswell (Nov 09 2024 at 02:27):

I think this is great!!

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 02:52):

This is fantastic! I'm very in favor.

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:14):

I'm not very keen on the .() syntax for chaining local function calls:

pipes =
    prev
    .pipes
    .(updatePipes("extra argument"))
    .appendIfOk(pipe)

Given that the syntax is intentionally similar to string interpolation, I assumed initially that the contents would need to be a normal function expression like any of these:

    .(someFunction)

    .(.updatePipes("extra argument"))

    .(\x -> x * x)

It surprised me that the leading . is not required in the case that the function accepts more than one argument.

    .(updatePipes("extra argument"))

This seems inconsistent as the code within the .() would not be a valid expression outside of the .().


I propose instead allowing local functions to be chained with : like this:

pipes =
    prev
    .pipes
    :updatePipes("extra argument")
    .appendIfOk(pipe)

pipes = prev.pipes:updatePipes("extra argument").appendIfOk(pipe)

This syntax is more concise and seems more natural to me. It is easy to guess how it works, it is cohesive with the . syntax, and it also has a clear visual difference from the . syntax. With this approach you'll be able to immediately communicate to the tooling if you're looking for suggestions for local functions or methods by typing either . or :.

One consideration is that current piping approach and the proposed syntax (I think) both allow lambdas to be used as standalone pipeline stages, but the : syntax does not.

number
|> \x -> x * x
|> Num.toStr

number
.(\x -> x * x)
.toStr()

I think this is fine, or perhaps even a good thing. When I see a standalone lambda in a pipeline, it is a strong indication that the pipeline should instead be broken up with an intermediate definition.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:17):

I agree, that syntax is the only part I don't love. I think .. might also work?

pipes =
    prev
    .pipes
    ..updatePipes("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:18):

I thought of it as a simplification of:

.(.updatePipes("extra argument"))

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:19):

I think it works well for autocompletion too. When you type the first ., you see all methods, and when you type the second you see all functions

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:21):

qualified example:

pipes =
    prev
    .pipes
    ..Foo.updatePipes("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:23):

An upside of : is that it lines up with . because it's only one character

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:23):

but maybe the distinction is actually a good thing?

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:25):

Yeah I do like that : keeps everything in line. Having the full autocomplete menu appear when hitting . does seem nice though.

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:26):

The qualified example looks very weird to me

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:27):

pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)

I think the .. syntax works less well when the calls are on a single line

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:28):

Yeah? I feel like it reads the same

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:28):

I agree the qualified example is a bit weird

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:30):

It looks very strange to me but of course it's subjective. Part of it is probably because I'm used to seeing foo()..bar() as a typo after I removed a method call from a chain but missed the period when deleting it.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:31):

just throwing it out there:

pipes =
    prev
    .pipes
    |updatePipes("extra argument")
    .appendIfOk(pipe)

pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)

That is, after all, the one character pretty much every programmer associates with "pipe" :smiley:

view this post on Zulip Sam Mohr (Nov 09 2024 at 03:31):

While we're leaning towards Gleam's syntax, we can just take their function capture syntax and make calling methods and functions look the same. If you call a function like this:

pipes =
    items
    .map(.transform("abc", _))

The _ usage calls transform like \x -> transform("abc", x).

But if you do:

pipes =
    items
    .map(.transform("abc"))

The lack of _means we should look for a method called Item.transform : item, Str -> ABC

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:34):

I think that makes sense, but I wonder if it's a little too subtle

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:34):

just throwing it out there:

pipes =
    prev
    .pipes
    |updatePipes("extra argument")
    .appendIfOk(pipe)

pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)

That is, after all, the one character pretty much every programmer associates with "pipe" :smiley:

This is a solid option. I still have a slight preference for : though because it looks more consistent with .

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:36):

The _ syntax seems like a good candidate for adding later if there is enough demand.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:36):

I think in the way Sam suggested, it'd be required to distinguish functions from methods

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:37):

_ both signifies that this is a pipe and where the argument goes

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:41):

I think the main issue with | is that this symbol is often sorrounded with spaces, but that looks weird if there's a method call after in the same line:

pipes = prev.pipes | updatePipes("extra argument").appendIfOk(pipe)

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:43):

I think I'm a bit confused about the "make calling methods and functions look the same part".

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:43):

All syntaxes suggested so far:

pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)

pipes = prev.pipes:updatePipes("extra argument").appendIfOk(pipe)

pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)

pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)

pipes = prev.pipes.updatePipes(_, "extra argument").appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:44):

_ looks pretty cool to me now

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 03:48):

I'm used to foo.bar(10) meaning that foo is the first argument to bar even if it is very implicit, so having foo.bar(_, 10) instead looks very wrong to me.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:50):

I'm thinking about it like this:

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:53):

this does mean . works differently depending on the RHS, which as you pointed out, is also a problem in the syntax in the proposal

view this post on Zulip Sam Mohr (Nov 09 2024 at 03:53):

That's my thoughts exactly, it must be the right way if we both had it

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:58):

hm, qualified looks pretty confusing, though:

pipes = prev.pipes.Foo.updatePipes(_, "extra argument").appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:59):

technically not ambiguous because only a module name can be uppercase

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 03:59):

but that totally looks like pipes is a record with a field called Foo

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 04:00):

pipes are very often used with qualified functions, so the syntax should be good for that case

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 04:03):

I think I'm back to .. or :

view this post on Zulip Jared Cone (Nov 09 2024 at 04:24):

Why can't local functions be chained?

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 04:27):

I assume you mean chained with .?

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 04:29):

One downside is that there could be conflicts where you might have a method MyType.foo and a local function called foo. When you type value.foo() the compiler would presumably pick the method. If you then deleted MyType.foo, the previous usage would silently fall back to the local function instead.

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 04:30):

It would also make it more difficult to determine which calls are local functions and which are methods

view this post on Zulip Richard Feldman (Nov 09 2024 at 04:59):

I think for the local function chaining, we should look at fully-qualified examples

view this post on Zulip Richard Feldman (Nov 09 2024 at 04:59):

e.g.

pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 05:00):

I also don't love this syntax, but the parens make it clear when it's fully-qualified local call

view this post on Zulip Richard Feldman (Nov 09 2024 at 05:01):

I think _ is a deep rabbit hole...Scala and LiveScript have it (as mentioned briefly in the doc) and I'd rather not introduce it if we can avoid it

view this post on Zulip Richard Feldman (Nov 09 2024 at 05:01):

then you get into things like "how about when _ is ..." etc. etc.

view this post on Zulip Jared Cone (Nov 09 2024 at 05:14):

For chaining functions imported from another module, would it be .Module.func() or .(Module.func())?

view this post on Zulip Richard Feldman (Nov 09 2024 at 05:16):

actually I think it's best to talk about examples of both qualified and unqualified side by side!

pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)
pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 05:17):

so one reason .Module.func() can't work is that if you drop the Module. because you imported func unqualified, now you're at .func() which is a method call rather than a local function call :big_smile:

view this post on Zulip jan kili (Nov 09 2024 at 05:31):

Me at the top of the doc: ohnoooooobleghhhhhh
Me halfway down: damnit I'm in

(clarification: @Richard Feldman I'm consistently very impressed with your technical writing's clarity and your rhetorical ability to change my mind. The insatiable bikeshedder in me is often sated in this chatroom.)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 05:32):

yeah, that's becoming a pattern with these proposals for me too :laughing:

view this post on Zulip jan kili (Nov 09 2024 at 05:33):

Random idea about operator overloading:
I remember liking Python's magic methods syntax in 2014, so maybe we could take inspiration from that to avoid users accidentally opting in to an operator - name the de-sugared operator functions something a little goofy but stylish, like ._add_()

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 05:36):

Or literally the symbol: (+)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 05:36):

(without allowing you to define new ones)

view this post on Zulip jan kili (Nov 09 2024 at 05:47):

Roc is my first language with Elm-like whitespace-y pipe chain syntax (see those examples in the Background section of the proposal), and I really like it. I hope that we don't trade much form/style for function/features here. I expect that if this is a more powerful syntax, then the whitespace alternative will be abandoned in a year or so. As long as I can still have a splendid time reading Roc via cat/less/vim, I'm happy.

view this post on Zulip jan kili (Nov 09 2024 at 05:50):

Significant reduction in # of sad lambda expressions required might be worth a significant reduction in helpfully-qualified function names.

view this post on Zulip jan kili (Nov 09 2024 at 05:52):

I suppose a motivated qualifier-lover could choose to write .try(Str.toI64) as .(Result.try(Str.toI64)), but that won't be the norm.

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 08:19):

Really love the general direction of this, especially how it manages to replace abilities! Couple of things I was thinking as I read this.

For me too one thing that I didn't super like was the .(func) syntax. I want to add another alternative to the ones already proposed:

pipes =
    prev
    .pipes
    .updatePipes
    .List:appendIfOk pipe

The idea here is:

The above suggestion would break the possibility to use this syntax to replace abilities. We could bring that back by optionally allowing the module qualifier to be ellided:

pipes =
    prev
    .pipes
    .updatePipes
    ._:appendIfOk pipe

In the above the compiler would replace _ with a module name in the same way that . does by default in the proposal. I'm using _ here because we use that in other places to tell the compiler it should fill in a gap for us, but other syntaxes could work too.

The above design would combine a couple of things I like both in current Roc / Elm and the new design:

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 08:24):

Another thing I noticed in the codesnippets is that replacing |> with . removed whitespace and made the code look more compact to me, but not necessarily in a nice way.

I don't know if it's just a matter of getting used to or if there's something more to this, curious if anyone else had this experience, I might just be an outlier.

I was wondering if it'd be an improvement for the formatter to put whitespace after . by default, similar to what we do for |>:

pipes =
    prev.pipes
    . (updatePipes)
    . appendIfOk pipe

This looks a bit different from what folks coming from languages with a C-like syntax are used to, so that's a downside, though you could still write the code without the spaces, so :shrug:

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 08:33):

One last thought: with regards to the problems newcomers have with putting parens in the right places to get function calls. One thought that occured to me: If the syntax of the language is such that it allows both C-like parens and ML-like parens to live side-by-side, then we have other options:

While that might help people learning the language, code snippets shared on the internet would still look ML-like so it'd maybe not trigger the "Gleam Effect" of pulling in people that otherwise bounce off the syntax.

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:03):

a downside of using a separator other than . for module qualifiers is that it would impact the autocomplete experience; in the proposal, if I say either dict. or Dict., in both cases right after I press . I get an autocomplete for whatever's valid in that spot

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:04):

I like the idea of . always resulting in autocomplete

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:20):

here's an option we haven't discussed:

pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)

pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)

view this post on Zulip Norbert Hajagos (Nov 09 2024 at 13:21):

Yes, not needing to think which key to press to bring up the completion list is very helpful. The experience of pressing ., then realising "ahh, it's somewhere in here, not in the module" (or the reverse), deleting ., then pressing that other key isn't that great. I also think piping to a qualified fn is confusing without the parens, so although my initial reaction to the .(fn) syntax wasn't joyful, all examples look and feel great.

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:23):

Richard Feldman said:

here's an option we haven't discussed:

pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)

pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)

this has the same number of parens as in the doc, but they aren't as nested. it's basically:

foo.bar(baz) uses static dispatch to find bar

foo.(bar)(baz) uses local scope to find bar

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:26):

I like this better than the doc version because it doesn't have the weird "using normal-looking function call syntax to call a function with one of its arguments missing" problem

view this post on Zulip Richard Feldman (Nov 09 2024 at 13:27):

e.g. str.(Str.append("!")) vs str.(Str.append)("!")

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 13:28):

a downside of using a separator other than . for module qualifiers is that it would impact the autocomplete experience; in the proposal, if I say either dict. or Dict., in both cases right after I press . I get an autocomplete for whatever's valid in that spot

A possible way around this would be to make the module separator .::

pipes =
    prev.pipes
    . updatePipes
    . List.:appendIfOk pi

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 13:46):

But also, pushing back on the previous a little, if : were the module separator, then I don't think putting a . after a capitalized word would be valid syntax, right?

I agree that having to try out both . and : to get all the possible completions would be annoying, but if the only thing that can get you completions in that context is : then maybe it's not a problem?

Because there's many other possible autocompletions that might trigger on other keypresses: ( to insert a closing bracket, any letter key to complete an identifier, etc.

view this post on Zulip Sam Mohr (Nov 09 2024 at 13:56):

I'm really not a fan of foo.(bar)(baz). I think a beginner would have no idea what it means, and it looks like some dynamic dispatch/currying stuff is happening without explanation. Maybe it's just a familiarity with Gleam, but it seems like _ capture syntax is pretty obvious because you have the same number of arguments passed to the function, and the missing one is what gets piped to. It still looks like a function call

view this post on Zulip Sam Mohr (Nov 09 2024 at 13:58):

With respect to qualified function calls, we can actually easily tell whether a function is qualified based on the capitalization of the function names, right?

pipes =
    prev.pipes
    .updatePipes
    .List.appendIfOk(xyz)

The formatter would just say "any list of title-case names is kept on the same line, and then one last lowercase ident is on the same line"

view this post on Zulip Sam Mohr (Nov 09 2024 at 13:59):

I'm not sure why we need to qualify things within parentheses

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:00):

Also, on _ syntax, we can keep it very simple by saying "only one of the function args can be _, and it has to be the whole arg, not nested in an expression"

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:01):

@Richard Feldman what do you think of the one I suggested at the beginning?

pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)

pipes = prev.pipes..Pipes.update("extra argument").appendIfOk(pipe)

Feels lightweight, distinguished enough from method calling, and the autocomplete experience is really good I think. The first . shows you method + functions, and the second shows you only functions.
You do have to learn what it means but I think that’s true of all the other syntaxes suggested.

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:02):

I really like this suggestion, and on the point of . vs. .. with respect to auto-complete, you just say:

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:04):

I think the first . should suggest methods first, but it should also suggest .-prefixed functions after

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:04):

Though as I said a few messages ago, I think basically the same benefit can come without ... You suggest both methods and functions (methods first), and then:

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:04):

kinda how rust analyzer suggests &-prefixed fields

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:04):

the .. syntax works, as far as I can tell

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:04):

And there's no need for surrounding parens AFAICT

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:05):

Agus Zubiaga said:

technically not ambiguous because only a module name can be uppercase

I mentioned that yesterday, but it looks like record field access to me

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:05):

my only concern with that one is that we already use .. to mean certain list-specific things, and we're planning to use it for even more list things, and this would be a use of it that has nothing to do with lists

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:06):

I disagree that it looks like record field access. The capitalization is not valid as a record field name, and we'd have different color syntax highlighting

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:06):

Sam Mohr said:

With respect to qualified function calls, we can actually easily tell whether a function is qualified based on the capitalization of the function names, right?

pipes =
    prev.pipes
    .updatePipes
    .List.appendIfOk(xyz)

The formatter would just say "any list of title-case names is kept on the same line, and then one last lowercase ident is on the same line"

yes, but if that's the design, how does it look if the call is not qualified?

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:07):

Richard Feldman said:

my only concern with that one is that we already use .. to mean certain list-specific things, and we're planning to use it for even more list things, and this would be a use of it that has nothing to do with lists

Yeah, that’s valid. We could do .> or something like that.

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:07):

When you say "the call is not qualified", are you asking how a user would write "call a function in scope that's not a method"?

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:07):

yeah exactly

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:08):

(this is why I think whenever we post examples in this thread about non-dispatch chaining, we should always post 2 versions, one qualified and one unqualified, because a lot of syntaxes seem to work fine with one and not at all with the other! :sweat_smile:)

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:09):

Yes, that's a good callout

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:09):

Okay, I think it doesn't work, so yep, leaning towards ..

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:10):

I'd much prefer .. to needing more parentheses personally

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:10):

It works with auto-complete, it's easy to type, it's visually sparse, and leaves parens to just denote function calls around args

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:11):

(unless I can figure out a way without it

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:11):

The one detriment I can see to . and .. is that they are different lengths

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:11):

But that communicates something, so I don't think that's so bad

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:12):

I think the collision with how we use .. for lists (and are planning to massively expand that use to things like record updates and possibly type signatures) is the main downside

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:12):

And I think

data
.first()
|second()

is visually inconsistent with respect to vertical space

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:13):

That's totally fair

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:13):

Do we want it that much for lists? haha

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:14):

I feel like piping is a lot more common

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:14):

well we already use it for pattern matching and I think the thread where we talked about using it more was really well received

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:14):

Then to make my thoughts clear, it's a high bar for me to accept

data
.first()
.(second)()

or

data
.first()
(.second())

because there's a strong currying/dynamic dispatch implication that I think is pretty misleading

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:15):

You're right that this is now syntactically ambiguous (or at least visually):

x = [first..second()]

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:15):

to me I think of dynamic dispatch being things like foo.dispatch(:bar, arg)

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:15):

Is second an unqualified function call on object first, or is second() generating a second list

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:16):

I don’t think that’s ambiguous, that’s not a valid pattern

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:16):

where there's some "symbol" concept that you send to specify the method

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:17):

pipes = prev.pipes->updatePipes("extra argument").appendIfOk(pipe)

pipes = prev.pipes->Pipes.update("extra argument").appendIfOk(pipe)

Yeah, sorry. It probably shouldn’t be -> because of the autocomplete experience.

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:17):

@Agus Zubiaga you're right, we require a comma according to the issue: https://github.com/roc-lang/roc/issues/7093

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:17):

to me, .(foo) reads like "evaluate the expression foo and then keep going based on that," kinda like $(foo) in string interpolation

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:18):

I'd be okay with -> or .>, though .> has the benefit of not having a current meaning, whereas -> means "define a closure"

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:18):

I hadn't thought about it, but I could see allowing foo.(makeFnOnTheFly(bar))(baz) working

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:19):

not saying that's a good idea, but rather that it explains what the syntax does

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:19):

Makes sense

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:20):

I know that in Roc, it's compiled to a lambda set, so it's not true "dynamic dispatch", but foo.(makeFnOnTheFly(bar))(baz) makes perfect sense as a "generate a function and then call it"

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:20):

I thought about foo->bar but a downside is that in every language that supports it, it means some combination of record access (basically) and/or method calls - so kinda the opposite of what we want it to mean here :sweat_smile:

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:21):

Which is what makes foo.(bar)(baz) smell weird to me, but it's literally just a vibes thing so that's not a good counterargument

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:22):

when you say "evaluate the expression foo", that implies a runtime cost

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:22):

But we don't have a runtime cost, it's just a compiler inference cost

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:22):

But it's a static dispatch

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:22):

That's maybe the best I can put it

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:23):

The syntax implies a runtime cost when it should be more obvious that it's a static call, if possible

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:24):

vibes are always a valid consideration when it comes to syntax, although for me I like the vibes of foo.bar(baz) vs foo.(bar)(baz) the only thing I've changed is .bar for .(bar) and all that's changing when I do that is how that one function gets determined (static dispatch vs evaluating the expression)

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:24):

it feels like it composes nicely to me

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:24):

Oh, I think I misunderstood

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:25):

If you want a syntax to do "evaluate this expression and then call it"

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:25):

That's literally perfect

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:25):

yeah that wasn't what I originally thought, but I realized that's actually a better design haha

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:25):

I thought we were looking for a way to do "look for an unqualified function and call it statically"

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:26):

well, and that's a subset of this

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:26):

in that in general in Roc if I write the expression (foo), foo can certainly happen to be a function

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:26):

Since it's a pure lookup, it'd constant eval to a static dispatch, sure

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:26):

yeah exactly

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:27):

so foo.(bar)(baz) would just be syntax sugar for bar(foo, baz)

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:28):

it wouldn't involve the type checker at all

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:28):

and then bar can be an arbitrary expression if you like

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:28):

Yeah, it works, but I think pipes will still be really common for structural types at the app level and that syntax looks pretty alien to me.

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:29):

welllllllll... is there a reason we couldn't just keep |> in addition to methods?

pipes =
    data
    .method()
    |> List.map(x -> x + 2)

-- unqualified
pipes =
    data
    .method()
    |> map(x -> x + 2)

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:29):

I'm okay with your suggestion because:

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:30):

But I'd rather have something that communicates our goal of "pass this to a local function statically"

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:30):

the problem with |> is: what if you want to keep calling methods afterwards?

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:31):

kinda need parens and dots to have one long chain which intermingles method calls and direct function calls

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:31):

pipes =
    data
    .method()
    |> map(x -> x + 2)
    .length()

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:32):

This requires newlines to have a specific meaning

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:32):

yeah

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:32):

looking at that, I'd expect the formatter to move .length() to the previous line :big_smile:

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:32):

It also doesn’t have the nice autocomplete experience, I think we want . on the first char

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:33):

true

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:33):

if the only downside of .(foo)(bar) is that it looks weird, I wonder if we'd just get used to it

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:33):

That's pretty much it, yeah

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:33):

Oh, we'd get used to it

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:33):

like the semantics feel straightforward, it has the nice autocomplete experience

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:34):

My worry is that newcomers would find it "icky"

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:34):

"Why can't I just put the function name"

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:34):

it might look funny to beginners but I don't think it would come up that often - like I went through all of rocci bird and it only came up once or twice that I wanted it

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:34):

all the others became either methods or direct calls

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:35):

(e.g. bar(foo, baz))

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:37):

So to beat a horse to death: if we can't find a syntactically ambiguous version of .. between list/record spreading and function calls, that's still probably better for cleanliness and newcomers in my mind.

And I know it breaks the 3 char operator rule, but this problem goes away if we use what basically every other language uses for spreads, which is ...

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:38):

I'd say that single . has multiple meanings as well, we're just used to them:

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:39):

So having .. mean "spread" and "local function call" is not so bad

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:40):

I don't remember what language already uses .. for function calls almost exactly like this (maybe Agus does), but there's precedent for this

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:40):

Let me look through the PL subreddit

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:41):

I think you mean Dart, but that’s a little different

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:42):

(deleted)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 14:42):

https://dart.dev/language/operators#cascade-notation

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:42):

Yep, that's what I tried to link

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:42):

It's different, yep

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:42):

So not precedent

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:43):

I think it might be valuable to support foo.(bar)(baz), but I don't imagine that's almost ever good to write with a complex expression for bar

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:44):

foo.(if x then bar else baz)(123) screams code golf to me

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:52):

same here at first glance, although I suppose I could imagine in a vertical pipeline maybe it could make sense legitimately in some circumstance?

view this post on Zulip Richard Feldman (Nov 09 2024 at 14:52):

very rarely if at all though

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:54):

shrug

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:56):

I keep thinking back to that example in the doc:

[-2, 0, 2].map(.abs().sub(1))

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:56):

This is all ways to "call a chain of functions"

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:57):

I think foo..bar(123) does that, I think foo.(bar)(123) does that with a slight hiccup

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:58):

So either of these is still an improvement to Roc for sure, but I'd rather find a solution that visually implies "simple chain of function calls"

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:58):

I think foo|bar(123), foo..bar(123), foo.>bar(123) all do that

view this post on Zulip Sam Mohr (Nov 09 2024 at 14:59):

I think I've made my point, I'm going to go to bed

view this post on Zulip Richard Feldman (Nov 09 2024 at 15:00):

yeah I dunno, to me both foo..bar(123) and foo.(bar)(123) look strange, just in different ways

view this post on Zulip Richard Feldman (Nov 09 2024 at 15:00):

neither feels like it's obviously the way to go

view this post on Zulip Sam Mohr (Nov 09 2024 at 15:00):

Still definitely an exciting change, any of these make me confident I could go up to someone that's only written code a couple times and get them to understand how things work

view this post on Zulip Sam Mohr (Nov 09 2024 at 15:00):

I think I just don't like parentheses

view this post on Zulip Sam Mohr (Nov 09 2024 at 15:00):

haha

view this post on Zulip Sam Mohr (Nov 09 2024 at 15:00):

Richard Feldman said:

neither feels like it's obviously the way to go

Totally agree

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 15:03):

foo.>bar(123) looks a bit more intuitive to me

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 15:46):

I like foo.>bar(123)
We could also do foo.|bar(123) which evokes the unix pipe and still has autocomplete on .

view this post on Zulip jan kili (Nov 09 2024 at 15:47):

.> seems optimal for communicating "now pipe without interrupting the method chain", if that's our intent

view this post on Zulip jan kili (Nov 09 2024 at 15:48):

Less notably, it kind of evokes a terminal prompt, which is appropriate for finding functions in the current namespace.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 15:52):

We also have a sign from the universe: on a US keyboard layout, > is shift + .

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:02):

definitely an option, but I'd like to keep exploring....> still looks weird to me haha

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:03):

it visually reminds me of the kind of custom operator I'd see in like a Haskell or Scala DSL or alternative stdlib :sweat_smile:

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:03):

It's not vertically balanced

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:03):

I wonder about like .= maybe

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:04):

like foo.=bar(baz)

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:04):

eh that feels like assignment

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:04):

@ and $ are free, single char symbols

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:04):

And neither mean anything in Roc

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:05):

. auto-completes methods, $ does functions

view this post on Zulip jan kili (Nov 09 2024 at 16:05):

Is the plan to keep |> with the nearly identical meaning?

view this post on Zulip jan kili (Nov 09 2024 at 16:06):

Otherwise, how is existing whitespace syntax preserved as an equal alternative?

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:07):

I would vote for removing the whitespace syntax

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:07):

Because it's better to have one way to do things

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:09):

The exception would be if we could start a whitespace block, aka

component = props -> do:
    use whitespace syntax

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:09):

Any way to denote "this is all whitespace, and it's on purpose and special"

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:10):

Richard Feldman said:

the problem with |> is: what if you want to keep calling methods afterwards?

@JanCVanB this is why pipelines can't stay in their current form

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:11):

@Agus Zubiaga message received lmao

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:11):

let's hold off on discussing "whitespace calling vs commas and parens" until later

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:11):

I agree we shouldn't do it

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:12):

certainly there will be a world where both coexist, for backwards compatibility, then once we're in that world we can experiment with having the formatter enforce one rule or the other and see how things look

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:12):

then have a more informed discussion with that info

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:14):

I really feel like trying to chain local functions or imported functions in some syntax that is shoved into dot based calling is a mistake. I think it eats into the weirdness budget and is harder to reader.

Either you define a method on a nominal type which is callable via x.method(...) or you define a function which will require calling with function(x,...) or `x |> function(...)

Other languages have perfectly fine syntax without needing something super strange like x.(function(...)).

This feels like we are prematurely trying to solve a problem that doesn't exist.

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:15):

That or we accidentally added a problem into roc that somehow isn't a problem in other languages

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:15):

interesting, I hadn't considered that possibility!

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:15):

yeah my general assumption was like "yeah of course we'll want that" but admittedly I've never wanted it in Rust or JavaScript

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:17):

definitely the easiest design to try out is "hold off on implementing it and see how much demand there is in practice first"

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 16:17):

I disagree. I love that |> allows me to conveniently pipe things I don’t control into my own functions. That’s something I really miss in mainstream languages.

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 16:17):

Some languages tackle this problem by allowing you to extend classes from the outside, but I don’t love that

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:17):

Oh, for sure keep |>. It is great for removing nested parens and making code more readable

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:18):

Just don't try to jam an extra variant of pipe into the dot calling syntax. Use pipe. It already exists

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 16:18):

Yeah, but in a world where most things in the stdlib are nominal types with methods, |> won’t compose nicely

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:19):

I think the better statement is that pipe generally isn't needed. Which is why I think for most mainstream languages it isn't added. A lot less pressure when dot syntax works for almost wverything

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:20):

There is nothing wrong with

y = x.method(...) |> function(...)

view this post on Zulip jan kili (Nov 09 2024 at 16:21):

(where function is a method and method is a local function?) heehee edited

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:23):

I get it isn't perfect (cause occasionally you will need a nested lambda to deal with things), but it is still better than any mainstream language. I think it solves most of the need. You also could allow defining local methods if you really wanted. They would just need unique names compared to the imported list of methods for a nominal type (though this can get hairy).

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:24):

@Brendan Hansknecht I agree, the only cost is that this elision of types that comes from the method paradigm is lost once you pipe to other functions. It seems like .> allows that, which is why we're trying to find a nice looking version of it.

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:24):

But I agree that it's minor

view this post on Zulip jan kili (Nov 09 2024 at 16:25):

Arguably breaking up the method dot chain with a pipe operator is a good thing, because I imagine it will usually correlate with a switch from one type to another. For example, |> Str.fromUtf8 is where the Str party starts.

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:25):

I'm not sure I follow. How does this change anything about the types?

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:27):

I think Jan is working under the assumption that most methods act like builder methods, where they take and return the same custom type

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:27):

But if that's not the case, then the types change a good deal

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:27):

Oh also, with pipe, this should be valid right?

list.method1().method2(123) |> localfn().method3()

view this post on Zulip jan kili (Nov 09 2024 at 16:27):

I think Brendan and I were both replying to a few messages prior heh

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:28):

Brendan Hansknecht said:

Oh also, with pipe, this should be valid right?

list.method1().method2(123) |> localfn().method3()

This implies that you're calling a method on the return value of localfn, since whitespace binds weaker than .

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:29):

It's clearer when you put newlines in this chain, but we shouldn't treat newlines special here IMO

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:30):

If we use .> sans spaces instead of current |> the problem disappears

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:30):

I think that should desugar to

localfn(list.method1().method2(123)).method3()

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:30):

That's what it would need to desugar to, yes

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:31):

So why do we need .>?

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:31):

They would just desugar to the same thing.

view this post on Zulip jan kili (Nov 09 2024 at 16:32):

To adapt Richard's last examples:

pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)
pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)
vs.
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)

both seem fine as

pipes = prev.pipes |> updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes |> Pipes.update("extra argument").appendIfOk(pipe)

though now I think this prevailing "local function" example looks more like it would actually be a method in this particular case

pipes = prev.pipes.update("extra argument").appendIfOk(pipe)

so maybe we need a new canonical syntax bikeshedding example :stuck_out_tongue_wink:

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:34):

Brendan Hansknecht said:

They would just desugar to the same thing.

The whitespace is the problem. Though I'd be okay with this, it's still a misleading visual grouping

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:34):

@JanCVanB sounds like you're on the same page as Brendan?

view this post on Zulip jan kili (Nov 09 2024 at 16:35):

Sam Mohr said:

This implies that you're calling a method on the return value of localfn, since whitespace binds weaker than .

Wait, isn't that the goal?

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:36):

Oh, so no type problem. I guess I am still confused by your comment on "the only cost is that the Ellison of types that comes from the method paradigm is lost on e you pipe to another function".

I don't understand how anything changes from .> to |> that would affect types.

view this post on Zulip jan kili (Nov 09 2024 at 16:36):

Sam Mohr said:

JanCVanB sounds like you're on the same page as Brendan?

I think so, but I'm getting confused too

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:36):

pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)

I guess this could be expressed using pipe and parens:

pipes = (prev.pipes |> Pipes.update("extra argument")).appendIfOk(pipe)
pipes = (prev.pipes |> updatePipes("extra argument")).appendIfOk(pipe)

but the parens would mess up a multiline version of this

view this post on Zulip jan kili (Nov 09 2024 at 16:37):

Why are the outer parens necessary?

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:40):

Brendan Hansknecht said:

Oh, so no type problem. I guess I am still confused by your comment on "the only cost is that the Ellison of types that comes from the method paradigm is lost on e you pipe to another function".

I don't understand how anything changes from .> to |> that would affect types.

It doesn't. I'm saying that the |> function().method3() example looks like it's calling method3 on the function result, so it is currently disallowed and you'd only be able to call functions once the |> operator is used in a pipeline to mitigate that confusion

view this post on Zulip jan kili (Nov 09 2024 at 16:41):

...but it is calling method3 on the function result, right?

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:41):

But since .> is a dot operator, it's not confusing so we'd allow method calls to follow function calls

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:41):

However, if you remove that assumed-necessary restriction, they're the exact same thing

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:41):

Oh (x.method() |> func()).method() vs x.method() |> (func().method())

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:42):

The second version where func has no arms.

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:42):

Exactly

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:42):

.> is unambiguous, |> is ambiguous

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:43):

It isn't ambiguous. It is a precedence question

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:44):

Sure

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:45):

I kinda assumed . is basically a form of |>. So same precedence

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:45):

I think we'd be keeping |> because it's familiar at that point, even though it looks further from most of our function call syntax

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:46):

I'd prefer something that looks similar to .

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:46):

.| or .. or .> do that

view this post on Zulip jan kili (Nov 09 2024 at 16:46):

If this is just about making whitespace visually match precedence, then don't we already allow the same issue with 1 |> add(2)/3? (lame example, can't think of a good one, maybe that means I'm wrong hehe)

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:47):

I don't think that's allowed?

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:47):

That would be (add(1, 2))/3

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:48):

What do languages with universal method calling syntax do to deal with these issues? They theoretically support calling any function as a method with dot syntax.

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:48):

They don't support this

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:48):

We're trying to do things with the same syntax: call local functions and call methods

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:49):

UFCS picks one of those to my understanding

view this post on Zulip jan kili (Nov 09 2024 at 16:49):

Sam Mohr said:

That would be (add(1, 2))/3

just like x.m1() |> f().m2() would be f(x.m1()).m2() as we want it to be

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:49):

This isn't a thing in other languages

view this post on Zulip Sam Mohr (Nov 09 2024 at 16:50):

@JanCVanB the precedence is different for math operators, but sure

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:51):

Example from d that I think would definitely hit this issue. So I think they need to have a solution to the problem:

int first(int[] arr)
{
    return arr[0];
}

int[] addone(int[] arr)
{
    int[] result;
    foreach (value; arr) {
        result ~= value + 1;
    }
    return result;
}

void main()
{
    auto a = [0, 1, 2, 3];

    // all the following are correct and equivalent
    int b = first(a);
    int c = a.first;

    // chaining
    int[] e = a.addone().addone();
}

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:51):

Oh nvm... I already know D's solution and we won't like it

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:52):

All functions in one global name space...no modules (though I should double check this)

view this post on Zulip jan kili (Nov 09 2024 at 16:52):

(going afk for activities, :heart: byeee)

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 16:55):

Oh, actually, D has proper modules. So maybe they have a real solution to this.

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:56):

I don't think we can reasonably define |> to have the same precedence as . - that would be really confusing :sweat_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:56):

foo.bar() + baz.blah()
foo.bar() |> baz.blah()

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:57):

I think if these two expressions have difference precedence on the operator in the middle, that's really surprising

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:57):

er, precedence with respect to .blah()

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:57):

like, this is really surprising:
foo.bar() + baz.blah() == (foo.bar()) + (baz.blah())
foo.bar() |> baz.blah() == (foo.bar() |> baz).blah()

view this post on Zulip Richard Feldman (Nov 09 2024 at 16:58):

I don't think anyone would expect that to be how it worked :big_smile:

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 17:01):

So for D to allow local functions to use dot syntax. They basically have a precedence rule for which function to pick. If a class defines a method direct (in roc's case, this would be if it was defined and exposed from the same module as the nominal type), it is picked first. After that, a local function can be used with the method call syntax.

...

I feel like that would not be nice to work with....

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 17:02):

Yeah, that’s why having a separate operator makes sense to me

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 17:03):

and I don’t think we need to keep |> if we have .>

view this post on Zulip Sam Mohr (Nov 09 2024 at 17:09):

I wonder if just > could work: if > is followed directly by an ident, it's a function call, otherwise it's the comparison operator

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:10):

ok so to summarize some of the options:

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:10):

dot-parens

pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .(updatePipes)("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .(Pipes.update)("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:10):

dot-dot

pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes..Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    ..updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    ..Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:11):

dot-greater-than

pipes = prev.pipes.>updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.>Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .>updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .>Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 17:13):

I vote dot parens. It feels like you are modifying the lambda to make it work in a dot chain.

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 17:14):

Dot dot looks like a bug/typo

view this post on Zulip Sam Mohr (Nov 09 2024 at 17:14):

I vote dot dot. It looks the nicest to me :point_right::point_left:

view this post on Zulip Kevin Gillette (Nov 09 2024 at 17:14):

x.foo is still a record field access; I can tell x is a record just from looking at this.
x.foo(y) is now a statically-dispatched method call, which I can also tell just from looking at it.

What is x.foo is a field of function type? How would I be able to call it?

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:14):

that's explained in the next paragraph or so

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:15):

(x.foo)(bar)

view this post on Zulip jan kili (Nov 09 2024 at 17:15):

I object that this sample from Rocci Bird is the canonical example of needing to pipe in the middle of the method chain. It overemphasizes that piping should feel method-y because updateThings/Things.update takes a List Thing as its first argument and that feels a lot like taking Thing as its first argument. I imagine that most mid-chain piping will be to change types entirely.

view this post on Zulip jan kili (Nov 09 2024 at 17:16):

also lol I'm back my first activity canceled

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 17:16):

You can change types without using pipe

myStr.toUtf8().somlistMethod()

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:17):

yeah this is definitely 100% unrelated to types :big_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:17):

nothing about this discussion changes no matter what the return types and/or argument types in question are

view this post on Zulip jan kili (Nov 09 2024 at 17:17):

okie dokie nvm :)

view this post on Zulip jan kili (Nov 09 2024 at 17:21):

Richard Feldman said:

that's explained in the next paragraph or so ... (x.foo)(bar)

I want to resist extra parens in Roc, yet I'm warming up to .(up) over .>up/..up, partly because it would be consistent with this (x.foo) syntax - identify a function by putting parens around its qualified name. It also feels the most like "hey Roc please treat this function like a method" before any explanation is provided.

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:26):

yeah I still don't love the way it looks, but I do like the explanation for .(foo)(bar) the most

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:26):

it's like, it's the same thing as .foo(bar) except you're substituting an arbitrary expression for the name foo

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:26):

it sort of "feels right" but doesn't "look right" :stuck_out_tongue:

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:26):

to me

view this post on Zulip jan kili (Nov 09 2024 at 17:27):

Maybe it will feel right after we introduce the many parens this proposal requires anyways!

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:27):

in comparison to .foo(bar) vs ..foo(bar) or .>foo(bar) where foo means something completely different in the first example vs the other two, but it's still kinda in the same position if that makes sense

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:28):

but also I don't really love how any of them look :sweat_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:28):

I think that's what makes me gravitate towards dot-parens - I'm not thrilled with how any of them look, but that one stands out for feeling like it has the best explanation for what's happening

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:29):

if we found something that just looked awesome, that would be a different story though

view this post on Zulip jan kili (Nov 09 2024 at 17:30):

Parens are like JSON - simultaneously an eyesore and hyper readable

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:32):

I think syntax highlighting makes a big difference...when punctuation like dots and parens are kind of a low-contrast gray, they fade into the background more easily (while still being visible as delimiters if you're looking for them to figure out where things start and end)

Screenshot 2024-11-09 at 12.32.03 PM.png

view this post on Zulip jan kili (Nov 09 2024 at 17:33):

Side benefit of .(up) - while it technically breaks the vertical alignment of the method chain by inserting an extra character like all options above... it doesn't FEEL like it is, because we know from other languages to visually anchor on the left paren as the start of the phrase.

view this post on Zulip jan kili (Nov 09 2024 at 17:35):

Visualized:

pipes =
    prev
    .pipes
    .>Pipes.update("extra argument")
#     ^-- start of the phrase
    ..Pipes.update("extra argument")
#     ^-- start of the phrase
    .(Pipes.update)("extra argument")
#    ^-- start of the phrase
    .appendIfOk(pipe)

I also expect that, just like in this code snippet, every editor will color parens differently than ./>, which is good here to preserve vertical alignment. (Maybe not always the same color as text like here, but still different than operators.)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 17:35):

Why doesn’t .> feel right but |> currently does? As it was pointed out before, it’s basically the same thing with different precedence

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 17:37):

Sure you might have to look it up to understand, but I don’t think most people would get the parens syntax immediately either

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:40):

Agus Zubiaga said:

Why doesn’t .> feel right but |> currently does? As it was pointed out before, it’s basically the same thing with different precedence

trying to unpack my own instinctual impression, I think it's because when I think about foo.bar, foo.>bar, and foo |> bar, in the first two cases the dot makes me think "this is looking something up on foo named bar" because that's what it means when there's a dot immediately following something

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:41):

and I think the reason foo.(bar) doesn't make me think that (as much) is that when something is surrounded in parens, that pretty much always means "this is its own self-contained thing"

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:41):

so I think (bar) in an expression refers to the expression bar

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:41):

(which is correct in this case)

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:41):

all that said, I think I could get used to any of these

view this post on Zulip jan kili (Nov 09 2024 at 17:42):

No matter what option we go with, there will be situations where it will feel more/less obvious to more/less experienced users. The canonical example we're using now anchors us to think of its use in the context of a longer chain with methods before and after, but what if the first ten experiences someone has with it are just foo.>bar(baz) by itself? We can say "well why wouldn't they just write it like bar(foo, baz)", but we're introducing the capability, and piping is stylistically enticing in general.

Possible horrors:

(a, b) = c((d, e)).(f)(g).h(i.(j))
a = [b..c(d.e(f..g)...h.i]         # assuming we rename .. to ...
a = \b -> b.>c.>d > b.>(\e -> e.>f)

(sorry for editing this indefinitely, it's just too much fun)

In other words, I think no matter the syntax, introducing a . version of |> will enable both more elegant code and more horrific code. It would be up to us to shield new users from pain.

view this post on Zulip Richard Feldman (Nov 09 2024 at 17:56):

the middle one there looks the most horrifying to me haha

view this post on Zulip Brendan Hansknecht (Nov 09 2024 at 17:59):

First line is definitely most readable to me of those. Though it is a bit of apples and oranges all with the worst syntax

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:11):

For example, it's common in many languages to have a function like Set.add.
(We happen to call it Set.insert, but Set.add was considered too.) If that's
the best name for your function, is it okay that + now Just Works on your type,
even if that usage doesn't make sense? It's a valid question. Also, the + operator
in arithmetic is commutative, but nothing says a.add(b) has to be
commutative; b doesn't even have to be the same type as a!

What about a new operator ++ that desugars to append?

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:11):

this might end up being a situation where we try out one thing and then reconsider later. fortunately, the semantics wouldn't be affected

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 18:12):

.> is my favorite by a large margin

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:13):

Derin Eryilmaz said:

For example, it's common in many languages to have a function like Set.add.
(We happen to call it Set.insert, but Set.add was considered too.) If that's
the best name for your function, is it okay that + now Just Works on your type,
even if that usage doesn't make sense? It's a valid question. Also, the + operator
in arithmetic is commutative, but nothing says a.add(b) has to be
commutative; b doesn't even have to be the same type as a!

What about a new operator ++ that desugars to append?

that's interesting! I thought originally it didn't seem worth it, but in the context of operator overloading the tradeoffs are different - e.g. having ++ would discourage overloading + for that operation

view this post on Zulip jan kili (Nov 09 2024 at 18:14):

I only just now realized that .>foo(bar) isn't "piping" any more than .foo(bar) is - it's just a question of namespaces... Both are new pipe-like operators. That makes it feel weird to give one of them the > from |> but not the other one. My .(foo)(bar) preference is getting stronger.

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:15):

.>bar and .(bar) would do the same thing as |> bar today, just with different precedence

view this post on Zulip jan kili (Nov 09 2024 at 18:16):

Richard Feldman said:

.>bar and .(bar) would do the same thing as |> bar today, just with different precedence

Yes, but critically: .bar would do the same thing too (the function just happens to live somewhere special). In other words, . alone is now a pipe.

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:17):

we should consider the rest of the ._ family besides .>, e.g..:, .$, .<, etc

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:17):

like maybe it's the best of that bunch, but I'd like to see how the others look in examples to compare

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:17):

(I'm on mobile now, so hard for me to do!)

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:20):

.! would look hilarious with effectful functions - foo.!bar!(baz)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:20):

i like .$ in line with "$(str)" interpolation

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:21):

you're inserting the function

view this post on Zulip jan kili (Nov 09 2024 at 18:21):

JanCVanB said:

Yes, but critically: .bar would do the same thing too (the function just happens to live somewhere special). In other words, . alone is now a pipe.

Now that I've realized this, I strongly oppose introducing .> because it would communicate to learners that .> is doing more |> than . is.

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:21):

Derin Eryilmaz said:

you're inserting the function

instead of inserting a string into another string, you're inserting a function into the call chain

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:21):

makes sense to me

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:22):

Richard Feldman said:

dot-greater-than

pipes = prev.pipes.>updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.>Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .>updatePipes("extra argument")
    .appendIfOk(pipe)

pipes =
    prev
    .pipes
    .>Pipes.update("extra argument")
    .appendIfOk(pipe)

can we get some more examples like this with other operator styles? e.g. .$

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:23):

(from anyone who is at a keyboard right now :big_smile:)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:23):

sure

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:24):

huh somehow my formatting isnt working

view this post on Zulip jan kili (Nov 09 2024 at 18:24):

Note: Richard's been using python syntax highlighting

view this post on Zulip jan kili (Nov 09 2024 at 18:25):

'''python
code
'''

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:25):

I used perl above

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:25):

just bc it highlighted .> as one operator

view this post on Zulip Anton (Nov 09 2024 at 18:25):

Maybe the ## is interfering?

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:25):

what..

view this post on Zulip jan kili (Nov 09 2024 at 18:26):

Not attacking Derin - Will this topic break any records for length in this Zulip? :laughing:

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:26):

not yet haha

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:26):

pretty sure we've already had longer ones

view this post on Zulip jan kili (Nov 09 2024 at 18:28):

Derin, want me to tag in? (assuming I have any better luck than you)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:28):

can someone else please try writing it out? sorry my formatting isnt working and idk why

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 18:28):

Syntax discussions are the real Zulip stress test

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:28):

Derin Eryilmaz said:

i like .$ in line with "$(str)" interpolation

I guess the most direct translation of that would be foo.$(bar)(baz)

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:29):

that one might be the absolute easiest to explain haha

view this post on Zulip jan kili (Nov 09 2024 at 18:30):

dot-dollar

pipes = prev.pipes.$updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip jan kili (Nov 09 2024 at 18:30):

I'm not endorsing this (yet?), just being our hands.

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:31):

dot-dollar-with-parens

pipes = prev.pipes.$(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$(Pipes.update)("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$(updatePipes)("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$(Pipes.update)("extra argument")
    .appendIfOk(pipe)

adding what richard suggested

view this post on Zulip jan kili (Nov 09 2024 at 18:31):

AYO, Derin in the formatting house

view this post on Zulip jan kili (Nov 09 2024 at 18:32):

dot-colon

pipes = prev.pipes.:updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.:Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .:updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .:Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:32):

Derin Eryilmaz said:

adding what richard suggested

not necessarily a suggestion, just a thought :big_smile:

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:33):

Fair enough, it is my favorite so far though because of its similarity to string interpolation

view this post on Zulip jan kili (Nov 09 2024 at 18:33):

dot-less-than

pipes = prev.pipes.<updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.<Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .<updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .<Pipes.update("extra argument")
    .appendIfOk(pipe)

imma say right now, ew

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:34):

that one at least has more vertical smoothness than .>

view this post on Zulip jan kili (Nov 09 2024 at 18:34):

dot-vert

pipes = prev.pipes.|updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.|Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .|updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .|Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip jan kili (Nov 09 2024 at 18:34):

(less ew but I still object for the same reason I objected to .>)

view this post on Zulip jan kili (Nov 09 2024 at 18:36):

My clipboard is ready, if anyone wants to shout ascii characters...

view this post on Zulip jan kili (Nov 09 2024 at 18:39):

Honestly... .$ is really smooth. It makes it clear the distinction is namespacing, not piping. And it leverages an association used in both Roc and other langs.

view this post on Zulip Isaac Van Doren (Nov 09 2024 at 18:40):

.> is still my favorite but I would be happy with any ._ variety

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:41):

Here's the previously mentioned "horrifying syntax", tried with $()

(a, b) = c((d, e)).$(f)(g).h(i.$(j))

view this post on Zulip jan kili (Nov 09 2024 at 18:42):

but without parens:

(a, b) = c((d, e)).$f(g).h(i.$j)

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:44):

if we went with that one, we could say the parens after the $ are optional

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:45):

so explain it exactly like string interpolation, but explain that the parens are optional unlike in string interpolation

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 18:45):

$ looks like it’s part of the name to me, but maybe I’m just scarred from jQuery and PHP

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:46):

yeah I feel that way a little bit too, although in those cases there's never a . before the $

view this post on Zulip jan kili (Nov 09 2024 at 18:46):

Optional parens could help with module qualification looking more natural sometimes
foo.$Bar.baz(a).b.c vs foo.$(Bar.baz)(a).b.c

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:46):

I think the parenthesis should be necessary at least if you're going to reference a moduled function like foo.$(Thing.test)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 18:46):

otherwise foo.$test should be fine

view this post on Zulip jan kili (Nov 09 2024 at 18:46):

jinx

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:47):

fwiw I think module qualification would be a rare use of this

view this post on Zulip jan kili (Nov 09 2024 at 18:48):

because aside from .fromFoo functions, modules rarely take another type as their first arg?

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:48):

yeah

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:48):

I could see it in like test helpers maybe

view this post on Zulip Richard Feldman (Nov 09 2024 at 18:49):

but locally defined functions would be the most common by far, I'd expect

view this post on Zulip jan kili (Nov 09 2024 at 18:51):

Yeah, and the optional parens will be great for lambdas, though hopefully many most lambdas will be rendered moot by point-free fun.

view this post on Zulip Richard Feldman (Nov 09 2024 at 19:35):

dot-dollar

pipes = prev.pipes.$updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$Pipes.update("extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$updatePipes("extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .$Pipes.update("extra argument")
    .appendIfOk(pipe)

view this post on Zulip Richard Feldman (Nov 09 2024 at 19:36):

an interesting observation about this is that the fact that various languages treat $foo as a variable makes this maybe look more approachable to beginners

view this post on Zulip Richard Feldman (Nov 09 2024 at 19:37):

like they might incorrectly guess what it's doing, but their first impression might be more "this looks familiar" than "this looks alien"

view this post on Zulip Richard Feldman (Nov 09 2024 at 19:50):

looking back at each of the examples, .$ now looks the least strange to me. it's my new frontrunner

view this post on Zulip Kevin Gillette (Nov 09 2024 at 20:23):

In the expression !foo.bar("hi"), the type of foo would be inferred as:

a where a.bar(Str) -> Bool

You can read this as "some value (whose type has been named a) that has a method named foo which returns a Bool and takes a Str as its second argument." (The first argument in a method is always the value it's being dispatched on.)

Should this be "has a method named bar" ?

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 21:01):

.$ looks a lot less intuitive to me, but at least it’s concise, so I’ll take it :grinning:

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 21:42):

Is my proposal still in the mix? I had the sense it had the same benefits as the original proposal but without introducing new syntax weirdness, but curious if I overlooked something.

To reiterate, I propose replacing |> with ., but otherwise not changing what the operator does. The benefit here would purely be easy-to-typeness. That would look like this:

pipes =
    prev.pipes
    . Pipes.update "extra argument"
    . List:appendIfOk

Module/exposed separators would become something else, could be : as in above, .:, or something else, to avoid ambiguity with the new . operator.

Then optionally the module qualifier can be elided to let the compiler use the same qualifier as the one of the previous method in the chain, so you can also write this:

pipes =
    prev.pipes
    . Pipes.update "extra argument"
    . _:appendIfOk

That would imply there's two different concepts to learn, one is ./|> which works exactly as it does today, the other is qualifier-elision using _ (or some other operator). My sense is that smooshing the two together creates a difference between method and function calls, while the above avoids that by just having function calls but keeping all the autocomplete benefits.

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:19):

hm, I think if we started with parens-and-commas and then proposed that syntax, we'd probably conclude it wasn't worth the strangeness budget cost :big_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:20):

but I'm curious what others think!

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:21):

it just occurred to me that this is an option:

.call

pipes = prev.pipes.call(updatePipes, "extra argument").appendIfOk(pipe)
pipes = prev.pipes.call(Pipes.update, "extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .call(updatePipes, "extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .call(Pipes.update, "extra argument")
    .appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:22):

Would it have to defined manually or does every type get it automatically?

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:23):

I'm not sure if .call is the right name, but the idea would be that this is a reserved method name which the type-checker is aware of, and would apply constraints the way you'd expect (like a normal direct call)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:23):

I think you’d need different names for the different number of args

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:23):

yeah every type would get it automatically, just like Inspect

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:23):

nah that could Just Work

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:24):

Ah, so is kinda how crash isn’t really a function but looks like one

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:24):

right

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:24):

it looks like a method but it's actually just a special case in the compiler

edit: specifically it would be treated by the compiler as syntax sugar for a normal function call, just like |> is today, so it would have been desugared before type-checking even began, and it wouldn't add any extra type constraints to anything compared to a direct call

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:25):

Would it work for structural types too?

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:25):

might read better as .passTo

.passTo

pipes = prev.pipes.passTo(updatePipes, "extra argument").appendIfOk(pipe)
pipes = prev.pipes.passTo(Pipes.update, "extra argument").appendIfOk(pipe)
pipes =
    prev
    .pipes
    .passTo(updatePipes, "extra argument")
    .appendIfOk(pipe)
pipes =
    prev
    .pipes
    .passTo(Pipes.update, "extra argument")
    .appendIfOk(pipe)

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:25):

Or .pipe?

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:26):

maybe, although I think we need a different example to see how that one reads

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:26):

otherwise it's like pipes.pipe(updatePipes, ... :laughing:

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:26):

what I like about this general idea is that it looks totally normal

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:27):

I don't think any beginner would look at prev.pipes.call(updatePipes, "extra argument") and be like "what is that doing?!?!?!"

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:27):

Yeah, that’s a good point

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:28):

and it wouldn’t be the first time we do something like that

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:33):

I do like that .passTo precisely describes what it's doing, although I wish there were a single word that was as self-descriptive :big_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:34):

.call isn't quite as accurately self-descriptive because you're not calling the subject itself

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:34):

.apply?

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:35):

.pipe feels like it needs more explanation than .passTo

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:35):

yeah same with .apply

view this post on Zulip Agus Zubiaga (Nov 09 2024 at 22:36):

.forward? idk

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:37):

so far of all the options we've discussed in this thread, I like .passTo the best because:

view this post on Zulip Richard Feldman (Nov 09 2024 at 22:38):

it's less concise than the operators but I'm fine with that given the other tradeoffs involved

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 22:42):

I don't know about all this--I really like the fact that roc has a lot of whitespace. A . isn't strictly necessary for autocomplete to exist. I also feel like we shouldn't be making this syntax easier/more visually pleasing than explicit module qualification for every function call, because that encourages importing tons of functions, which could be pretty bad. And this also makes it hard to decipher types at times (if you're reading code without an IDE).

That said, I think the power of this static dispatch idea is allowing a nicer form of abilities, while also allowing limited operator overloading.

I like what Jasper proposed earlier. here's my (similar) syntax idea:

fn = \header ->
    when headers | _keepIf \{ name } -> name == "cookie" is
        [reqHeader] ->
            reqHeader.value
            | Str.fromUtf8
            | _try \s -> s | _split "=" | _get 1
            | _try Str.toI64
            | _mapErr \_ -> BadCookie
        _ -> Err NoSessionCookie

I just put a singular pipe char as an operator; that part of it could change.

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:00):

I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:01):

but I appreciate that different people may assign more or less value to the aesthetic delta there, based on personal preference :big_smile:

view this post on Zulip Luke Boswell (Nov 09 2024 at 23:02):

Richard Feldman said:

I do like that .passTo precisely describes what it's doing, although I wish there were a single word that was as self-descriptive :big_smile:

The first thing that came to mind for me was .next(..)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 23:11):

Richard Feldman said:

I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"

If we decide not to switch to the dot syntax with parentheses and commas, I think adding an underscore before function names that should be statically dispatched is probably the best approach to implement this:

foo |> _fn bar

foo |> _.fn bar is logical too but the extra dot is just a bit too clunky imo.

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 23:16):

I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"

The aesthetic delta is definitely part of it. I do believe it would probably help adoption if the syntax were more C-like, though it hurts a bit to say it :sweat_smile: . That said, wouldn't that argument apply just as easily to many other things, like wrapping blocks in {}?

But it's not just the aesthetics for me:

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:22):

Jasper Woudenberg said:

I like the default-explicit prefixes. I wonder if omitting module names by default will result in function names getting longer, to add extra context. Also, I use Vim and don't have type hints set up currently. Github PR diffs don't either. New programmers might not necessarily have all the best tooling set up either.

I also like them, although there is a tradeoff here - someone (I think @Aurélien Geron if I remember right?) mentioned that a current downside of Roc compared to (for example) Python is that when you're doing things like quick scripts and exploratory programming (e.g. data science, where you're doing relatively more writing code compared to reading it later), which are domains where conciseness is disproportionately valuable, it's an advantage to Python that you have the ability to write these things more concisely

view this post on Zulip Jasper Woudenberg (Nov 09 2024 at 23:24):

Don't you get the write-speed improvements with auto-complete though?

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:24):

Jasper Woudenberg said:

The proposal introduces a difference between method and function calls. I love that in Roc currently and Elm there is no such difference. I think it's also easier to explain one calling convention over two.

in fairness, I think this is actually more about aesthetics than it might appear.

today we have direct function calls and ability calls, which use type information to look up which specific function implementation to use. In the proposal, we have the same split, just with "methods" instead of "abilities" being the ones that use type information to look up which specific function implementation to use :big_smile:

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:25):

Jasper Woudenberg said:

Don't you get the write-speed improvements with auto-complete though?

to some extent, although you'd need an autocomplete that would automatically fill in the module prefix I suppose (so that you could just type the name of the function, unqualified, like you can in the method-style design)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 23:28):

i would support the autocomplete doing that. i also think that, if we take that path, the dot syntax wouldn't look great

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 23:32):

also, another advantage of using foo |> _fn bar syntax:
_fn foo bar means the same thing, so you wouldn't be forced to write the function after the argument if using abilities

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:32):

something worth noting is that I have a lot of affection and nostalgia for whitespace calling, although I remember something Evan once said about Elm - "I didn't make this language to fight the Syntax Wars."

To him, that means having a syntax that's in the tradition of the ML family of languages that Elm is descended from, and not spending a lot of time deviating from that. But the same "not fighting the Syntax Wars" sentiment resonates with me in a different way - in that it does feel like the syntax being so different from what people are used to is holding back the other great ideas in the language from reaching a wider audience, and that creates tension in my mind with the feelings of affection and nostalgia for the current syntax

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:34):

which is not to say that I want to (for example) just make the syntax be a subset of C or Java or something, but rather acknowledging that some syntax changes are more or less discouraging to beginners than others (much as I wish it weren't the case), and part of being a beginner-friendly language is having a syntax that beginners find approachable

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:34):

for example, I don't think anyone looks at if/then/else and quietly closes the tab

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:34):

but whitespace calling does make the code look a lot different, and I know that's a barrier for people

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:35):

and that shouldn't be the only consideration, of course, but I do think it should be a consideration, and it deserves thought and weight alongside the pros and cons of other syntax considerations (e.g. the whitespace calling looking better in DSLs, as noted in the doc)

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 23:39):

Richard Feldman said:

which is not to say that I want to (for example) just make the syntax be a subset of C or Java or something, but rather acknowledging that some syntax changes are more or less discouraging to beginners than others (much as I wish it weren't the case), and part of being a beginner-friendly language is having a syntax that beginners find approachable

i guess you're using "beginner" here to refer to someone who's already used java or c--what if beginner means someone who's never coded before? I would argue whitespace syntax could be nicer to a lot of those people: it's far less overwhelming than a barrage of dots and parentheses :grinning_face_with_smiling_eyes:

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:39):

yeah, sorry - beginner in the sense of coming to Roc for the first time

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:40):

some percentage of beginners trying Roc for the first time will never have used any programming language, but I'd guess it would be less than 1% :big_smile:

view this post on Zulip Derin Eryilmaz (Nov 09 2024 at 23:40):

hey, i do think it would actually be a good first language--if only more people knew about it :grinning:

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:40):

it would be cool if someday that were different, but realistically the path to being that mainstream would involve first getting widespread adoption through the path of lots of beginners coming to the language with a background in other languages

view this post on Zulip jan kili (Nov 09 2024 at 23:51):

@Richard Feldman I have a serious but also :laughing: question for you: How much of this proposal and focus on autocomplete is influenced by your new (to me) role at an IDE company?

I've been here since when the editor was still a more active effort, so I know that Roc has always had UI in mind - I just want us to do a quick due diligence on conflict of interest. Please place your hand on the Tufte:

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:59):

oh almost none

view this post on Zulip Richard Feldman (Nov 09 2024 at 23:59):

we've talked about autocomplete for years

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:00):

also, one of the reasons I applied to Zed in the first place is wanting to learn about editors, and also how a really good (and open-source!) one works

view this post on Zulip Jasper Woudenberg (Nov 10 2024 at 00:01):

The ML syntax adding the the weirdness budget and that pushing some people away I get. It's a shame, but yeah, maybe not worth the fight.

Do you see removing default-qualifiers and replacing pizza-style function chaining with method chaining in a similar vein, ways to get to syntax more familiar to folks new to the language? Wondering if workshopping alternative syntax that makes pizza-chaining and module qualifier (elision) look more like mainstream languages would change things for you, or if those changes are less about the syntax for you.

Richard Feldman said:

Jasper Woudenberg said:

The proposal introduces a difference between method and function calls. I love that in Roc currently and Elm there is no such difference. I think it's also easier to explain one calling convention over two.

in fairness, I think this is actually more about aesthetics than it might appear.

today we have direct function calls and ability calls, which use type information to look up which specific function implementation to use. In the proposal, we have the same split, just with "methods" instead of "abilities" being the ones that use type information to look up which specific function implementation to use :big_smile:

Most of our ability calls are wrapped up. You use an operator like == and it calls eq for you behind the scenes but it's not something you're thinking a lot about. Similar with running decoders via Decode.fromBytesPartial. So I agree with the principle, but think in practice you can get by today without knowing the details of the calling conventions that exist, while with this proposal I think you'd need to actively understand these two calling syntaxes to be able to work with a codebase.

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:02):

good point about abilities!

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:02):

Jasper Woudenberg said:

The ML syntax adding the the weirdness budget and that pushing some people away I get. It's a shame, but yeah, maybe not worth the fight.

Do you see removing default-qualifiers and replacing pizza-style function chaining with method chaining in a similar vein, ways to get to syntax more familiar to folks new to the language?

yeah, that's why I started the doc out with the example of how the string replacing looks in mainstream languages

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:03):

to me, though, it makes a really big difference what the semantics of those calls are

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:03):

e.g. if it's an inheritance hierarchy vs. "we just call the function in the module where the subject was defined"

view this post on Zulip Jasper Woudenberg (Nov 10 2024 at 00:04):

Gotcha, that makes sense. How important is it that calling the function in the module where the subject was defined is the 'favored syntax', so to speak? Compared with say local module top-level functions or let bindings?

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:05):

I don't think it's necessarily crucial, but it does seem like in practice that's what tends to get used the most

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:06):

like how for example in Elm and Roc code bases it feels to me like most |> calls are qualified by module

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:06):

as opposed to local

view this post on Zulip Derin Eryilmaz (Nov 10 2024 at 00:07):

so, from what i can tell, the tradeoffs are:

but:

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:19):

I think saying "slightly better auto-complete" is underrating the benefit

view this post on Zulip Jasper Woudenberg (Nov 10 2024 at 00:20):

Gonna think a bit on this. I definitely better appreciate the syntax in the proposal. Sorry for covering some ground I think the doc covered, some if it I don't think fully clicked before.

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:21):

With people that don't know the API they're working with, method calls don't require knowing where to import the qualifying type from

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:21):

You just do thing.otherthing().next()

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:22):

But if those are all in different modules, today you'd have to either:

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:22):

With methods, you can "discover as you type"

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:23):

I think we could upgrade that via the formatter

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:24):

and maybe even upgrade function calls to method calls, but that'd require type information

view this post on Zulip Luke Boswell (Nov 10 2024 at 00:25):

I like the idea of using a word like call instead of a symbol as it's more explicit. But one think I don't like about .passTo is that it includes a capital letter.

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:25):

And for the record, data.passTo(func, other, args) removes the problems from the dot parens syntax for me, so besides it being longer, I'm here for it because it is "shaped" like the other method calls

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:25):

Though as Agus said:

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:26):

Agus Zubiaga said:

.$ looks a lot less intuitive to me, but at least it’s concise, so I’ll take it :grinning:

I prefer .$ if we're all onboard to the same thing

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:27):

The benefit of .$ over .call/.passTo is that it's "a little weird"

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:27):

I'm leaning towards .passTo (or whatever name) now

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:30):

The main thing I don't like about .passTo is that beginners will probably think that methods can be variadic, but only .passTo will be able to do that

view this post on Zulip Sam Mohr (Nov 10 2024 at 00:30):

.$ looks like an operator, so it has the benefit of implying it's special

view this post on Zulip Luke Boswell (Nov 10 2024 at 00:37):

I feel like it wouldn't be fair not to ask our LLM friends. They might have words in the future if we never even consulted them about this problem.

Claude's opinion

Based on the extensive discussion, here are my key recommendations:

  1. Keep the dot-method syntax (.method()) as proposed for static dispatch, as it provides good ergonomics, familiar syntax, and excellent autocomplete support.

  2. For chaining local/unqualified functions, I recommend NOT trying to force it into the dot syntax. Instead:

(x.method1().method2() |> localFn).method3()

Rationale:

  1. For formatting guidance:

Example of recommended style:

result =
    input.parse().validate()
    |> transform
    |> format
    |> (.toString().trim())

This recommendation prioritizes:

The key insight is that trying to unify method calls and function application into a single chaining syntax may be more trouble than it's worth. The small ergonomic cost of occasional parentheses is outweighed by the benefits of having clear, separate syntaxes for these two different operations.

I believe this approach would serve Roc well - it maintains the benefits of the method syntax proposal while avoiding unnecessary complexity and confusion around local function chaining.

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:40):

I hadn't thought of |>-ing into method call shorthand, that's interesting.

view this post on Zulip jan kili (Nov 10 2024 at 00:46):

Side note, .$() could also accept the function as the first arg, like call/passTo.

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:46):

Agus Zubiaga said:

The main thing I don't like about .passTo is that beginners will probably think that methods can be variadic, but only .passTo will be able to do that

I also briefly had that concern, but I think it would help to teach it as "hey this is just syntax sugar for a normal function call, it looks like a method call for stylistic reasons but it's not actually dispatching to anything at all"

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:47):

just like |>

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:47):

which is also unusual when it comes to arity, in that you have functions that look like they're being called with 1 fewer arg than normal, but actually it's coming from the |> etc.

view this post on Zulip Richard Feldman (Nov 10 2024 at 00:47):

but that hasn't felt like a significant problem in practice

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:49):

yeah, I just think |> is a little different because it looks like an operator and you can't define those, so it's normal for it to be special

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:49):

I don't think it's a big deal, though

view this post on Zulip jan kili (Nov 10 2024 at 00:51):

Luke Boswell said:

I believe this approach would serve Roc well - it maintains the benefits of the method syntax proposal while avoiding unnecessary complexity and confusion around local function chaining.

I agree that this whole proposal seems worth implementing without any .$/.passTo initially, and then after a few months of lots of already-planned refactoring we can see if it feels wanted. A lot of us seem in agreement on moving 90% of the way in the same direction, but there are still valuable discussions being had about tradeoffs of the proposal's core. I wonder if .$ alone should split into a secondary proposal and topic.

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 00:51):

F# for example, doesn't allow variadic functions (except in classes but that's for C# interop), and it still has printfn which is variadic and it even parses the format string at compile time

view this post on Zulip Derin Eryilmaz (Nov 10 2024 at 01:04):

how is static dispatch going to work for functions that take non-opaque types as their first parameters? imagine in a module you have:

Person : { name: Str, age: U32 }
lastName : Person -> Str

then from another you call

person = {name: "J", age: 100}
last = person.lastName()

the compiler would have no way of knowing which module the type of person "belongs to," so I guess we couldn't do static dispatch in this case? like .lastName() would be an error?

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 01:05):

They just wouldn't work, I think

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 01:07):

(you'd get a compiler error telling you that you can only call methods on nominal types)

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:41):

Luke Boswell said:

I like the idea of using a word like call instead of a symbol as it's more explicit. But one think I don't like about .passTo is that it includes a capital letter.

if we went snake_case it could be .pass_to

view this post on Zulip jan kili (Nov 10 2024 at 01:47):

Oh pleasssse let this be the day Roc goessss ssssnake casssse :pleading_face:

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 01:47):

:100000:

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:48):

haha I guess we've never really talked about it...I'm open to it, but I think that's a totally separate topic

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:48):

if someone wants to start a topic in #ideas about it, seems fine to discuss!

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 01:48):

didn't we? I'm pretty sure it was even in a proposal

view this post on Zulip Sam Mohr (Nov 10 2024 at 01:49):

It came up as a "if we did this, it would be compatible" in one of the purity inference docs

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 01:49):

yeah, it was in purity inference v2: https://docs.google.com/document/d/1Nsg6I8Y27WAk7Aj6_fxKB17VGjQaCRMJ-nFfc0hJiwQ/edit?tab=t.0#heading=h.qfvf41di9dxa

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:49):

I think the original ! syntax sugar proposal (or maybe it was Purity Inference) noted that functions ending in ! look nicer to me in snake_case

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:50):

e.g. Ruby has that convention, Rust uses it for macros, and both of them are snake_case

view this post on Zulip Richard Feldman (Nov 10 2024 at 01:51):

anyway, happy to discuss in another thread if anyone wants to start one! :big_smile:

view this post on Zulip Agus Zubiaga (Nov 10 2024 at 02:03):

started: #ideas > snake_case instead of camelCase

view this post on Zulip jan kili (Nov 10 2024 at 02:12):

omg #manifesting

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 15:29):

Oh, also, we were discussing .passTo at some point while I was away. I'm not sure if it is still in the field of options. My first git feeling was that I really dislike it compared to parens or a symbol. Then I remembered that most languages I use every day just don't have a solution for this. So that would strictly be more flexible than what I am used to and it is very readable. I think I would prefer|> over it, but that doesn't always play nice with precedence.

I think it should be much nicer of the rust/c++ equivalent of needing to read in different directions

// I honestly think many people would reformat this in rust due to going between method and function and back
function1(a.method1().method2()).method3()

// This on the other hand would probably be left alone. (Formatter would just reflow it)
a
  .method1()
  .method2()
  .pass_to(function1)
  .method3()

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 15:30):

So pass_to probably is a very readable and reasonable starting point. And it may also be a reasonable finishing point.

view this post on Zulip Vladimir Zotov (Nov 12 2024 at 15:22):

Is this similar to Extension Methods in C#? Back in the day when they introduced that feature, I thought of C# trying to be more functional without bringing the pipe operator :-D
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

view this post on Zulip Dawid Danieluk (Nov 13 2024 at 22:44):

Hello, I'm not very active here and just lurking around until Roc stabilizes, but I really love the direction the language is taking.
I like the approach of using 'special' function call instead of separate syntax as it keeps mental model very simple. Other solutions make understanding code at a glance harder and while current pipe syntax ( |> ) is easy to understand for goblin like me (actually I like it very much) none of other solutions capture that elegance.
Using something like pass_to isn't as nice to look at, but doesn't break the mental model, I can just read the chain from left to right, from top to bottom without having to pay attention to where exactly the brackets are placed.
The only thing I don't like about pass_to is that it's two words and is harder to type. I suggest using something like thru. This suggestion comes from two places:
1) it's function used in lodash, so it comes with some history that already may suggest what it's doing (take input, pass it through the function).
2) It's short, simple to write and more importantly it won't take naming space from functions/methods (I've someone propose 'next' which is something that might be very useful as a method name).
Can't wait to try new syntax :snake:

EDIT. actually I think the 'pass_to'/'thru' approach maps almost 1 to 1 to lodash which is staple JS/TS functional programming library. It's almost as if everything in Roc would be at the start of _.chain(thing) in lodash, so that would make Roc approachable for all JS/TS devs using functional approach.

Here's simple comparison in case you want to see how close they look.

roc

passApproach =
    array = [1, 2, 3].map(\n -> n+1).thru(getSecondEl)
    array

getSecondEl = \list
    list.get(1)

TS/JS lodash

function passApproach() {
   const array = _.chain([1,2,3]).map(n => n+1).thru(getSecondEl).value()
   return array
}

function getSecondEl(list) {
   return list.at(1)
}

By the way I think this comparison shows Roc in very good light. Functional style becomes more and more common in JS/TS land and Roc with this syntax looks like a obvious step up. It's 'lighter', clean, less verbose and has better support for functional programming (duh).

view this post on Zulip Anton (Nov 15 2024 at 12:59):

Good to know that this has been done before and works well!
Perhaps we can start with the clear and self-evident pass_to and move to something shorter if there is demand.

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 22:25):

I've read the proposal and skimmed most of the this thread, but apologies if I've missed something here: There was a lot of chatter about .( vs .> vs .| vs .passTo( vs so many other things, and the lack of consensus suggests to me that it might not be possible to find a "non-weird" option, which sort of defeats the purpose of making the syntax more familiar to mainstream language users.

What I'm wondering is whether the local function piping needs any special syntax at all.

Would it be possible to alter the resolution algorithm such that val.localFunc() Just Works? No val.(localFunc) necessary? Perhaps I'm overlooking something important, but to an outsider, it's not obvious to me why local functions need a special syntax. I'd imagine we'd need to solve the issue of disambiguation, but realistically how often will that come up for users? I would think it's more common that a user would want to write val.method().localFunc() than result.mapErr with a local, custom mapErr, and in those rare situations it's obvious why a special syntax is needed to disambiguate.

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 22:31):

Perhaps local functions take priority over methods from external modules? And if you actually intend to use a same-named function from a different module, that is when you use the dot-parens syntax (e.g., result.(Result.mapErr) to prevent a local mapErr function from being used, which again, I think would be rare)

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 22:39):

maybe functions with the same name and same first type just shouldn't be allowed, so you'd have to rename one of them

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 22:39):

i can't think of any case where you'd actually want to override a method provided for a type

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 22:39):

but i could see why you'd want to add new ones locally with the same syntax

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 22:40):

so i support that idea :thumbs_up:

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 22:41):

I was just thinking about the same thing! If we prioritize local functions during method resolution, then we could just say "sorry, gotta rename the local function if you don't want it to override the function from the other module"

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 22:42):

This wouldn't seem too unreasonable to me as a language user. And down the line we could introduce a special disambiguation syntax to improve quality of life, but that potentially could be separate from this proposal to keep the scope small.

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:43):

I know it's a long thread, but we did talk about all of those things earlier :big_smile:

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:44):

e.g. we talked about "do we really need this?" and someone pointed out that they've wanted it in other languages (and actually now that we've talked about it, I keep seeing cases in Rust where I'd use it :laughing:)

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:44):

we also talked about "Perhaps local functions take priority over methods from external modules?" - the problem with that is, it can create bugs

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:45):

where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:45):

also, I definitely think .pass_to is the design we want

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 22:46):

Ah shoot, sorry for rehashing things that were already discussed. And thanks for the summary!

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:46):

no worries! I know this is a particularly long thread :big_smile:

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:47):

actually it's nice to have a summary of those things here!

view this post on Zulip Richard Feldman (Nov 16 2024 at 22:48):

oh, also we talked about one-word alternatives to .pass_to like .call, but the problem is then they're inaccurate - e.g. if I say foo.call(bar, baz) then it's saying foo.call() but that's wrong; foo is not a function, and it's not being called!

foo.pass_to(bar, baz) is accurate; foo is being passed to bar

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 22:50):

Richard Feldman said:

where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen

well, the other solution is to make it so that the methods, not local functions, have the out-of-place syntax, which would also discourage overuse when autocomplete could be designed to fill in module names:

[1, 2, 3]
  |> _map \x -> x + 1
  |> _keepIf \x -> x > 2
  |> _first
  |> someLocalFunction
  |> Inspect.toStr
  |> Stdout.line!

and i like this better than anything proposed in this channel, but i realize that's an unpopular opinion.

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 23:00):

I also like the .pass_to solution, because even if it's implemented as a special case in the compiler, an unfamiliar user can at least imagine it as a "builtin method" that can be used like any other method via static dispatch:

pass_to : a, (a -> b) -> b

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 23:02):

i don't know if it would be possible to write a type signature for pass_to with the current syntax

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 23:02):

Right, it's not a perfect mental model, but an unfamiliar user could at least think of it this way

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 23:03):

well that only works for pass_to if there are no other arguments :sweat_smile:

view this post on Zulip Derin Eryilmaz (Nov 16 2024 at 23:03):

but it's close enough

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 23:04):

Rust users might think of it more like a macro then to account for the variable number of args :laughing:

view this post on Zulip Tanner Nielsen (Nov 16 2024 at 23:08):

And maybe if Roc ever supports variadic functions in the future, then this could be implemented in the standard library, rather than a compiler special case

view this post on Zulip Richard Feldman (Nov 17 2024 at 00:04):

nah, varargs would all need to have the same type

view this post on Zulip Richard Feldman (Nov 17 2024 at 00:05):

I don't think it could be implemented as a "normal" function

view this post on Zulip Sky Rose (Nov 17 2024 at 05:07):

Is .pass_to( still up for discussion or petty much chosen at this point? I really dislike it. It's 9 characters to do a basic function call when the status quo is at most 4 (a space, |> , (), or .()). It also got letters in it, which when reading will make it harder to pick out actually meaningful names like the function name and nearby variables and methods. I'd prefer something punctuation-only.

view this post on Zulip Sky Rose (Nov 17 2024 at 05:07):

Also, once the dust settles on all these discussions, are you planning to release a second draft of the proposal document? A lot has changed and it's hard to keep track of what the current plan is.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 06:12):

To be clear, you can still use |> even if .pass_to exists. Just a tradeoff around parens and readability in certain cases.

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:52):

I wasn't thinking we'd keep |> in that world - really seems like what we want there is something that starts with . and can have things chained after it

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:54):

keep in mind that I think this would be infrequently used compared to |> today - pretty much only when doing |> into an unqualified call today

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:54):

(with some exceptions that are even rarer)

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:54):

so I don't think the verbosity will come up that often

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:55):

I'm open to alternative ideas but so far this feels like the best option by a significant margin to me

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:56):

like if we wanted it to be shorter, I could see an argument for .pt(fn, arg)

view this post on Zulip Richard Feldman (Nov 17 2024 at 06:57):

on the theory that it's a common enough builtin to use that everyone will learn what it stands for quickly

view this post on Zulip Isaac Van Doren (Nov 17 2024 at 07:20):

I like the idea of pt. We could also go with just to

view this post on Zulip Oskar Hahn (Nov 17 2024 at 07:45):

I really like the current syntax where you have one thing and pipe it through a list of functions.

In the proposal is a quote from gleam, that it is just syntax and should not matter, but it did.

My question is, if it is just syntax, why will I not use the alternative to |> as frequently as I use it today?

view this post on Zulip Jasper Woudenberg (Nov 17 2024 at 08:16):

Oskar Hahn said:

My question is, if it is just syntax, why will I not use the alternative to |> as frequently as I use it today?

I think a motivation behind static dispatch is reducing the amount of times we have to write module qualifiers, currently a lot more frequent in Roc than in other programming languages. A syntax-only change wouldn't reduce qualifier usage. Adopting static dispatch will make Roc more competitive with mainstream programming languages in terms of verbosity, and more familiar to programmers in those language as well.

view this post on Zulip Eli Dowling (Nov 17 2024 at 08:35):

I strongly think .> Is the best of the alternative pipe syntaxes:

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 08:38):

Would |> really go away? That will only happen if you mostly use nominal types and also don't have many local or imported functions. I think it would still be super common to have local or imported functions that aren't from the module a nominal type is defined in. So I would expect to still significantly use |> even if static dispatch existed.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 08:40):

I don't really see .pass_to as a viable replacement for |>. .pass_to would only be used if everything else is static dispatch and only one or two things need .pass_to. I would probably switch back to |> if there was too much noise or too many .pass_tos in a row

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 08:42):

If we truly expect this to be super common such that pass_to or the equivalent is used all over the place, I would argue that we should just commit to a smarter resolution algorithm that allows using local functions directly with the dot method syntax instead of limiting it to nominal types and functions exposed in their modules.

view this post on Zulip Eli Dowling (Nov 17 2024 at 08:59):

Just a little syntax comparison using an example from above:

What we have now:

[1, 2, 3]
|> List.map \x -> x + 1
|> List.keepIf \x -> x > 2
|> someLocalFunction
|> List.first
|> Inspect.toStr
|> Stdout.line!

With the pass_to syntax:

[1, 2, 3]
.map( \x -> x + 1)
.keepIf( \x -> x > 2)
.first()
.pass_to someLocalFunction("extra arg)
.pass_to toStr
.pass_to Stdout.line!

with .> syntax:

[1, 2, 3]
.map( \x -> x + 1)
.keepIf( \x -> x > 2)
.first()
.> someLocalFunction( "extra arg" )
.> toStr
.> Stdout.line!

A syntax I just thought of:

[1, 2, 3]
|> .map \x -> x + 1
|> .keepIf \x -> x > 2
|> someLocalFunction
|> .first
|> Inspect.toStr
|> Stdout.line!

Was keeping dropping the fully qualified requirement by simply using .name _.name? Instead of changing the method call syntax.
Or even having a different pipe operator, eg:

[1, 2, 3]
.> map \x -> x + 1
.> keepIf \x -> x > 2
|> someLocalFunction
.> first
|> Inspect.toStr
|> Stdout.line!

Of all these syntaxes, the suggested syntax with .> for piping, and the last one that uses .> for static dispatch calls are my favourites.

view this post on Zulip Eli Dowling (Nov 17 2024 at 09:01):

Brendan Hansknecht said:

I don't really see .pass_to as a viable replacement for |>. .pass_to would only be used if everything else is static dispatch and only one or two things need .pass_to. I would probably switch back to |> if there was too much noise or too many .pass_tos in a row

A very significant number of my pipe functions would be static dispatch, though. Like a huge number of my function calls are operating on some built in data type.

I do think that .> looks nicer than |> when combined with static dispatch calls.

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:42):

yeah in Rocci Bird there are 40 uses of |> and 38 would become static dispatch. The two that wouldn't are these two:

    pipes =
        prev.pipes
        |> updatePipes
        |> List.appendIfOk pipe
    plants =
        prev.plants
        |> updatePlants
        |> List.appendIfOk plant

...because the update___ functions take a List

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:43):

so these might become:

    plants =
        prev.plants
        .pass_to(update_plants)
        .append_if_ok(plant)

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:43):

or:

    plants =
        prev.plants
        .pt(update_plants)
        .append_if_ok(plant)

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 14:44):

Wouldn’t .thru be a lot more intuitive than .pt?

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 14:45):

I feel like I wouldn’t have to Google the former

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:46):

well personally my main reaction to the above is that .pass_to is fine :big_smile:

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 14:47):

Yeah, I mean if we are considering something different

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 14:48):

I think .pt is too cryptic

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:50):

also of note, in the Roc-Ray physics example port it started with 17 uses of |> and ended up with only static dispatch

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:51):

so between the two, that's 57 uses of |> and 55 of them became static dispatch, meaning (at least for those two examples) we're discussing syntax for 2 out of 57 usess

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:51):

I had a general intuition that almost every use of |> would become static dispatch, but going through those two examples reinforced it

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:53):

of note, in mainstream languages, this:

    plants =
        prev.plants
        .pass_to(update_plants)
        .append_if_ok(plant)

...could only be written as:

    plants =
        update_plants(prev.plants)
        .append_if_ok(plant)

which also looks fine to me

view this post on Zulip Richard Feldman (Nov 17 2024 at 14:54):

personally I could see myself writing it either way

view this post on Zulip Richard Feldman (Nov 17 2024 at 15:00):

so my overall thought here is that if we're talking about an edge case, it doesn't make sense to spend a bunch of strangeness budget to make it look more concise

view this post on Zulip Richard Feldman (Nov 17 2024 at 15:01):

Brendan Hansknecht said:

If we truly expect this to be super common such that pass_to or the equivalent is used all over the place, I would argue that we should just commit to a smarter resolution algorithm that allows using local functions directly with the dot method syntax instead of limiting it to nominal types and functions exposed in their modules.

oh I forgot to address this earlier - the problem with this is that any resolution that allows using local functions silently is error-prone

view this post on Zulip Richard Feldman (Nov 17 2024 at 15:02):

the specific way it's error-prone is:

Richard Feldman said:

we also talked about "Perhaps local functions take priority over methods from external modules?" - the problem with that is, it can create bugs
where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen

view this post on Zulip Richard Feldman (Nov 17 2024 at 15:11):

so to summarize:

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:33):

How is it 2 out of 55? You expect numbers and model to be nominal types? Also, you can't use static dispatch with functions like List.map. so it would be all of those as well.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:35):

Also, I really don't think these are even close to representative when thinking about how real projects and large code bases will be architected. These examples are single file scripts.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:36):

I do agree that mainstream languages don't have |>, but I think that it is one of my favorite pieces of roc syntaxes that would enhance other languages.

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:40):

Brendan Hansknecht said:

How is it 2 out of 55? You expect numbers and model to be nominal types? Also, you can't use static dispatch with functions like List.map. so it would be all of those as well.

sorry, I don't follow :sweat_smile:

can you give an example of some code from Rocci bird that you don't think would use static dispatch?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:44):

Any |> List.map cause static dispatch doesn't support it

The places where |> is used with the model like:

        prev
        |> updateFrameCount
        |> runGameOver

Any use with Num I assume cause Num isn't a single type but multiple types. So I don't think it will have static dispatch? Though I just don't know how resolution works there (we should probably discuss that)

        frameCount
        |> Num.bitwiseAnd 0xFF
        |> Num.toU8

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:46):

oh Num is a nominal type, so static dispatch would definitely work on it

view this post on Zulip Oskar Hahn (Nov 17 2024 at 16:48):

Since there are no big projects in Roc jet, maybe you could analyze an elm codebase how many usages of |> could use static dispatch?

For example the real world example app?

https://github.com/rtfeldman/elm-spa-example/tree/master

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:48):

Ah yeah, Num is a single nominal type and all of the specific types are just aliases.

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:49):

and updateFrameCount takes a TitleScreenState, which I was assuming would become a custom record in this world, which in turn would mean you could just do prev.update_frame_count().run_game_over()

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:51):

It's not an opaque type today, would it really be a nominal type in this new world?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:51):

Is nominal the expected default?

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:52):

I'm not sure one way or the other to be honest

view this post on Zulip Richard Feldman (Nov 17 2024 at 16:53):

my default assumption is that for things that don't get constructed very often (e.g. application state), nominal would make more sense

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 16:59):

so for any non-nominal type, you will still want |> and this decision feels a lot harder in general

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:00):

:thinking: what would be the downside of making TitleScreenState nominal?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:02):

adding .0 everywhere. feeling more complex. feeling concerned about not being able to pass it to another module if the app gets more complex. No longer supporting pattern matching or partial record functions. Maybe not specifically an issue for rocci-bird, but definitely issue for real projects

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

.0 wouldn't be necessary for custom records

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:03):

that's only for custom tuples, which are wrappers around other types (e.g. Email being a wrapper around Str)

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:05):

btw in the specific case of Rocci Bird, if you did want them to be structural, it would change this:

        TitleScreen prev ->
            prev
            |> updateFrameCount
            |> runTitleScreen

        Game prev ->
            prev
            |> updateFrameCount
            |> runGame

        GameOver prev ->
            prev
            |> updateFrameCount
            |> runGameOver

...to this:

        TitleScreen prev ->
            run_title_screen(update_frame_count(prev))

        Game prev ->
            run_game(update_frame_count(prev))

        GameOver prev ->
            run_game_over(update_frame_count(prev))

...or this:

        TitleScreen prev ->
            prev
            .pass_to(update_frame_count)
            .pass_to(run_title_screen)

        Game prev ->
            prev
            .pass_to(update_frame_count)
            .pass_to(run_game)

        GameOver prev ->
            prev
            .pass_to(update_frame_count)
            .pass_to(run_game_over)

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:05):

all 3 of these look totally fine to me

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:06):

like I could do a stack ranking of my preferences in terms of how they look, but the delta between them is not like "I'm gonna flip a table if it can't be written that way" :big_smile:

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:06):

at least to me!

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:07):

Switching between, rust/c++ and roc, going from run_title_screen(update_frame_count(prev)) to prev |> update_frame_count |> run_title_screen is absolutely huge

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:08):

So I disagree on those being equivalent

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:09):

well if you wanted this (which is what I personally would go for):

        TitleScreen prev ->
            prev
            .update_frame_count()
            .run_title_screen()

        Game prev ->
            prev
            .update_frame_count()
            .run_game()

        GameOver prev ->
            prev
            .update_frame_count()
            .run_game_over()

...the only additional change would be changing TitleScreen : to TitleScreen := and then in initGame and initTitleScreen changing { ... } to TitleScreen.{ ... }

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:10):

But only if a nominal type makes sense. I don't think it always will and it definetly is a decision

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:11):

totally!

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:11):

I have nominal types in rust and c++, but I see run_title_screen(update_frame_count(prev)) all the time

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:14):

Brendan Hansknecht said:

Switching between, rust/c++ and roc, going from run_title_screen(update_frame_count(prev)) to prev |> update_frame_count |> run_title_screen is absolutely huge

can you say more about why you see this as huge? :thinking:

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:15):

to me, the main situations where pipelines are much nicer is when a single-line call wouldn't be reasonable, and when order of arguments matters and it's clearer with a pipe (e.g. today I like to do str |> Str.endsWith ".txt" over Str.endsWith str ".txt")

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:15):

and I do have a preference for prev.update_frame_count().run_title_screen() over run_title_screen(update_frame_count(prev)) but I wouldn't call it a huge difference

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 17:19):

I think prev.update_frame_count().run_title_screen() is best in general. I think that prev |> update_frame_count |> run_title_screen is essentially as readable and also pressure people into designing to put the most important arg first like with the dot syntax. run_title_screen(update_frame_count(prev)) with more complex functions and multiple args quickly gets complex and hard to read. Often requires lots of extra parens or breaking out a temporary variable to deal with splitting complex expressions. I think it is a significant readability hit.

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:30):

Oskar Hahn said:

Since there are no big projects in Roc jet, maybe you could analyze an elm codebase how many usages of |> could use static dispatch?

For example the real world example app?

https://github.com/rtfeldman/elm-spa-example/tree/master

good idea! It has 127 uses of |> and I went through and looked at which ones would be static dispatch vs not. It's a bit of an apples-to-oranges comparison because a ton of them are involved in writing out encoders and decoders, which in Roc would just go away because we can infer those. So the total count in Roc would be a lot less than 127 (maybe like 25% of them would go away).

there are a bunch like this, which wouldn't come up in Roc because of automatic encoder/decoder inference:

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Article/Comment.elm#L64-L65

there are also some where I don't think it really mattered one way or the other:

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Article/Feed.elm#L184

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Api/Endpoint.elm#L59

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Page/Article.elm#L415

this one is interesting because we don't have the equivalent of Tuple.mapFirst in Roc:

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Page/Article/Editor.elm#L260

...but with static dispatch, we could offer .map_0, .map_1, etc. as builtin methods on tuples that are just automatically available. Not saying we should or shouldn't do that, just noting that if we do want something like this to be available, static dispatch makes it possible for it to be more ergonomic than what's possible today.

here's a good example of something similar to the Rocci Bird structural vs nominal type question:

https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Main.elm#L175-L176

Editor is a structural record today (Elm doesn't have a concept of nominal records), but it has its own Editor.elm module, and I think it would be about as ergonomic as a nominal record.

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:33):

overall I think if you take out the encoding/decoding uses of |> that wouldn't apply in Roc (but which would use static dispatch anyway even if they stayed), this looks pretty comparable to Rocci Bird in that:

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:34):

this one is interesting:

        List.range 1 totalPages
            |> List.map viewPageLink
            |> ul [ class "pagination" ]

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:35):

the direct translation would be:

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

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

if there were no .pass_to (or equivalent), it would have to be this:

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

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:38):

here are some different ways this could be written based on some ideas in this thread:

.pass_to()

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

.pt

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

|>

        List.range({ start: At(1), end: Length(total_pages) })
        .map(view_page_link)
        |> 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 (Nov 17 2024 at 17:39):

of all these options, including the "just write it in the direct call style" I personally prefer .pass_to over all the other options

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:40):

also, something I don't think I noted earlier - only .pass_to(...) and .pt(...) allow you to continue chaining afterwards

view this post on Zulip Richard Feldman (Nov 17 2024 at 17:41):

|> and .> don't have the same precedence, so if you wanted to throw another .foo() on the end there, it wouldn't work

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

Would have to be this, right?

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

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:13):

hm, why?

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:14):

.map on a List would dispatch to List.map

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:14):

I thought we said that static dispatch doesn't work with function like map due to changing the inner type variable of List.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:14):

Just like abilities fail on this case today

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:15):

Though I guess it should work as map: a, (b -> c) -> d?

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:15):

oh, no it turned out that works fine

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:16):

what doesn't work is if you try to abstract over that

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:16):

in a way that uses the element type

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:16):

but using it like this is fine

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:16):

Can you give an example?

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:18):

the example we talked about last time was trying to make something like Rust's IntoIterator - where you say like "give me a container with elem as a type parameter and I'll give you back an Iter elem"

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:19):

that can't be done today, and it also couldn't be done using static dispatch

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:19):

But you could make List.toIter: List a -> Iter a and it would work to call myList.toIter with toIter: a -> b, right?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:20):

So you can't make an abstract toIter, but you can make many types have a concrete impl?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:21):

Or is that toIter still broken?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:27):

Also, I feel like I have gotten into the weeds away from my initial point. Even in rust/c++ where all types are nominal, it is still common to see complex nested function call sequences that are not methods. In those cases, I find |> much more readable. So I would want to keep it even with this proposal and I would expect it to still be pretty common in larger more real world code bases. We should have strictly less nominal types than those languages and they can still get bad function chains pretty often.

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:45):

I guess we can just find out? haha

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:45):

certainly |> would be there at the outset to avoid a breaking change

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:45):

my prediction is that it will be used so infrequently (especially assuming we have .pass_to) that it will end up feeling unnecessary

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:46):

but maybe it doesn't turn out that way in practice!

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:46):

no real harm in leaving it in at first and finding out empirically

view this post on Zulip Sam Mohr (Nov 17 2024 at 18:47):

We'd want |> and dot calls to have the same precedence there, right?

view this post on Zulip Sam Mohr (Nov 17 2024 at 18:48):

It might be unintuitive, but the pizza is unintuitive to C-style experienced beginners anyway

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:48):

Sam Mohr said:

We'd want |> and dot calls to have the same precedence there, right?

I would find this really confusing personally. No other infix operators work that way.

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:48):

also it would necessarily be whitespace-sensitive

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:49):

e.g. there's only one reasonable way to parse foo.bar() |> baz().blah()

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:49):

there's a case to be made that this should parse differently:

foo.bar()
|> baz()
.blah()

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:49):

but I think it's confusing if those parse differently

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:50):

this is one of the reasons I prefer .pass_to - it parses the same way in single-line vs multiline, and there are no special precedence rules needed:

foo.bar()
.pass_to(baz)
.blah()

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:51):

Yeah, they have different precedences. So both are needed for different use cases

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:51):

as in both |> and .pass_to are needed?

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:51):

Yeah

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:52):

that's possible, although my prediction is that people will say "why do we have both of these? why not just have .pass_to" - but again, I could be wrong and there's only one way to find out in practice! :big_smile:

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:52):

|> for structural types and more complex function chaining. Though maybe if everything is nominal in practice, only .pass_to will be needed

view this post on Zulip Richard Feldman (Nov 17 2024 at 18:53):

yeah my guess is that custom records will be more common once we introduce them, but also I could be wrong about that!

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:53):

And yeah, I think at this point, it is something where we really need to see in practice.

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 18:53):

Though may still be hard to fully judge due to all roc code being very small today

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 18:57):

.pass_to would still work with structural types, right?

view this post on Zulip Agus Zubiaga (Nov 17 2024 at 18:57):

Like how .is_eq would be autoderived

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 19:07):

I guess it could work with structural types. Though having it be a single piece in a chain of mostly method calls looks natural. Having it on every single call due to using a structural type is a lot of extra noise compared to |>

view this post on Zulip Sam Mohr (Nov 17 2024 at 19:16):

@Brendan Hansknecht would you get the benefits you want from |> out of .> chaining? It feels like that has the same visual shape while still supporting autocomplete and equal precedence to method calls

view this post on Zulip Brendan Hansknecht (Nov 17 2024 at 19:24):

Quite possibly assuming it is for both nominal and structural types

view this post on Zulip Sam Mohr (Nov 17 2024 at 19:33):

I'm not sure how .pass_to would be implemented, but .> would just be sugar, so I'd expect it'd work for both

view this post on Zulip Richard Feldman (Nov 17 2024 at 19:45):

.pass_to would be sugar that would work on any value regardless of type

view this post on Zulip Richard Feldman (Nov 17 2024 at 19:45):

it would be exactly the same as |> today except spelled differently :big_smile:

view this post on Zulip jan kili (Nov 17 2024 at 19:49):

So then... can we instead/also call it .pipe_to()?

view this post on Zulip Sam Mohr (Nov 17 2024 at 19:50):

Then I'm still happy with .pass_to!

view this post on Zulip jan kili (Nov 17 2024 at 19:56):

It would be nice to have one Roc Term for this concept. Is |> a pipe that passes? Does .pass_to pipe? Is the definition for one word just a link to the other?

view this post on Zulip jan kili (Nov 17 2024 at 20:02):

I like that .pipe_to feels like "hey compiler, here's some syntax sugar, please make the invisible plumbing to chain this together"

view this post on Zulip Richard Feldman (Nov 17 2024 at 20:19):

what I like about .pass_to is that you're passing it to a function

view this post on Zulip Richard Feldman (Nov 17 2024 at 20:19):

foo.pass_to(bar) is so clear it practically doesn't even need documentation :big_smile:

view this post on Zulip Richard Feldman (Nov 17 2024 at 20:19):

I can't say the same of any of the alternatives we've discussed, which is one of the reasons I like it the best

view this post on Zulip Kiryl Dziamura (Nov 17 2024 at 20:39):

Sorry, I have no time to read through the thread. The proposal is great, I also don’t very like the .(f) (it also reminds me of .call(f) from js but without “call”), but skimming the thread, I see there are already a great conversation and ideas on it.

My question is regarding abilities. First, where item.Sort looks unexpected as the dot is an accessor in the lang, but here it works as special syntax. I’d propose having where item ~ Sort so item : List means “item is a List”, and item ~ Sort means “item conforms Sort”, so you can even do this in simple cases: f : ~Sort -> Bool omitting the where block.

But more important for me is the part about abilities removal. I thought the key advantage of traits/type classes is the separation of a specific behavior. So if one ability implements method named toString and another ability implements a method with the same name, there won’t be any clashes as you have to explicitly specify what you’re using at the call site. Without this, it feels like composition is the only way. Isn’t it a more confusing and complex concept? I might be missing something, unfortunately, have no time to read the whole discussion.

view this post on Zulip Kiryl Dziamura (Nov 17 2024 at 21:17):

Ah, there’s another thread about it.

Upd. Not quite. Can someone guide me on where I can find a relevant discussion? Or even give a summary? Thank you.

view this post on Zulip Artem Shamsutdinov (Nov 18 2024 at 11:43):

Agree. Would be bad if names clash. I suppose if names clash but types are different there will have to be another function in the module with the same name but a different type (overloading)? If the names clash but types are the same - there will be just no way to specify different implementations

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:51):

Yeah, this is much more akin to go interface than abilities. If you happen to have the right function, you match the static dispatch.

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:52):

Go does not have overloading and I haven't seen issues with this in practice, but it does mean you could hit a hard place where it is impossible to implement two different static dispatch interfaces on the same type

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:55):

If that happened, you would have to either:

  1. Change one of the interfaces
  2. Convert to a different version of your type to get the interfaces. myType.toReadable()
  3. Not use the interface

view this post on Zulip Brendan Hansknecht (Nov 18 2024 at 16:55):

I'm sure it must happen on occasion in practice, but I'm not sure it generally becomes a real issue.

view this post on Zulip Richard Feldman (Nov 18 2024 at 18:55):

yeah I think it's really valuable to have Go as a reference point for what happens if you use method name as the only way to resolve them

view this post on Zulip Richard Feldman (Nov 18 2024 at 18:55):

and it's really good to know that it hasn't been a problem in practice there! :smiley:

view this post on Zulip Oskar Hahn (Nov 18 2024 at 19:42):

The implicit interfaces is probably the feature, I love most about Go. The full type inference feature is probably the feature, I love most about Roc.

I am a bit skeptical about the static dispatch proposal, but if it would combine both of these features, it would be fantastic.

view this post on Zulip Richard Feldman (Nov 19 2024 at 02:21):

just ran into some Rust code where I would have used .pass_to if Rust had it:

NonEmptySlice::from_slice(mono_exprs.extend(fields.iter()))
    .map(MonoExpr::Struct)

I would have preferred to write it like this (using proposed Roc syntax):

mono_exprs.extend(fields.iter())
    .pass_to(NonEmptySlice.from_slice)
    .map(MonoExpr.Struct)

view this post on Zulip Dan G Knutson (Nov 19 2024 at 21:41):

I haven't successfully kept up with this thread. I will say that I was using |> localFunction quite a bit for game stuff, so I'm another vote in favor of pass_to (although maybe local nominal types would work fine too; I like defaulting to non-opaque records).

view this post on Zulip Richard Feldman (Nov 23 2024 at 14:01):

random thought: we were talking at some point earlier in the thread about how Abilities let you define multiple opaque types in the same file with their own equals etc.

I just realized that if it turns out we miss that in practice, one way we could reintroduce it in the static dispatch world would be to let you do nested modules - that is, use the module keyword more than once in a file

view this post on Zulip Richard Feldman (Nov 23 2024 at 14:02):

like Rust, OCaml, etc. allow

view this post on Zulip Richard Feldman (Nov 23 2024 at 14:03):

that way we'd have both the simple "static dispatch resolves to the function inside the module where the nominal type was defined" design and also "you can define multiple nominal types in the same file which have their own methods"

view this post on Zulip Richard Feldman (Nov 23 2024 at 14:04):

but I think regardless we should not include that at first, to see what it feels like without it - maybe it's not necessary in practice

view this post on Zulip Richard Feldman (Nov 23 2024 at 14:04):

just good to know that it's a straightforward thing we could do if it turns out we miss that aspect of the current Abilities design

view this post on Zulip Maksim Volkau (Nov 23 2024 at 19:04):

My small feedback regarding the [-2, 0, 2].map(.abs().sub(1)) to [-2, 0, 2].map(\x -> x.abs().sub(1)).

What I love about the current Roc syntax is that lambda is almost always starts with the \ which brings the attention to the fact that it is a lambda, plus simplifies the parsing.
I do not particularly like the '.foo' and '&foo' syntax, as it diverges from the above.

But I still like to express simple things with fewer characters. That's why I have experimented with a closure shortcut in this idea and the related PR #ideas > Closure shortcut feature

Using my syntax, you will add a slash to indicate the lambda [-2, 0, 2].map(\.abs().sub(1)).
Plus, you can say other things in consistent way [-2, 0, 2].map(\+1), [a, b].map(\.foo * 2), etc.

view this post on Zulip Kasper Møller Andersen (Nov 27 2024 at 21:05):

One thing I didn't see mentioned is what kind of importing is required to use static dispatch. For example, if I call Dir.copyAll! "public" "build" which returns a Result, I've obviously imported Dir into my file, but would I also need to import Result to then call map_err on it? (And if Result happens to be automatically imported, then same question but on a type that does need an import statement usually)

If you can call map_err without importing Result via static dispatch, then it does seem to me like we at least lose local reasoning at the module level. As in, one can only use what is actually imported. Not a huge loss necessarily, but I would guess it makes it harder to write linting rules a la elm-review for example.

view this post on Zulip Brendan Hansknecht (Nov 27 2024 at 21:18):

To my understanding, no import is necessary. Importing would only be necessary for typing functions and variables.

view this post on Zulip Felipe Vogel (Nov 27 2024 at 22:04):

I'm a lurker here, hoping to dive in to Roc at some point soon, but for now I wanted to say I really like this proposal and it reminds me of something I read about the Koka language the other day: https://koka-lang.github.io/koka/doc/book.html#sec-dot. Wanted to mention it in case anyone else sees an interesting connection here.

Koka is a function-oriented language where functions and data form the core of the language (in contrast to objects for example). In particular, the expression s.encode(3) does not select the encode method from the string object, but it is simply syntactic sugar for the function call encode(s,3) where s becomes the first argument. Similarly, c.int converts a character to an integer by calling int(c) (and both expressions are equivalent).

view this post on Zulip Joshua Warner (Nov 27 2024 at 22:29):

I like this proposal!

view this post on Zulip Joshua Warner (Nov 27 2024 at 22:29):

Is there a summary floating around of the current alternatives considered for calling local functions in this syntax, and the arguments for/against those?

view this post on Zulip Joshua Warner (Nov 27 2024 at 22:30):

I have ideas but don't want to re-tread ground

view this post on Zulip Richard Feldman (Nov 27 2024 at 23:01):

this is where I'm at right now:
Richard Feldman said:

what I like about .pass_to is that you're passing it to a function
foo.pass_to(bar) is so clear it practically doesn't even need documentation :big_smile:
I can't say the same of any of the alternatives we've discussed, which is one of the reasons I like it the best

view this post on Zulip Joshua Warner (Nov 27 2024 at 23:03):

Fair

view this post on Zulip Joshua Warner (Nov 27 2024 at 23:04):

My suggestion was going to be, "what if we hold off on allowing local functions in the '.' syntax for a bit, keep |> for the moment, and see how things evolve"

view this post on Zulip Joshua Warner (Nov 27 2024 at 23:05):

Easier to land on another solution later if we don't have a bunch of code to migrate

view this post on Zulip Richard Feldman (Nov 27 2024 at 23:21):

definitely want to keep |> at first for backwards compatibility and incremental migration regardless! :big_smile:

view this post on Zulip Eli Dowling (Nov 30 2024 at 05:15):

Eli Dowling said:

Just a little syntax comparison using an example from above:
....

@Joshua Warner I outlined a few syntax options here :)

view this post on Zulip Paul Stanley (Nov 30 2024 at 08:24):

Richard Feldman said:

definitely want to keep |> at first for backwards compatibility and incremental migration regardless! :big_smile:

I'm glad, because I'd like to see how this actually works in practice before removing |>. I see all the benefits of (a) consistent idioms and (b) syntax that is familiar ... but

For all those reasons, I'd like to see how it actually works in practice before putting |> to sleep.

view this post on Zulip Nicola Peduzzi (Dec 05 2024 at 22:02):

loving this. my 2 cents: I recently had this situation:

aList
|> List.walk ...
    List.walk ...
    ...lots of code...
    |> List.dropIf ... # this is indented one tab too many and producing a strange bug hard to catch

with this new proposal it would become:

aList.walk(\...
   subList.walk(...)
).dropIf() # because of the parenthesis this can no longer be misplaced

w

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

Yep, as much as parentheses can feel cumbersome, they are much easier to parse correctly for humans and the compiler alike

view this post on Zulip Anthony Bullard (Dec 05 2024 at 22:11):

And that calling convention is just SO mainstream that it helps with adoption

view this post on Zulip Brian Teague (Dec 09 2024 at 20:22):

Sorry for the beginner question, but how does this proposal impact back passing <- and tasks?

storeEmail=path->
    url <- File.readUtf8 path |> Task.await
    user <- Http.get (url,Json.codec) |> Task.await
    dest = Path.fromStr "$(user.name).txt"
    _ <- File.writeUtf8 (dest,user.email) |> Task.await
    Stdout.line "Wrote email to $(Path.display dest)"

view this post on Zulip Brendan Hansknecht (Dec 09 2024 at 20:22):

Tasks and backpassing are both already deprecated

view this post on Zulip Brendan Hansknecht (Dec 09 2024 at 20:23):

Task is almost done being replaced by purity inference

view this post on Zulip Brendan Hansknecht (Dec 09 2024 at 20:23):

Backpassing is just slated for eventual removal.

view this post on Zulip Anthony Bullard (Dec 09 2024 at 20:42):

It’ll be crazy to compare the ROC of April 25 to the ROC of April 23

view this post on Zulip Anthony Bullard (Dec 09 2024 at 20:42):

And my phone won’t stop changing Roc to ROC

view this post on Zulip Brendan Hansknecht (Dec 09 2024 at 20:50):

Yeah... purity inference, snake_case, and static dispatch

view this post on Zulip Luke Boswell (Dec 09 2024 at 20:50):

Anthony Bullard said:

And my phone won’t stop changing Roc to ROC

Even the machines are excited about ROC :rock_on:

view this post on Zulip Richard Feldman (Dec 09 2024 at 21:03):

it is one of the officially-accepted spellings, to be fair

view this post on Zulip Anthony Bullard (Dec 09 2024 at 22:20):

Can’t wait to see a full app (or some AOC) converted to PI, snake case, and static dispatch

view this post on Zulip Anthony Bullard (Dec 09 2024 at 22:21):

Oh and with var and for

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

Yep...will be crazy different

view this post on Zulip witoldsz (Dec 10 2024 at 10:11):

Hi, I have not been around for a while and when I came I saw this proposal :sob:
I can understand where does it come from (it's so popular, let's do the same and also be popular).

It reminds me Kotlin, the language I really dislike in many aspects, but one of the biggest is that something.method(...) is so ambiguous. You NEVER know where does the method come from. And the parenthesis all over the place :sad:

Is this proposal widely accepted? I can't see much resistance when scrolling through this thread…

In the end, I will probably accept whatever Roc morphs into, but I was soooooooo excited when first saw Roc without the dot-parenthesis-madness.

view this post on Zulip witoldsz (Dec 10 2024 at 10:28):

One more thing why I hate Kotlin-like it.that(…) aka. the "static dispatch":

Now, compare this with the beautiful world of |> SomeModule.functionXyz and all is there, just in front of you. In an instant you know you need SomeModule and the world is a wonderful place to live again.

view this post on Zulip witoldsz (Dec 10 2024 at 10:33):

Nicola Peduzzi said:

loving this. my 2 cents: I recently had this situation:

aList
|> List.walk ...
    List.walk ...
    ...lots of code...
    |> List.dropIf ... # this is indented one tab too many and producing a strange bug hard to catch

with this new proposal it would become:

aList.walk(\...
   subList.walk(...)
).dropIf() # because of the parenthesis this can no longer be misplaced

w

I had similar bug in Java once. The parenthesis mess won't protect you from such a subtle bug, because often there are so many of them, you can …walk(…).dropIf()) or …walk(…)).dropIf() as easily.

view this post on Zulip Anton (Dec 10 2024 at 10:52):

Is this proposal widely accepted?

We want to try it out and make a decision based on that experience.

view this post on Zulip Jasper Woudenberg (Dec 10 2024 at 10:54):

Hey @witoldsz, I in a similar place to you with regards to the mix of understanding why the proposal would draw more people to the language (which is an important and big benegit), but also feeling a bit sad about it. :heart:

I also share your misgivings about Kotlin's take on this syntax, but I do not believe the proposed design would have the same downsides to auto-complete. The proposed design contains a .pass_to(OtherModule.some_method) syntax for chaining to to arbitrary non-method functions, and it expressly starts with a . so those options can be part of the same auto-complete experience as method calls.

view this post on Zulip Anton (Dec 10 2024 at 10:58):

AND IT IS IMPOSSIBLE to figure out why… which import statement is missing or what, in order to have same method.

We don't rely on imports in this proposal, so as long as the type is the same as in the example you should be good. The method can change between different version of a library but you can view the type on hover and go look in docs and release notes, same as usual it seems.

view this post on Zulip Alex Nuttall (Dec 10 2024 at 11:02):

I'd be really interested to see some longer, less trivial examples converted into the proposed style. Especially with full indentation (as if auto-formatted). I would do it myself, but I'm not sure I understand some nuances.

I think the upside of whitespace calling has been underrated here. I think it may be beneficial in languages with nested lambdas.

view this post on Zulip Jasper Woudenberg (Dec 10 2024 at 11:06):

At the bottom of the proposal linked at the top of the thread, Richard links to a couple of larger code samples converted to the new style!

view this post on Zulip Alex Nuttall (Dec 10 2024 at 11:07):

witoldsz said:

I had similar bug in Java once. The parenthesis mess won't protect you from such a subtle bug, because often there are so many of them, you can …walk(…).dropIf()) or …walk(…)).dropIf() as easily.

Auto-indentation and editor-based bracket highlighting help a lot in these 'pyramid of death' situations, in my experience

view this post on Zulip Nicola Peduzzi (Dec 10 2024 at 11:07):

I see where @witoldsz comes from. Roc would take quite a turn from "elm inspired" with these changes.

speaking of autocomplete, wouldn't be possible for an editor to show a list after typing |> with all imported functions accepting the current type as first argument?

perhaps out of scope, but I think editor ergonomics could go further for language adoption than familiar syntax.

still it'll be interesting to try out the new syntax. it will be a breaking change right?

view this post on Zulip witoldsz (Dec 10 2024 at 11:10):

Nicola Peduzzi said:

speaking of autocomplete, wouldn't be possible for an editor to show a list after typing |> with all imported functions accepting the current type as first argument?

Coding in F# a lot lately. I was thinking, do people love the . because it's easy to type? What if editor would autocomplete the |> SomeModule.someFunction when user press . at the end of a variable…

Would that be OK, let's move on with ML-syntax, or people "dislike" even looking at the |> ... syntax?

view this post on Zulip Nicola Peduzzi (Dec 10 2024 at 11:13):

well I use a font with ligatures in my editor and the arrow made by |> is very pleasant to look at :joy:

view this post on Zulip Anton (Dec 10 2024 at 11:16):

still it'll be interesting to try out the new syntax. it will be a breaking change right?

I think we planned to allow both styles during the tryout phase

view this post on Zulip Alex Nuttall (Dec 10 2024 at 11:36):

Jasper Woudenberg said:

At the bottom of the proposal linked at the top of the thread, Richard links to a couple of larger code samples converted to the new style!

Thanks. Interestingly it doesn't contain many nested lambdas (link). It could be seen as a positive that the new style will discourage them, but in my opinion, it makes the code less readable where they are used

old:

parse = \str ->
    Str.toUtf8 str
    |> List.splitOn '\n'
    |> List.walkWithIndex (Dict.empty {}) \dict, row, y ->
        List.walkWithIndex row (Dict.empty {}) \rowDict, cell, x ->
            Dict.insert rowDict (Num.toI16 x, Num.toI16 y) (cell - '0')
        |> Dict.insertAll dict

new:

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(Dict.empty {}, \dict, row, y ->
        row.walkWithIndex(Dict.empty {}, \rowDict, cell, x ->
            rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
        )
        .insertAll(dict)
    )

I find it a bit harder to see the structure in the second version. It looks almost identical to javascript.

view this post on Zulip Alex Nuttall (Dec 10 2024 at 11:56):

In my converted example, Isn't there a need to disambiguate the arguments to the walkWithIndex function call from the parameters of the lambda? Javascript requires round brackets when there is more than one argument to a lambda

Dict.empty {}, \rowDict, cell, x ->

needs to be

Dict.empty {}, \(rowDict, cell, x) ->

but that conflicts with tuple syntax

view this post on Zulip Alex Nuttall (Dec 10 2024 at 12:00):

actually it should be:

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(Dict.empty {}, (\dict, row, y ->
        row.walkWithIndex(Dict.empty {}, (\rowDict, cell, x ->
            rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
        ))
        .insertAll(dict)
    ))

or avoiding multiple brackets on one line:

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(
        Dict.empty {},
        (\dict, row, y ->
            row.walkWithIndex(
                Dict.empty {},
                (\rowDict, cell, x ->
                    rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
                )
            )
            .insertAll(dict)
        )
    )

view this post on Zulip Anthony Bullard (Dec 10 2024 at 12:25):

witoldsz said:

What if editor would autocomplete the |> SomeModule.someFunction when user press . at the end of a variable…

I had this exact idea when I thought about this for my language a couple of years ago! Just have the LSP do all the work talked about here, but you still have explicit locality throughout the file

view this post on Zulip Anthony Bullard (Dec 10 2024 at 12:27):

The thing is, this is very different from the normal LSP (textDidChange -> Response) model, the response of which is usually an CompletionItem (adding some text after the cursor). I don't remember if you can rewrite before the cursor easily. That's usually Code Action territory

view this post on Zulip Anton (Dec 10 2024 at 12:51):

I've also had this idea a long time ago, but nobody else liked it then :big_smile:
Discoverability of that feature would not be great though

view this post on Zulip witoldsz (Dec 10 2024 at 12:58):

@Anthony Bullard after reading the google doc proposal, it is clear @Richard Feldman 's main motivation is to follow the experience of Gleam's creator Louis.

CORRECTION: it was not Richard's main motivation (as explained in following messages)

This might be interesting topic to dig into, if Gleam became popular because

view this post on Zulip Alex Nuttall (Dec 10 2024 at 13:01):

Gleam also has curly brace function bodies and brackets around parameters. Future-roc is a different mix

view this post on Zulip Richard Feldman (Dec 10 2024 at 13:03):

witoldsz said:

Anthony Bullard after reading the google doc proposal, it is clear Richard Feldman 's main motivation is to follow the experience of Gleam's creator Louis.

since this was not my main motivation, I'm curious what about the doc gave the misimpression that it was :thinking:

view this post on Zulip Anton (Dec 10 2024 at 13:06):

It's an easy story to remember

view this post on Zulip witoldsz (Dec 10 2024 at 13:07):

Richard Feldman said:

since this was not my main motivation

OK, I am sorry if I did misinterpret that part.

It was my impression, because from the very beginning you always said being friendly to newcomers is super important and I do appreciate this POV alot! So, going down that though I misinterpret you would otherwise not propose such a big shift in the syntax. My bad.

view this post on Zulip Richard Feldman (Dec 10 2024 at 13:16):

it's all good, no worries!

view this post on Zulip witoldsz (Dec 10 2024 at 13:19):

To be completely honest: as much as I do not like that syntax, if it was the mean to reach wide audience, I think I would have to agree. With tears in my eyes, but still.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 13:23):

I feel your pain @witoldsz , but I'm excited because Roc is moving syntactically to being very simiilar to my now-defunct language that I never completed.

view this post on Zulip Alex Nuttall (Dec 10 2024 at 13:24):

Alex Nuttall said:

actually it should be:

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(Dict.empty {}, (\dict, row, y ->
        row.walkWithIndex(Dict.empty {}, (\rowDict, cell, x ->
            rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
        ))
        .insertAll(dict)
    ))

or avoiding multiple brackets on one line:

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(
        Dict.empty {},
        (\dict, row, y ->
            row.walkWithIndex(
                Dict.empty {},
                (\rowDict, cell, x ->
                    rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
                )
            )
            .insertAll(dict)
        )
    )

What are people's opinion on that the formatter should do here? Should it enforce the second style? I think that's what prettier would do

view this post on Zulip Anton (Dec 10 2024 at 13:28):

I've suggested that before ( without the parenthesis) but people did not like it because it leads to more indentation

view this post on Zulip witoldsz (Dec 10 2024 at 13:33):

Both do not look good… :( The main problem is embedding lambdas. Kotlin tried to solve it, I don't know – maybe Kotlin's style to drop lambda out of parens would help here…

P.S.1. Elm's formatter has that one distinct feature to allow first argument in the same line, so it would be a mix first and second version.

P.S.2. I found out it is sometimes good to do exactly opposite to what "Prettier" does, to help with the layout.

view this post on Zulip Norbert Hajagos (Dec 10 2024 at 13:34):

I don't think the syntax is ugly, and so do many here. I do prefer the whitespace syntax too, but almost all of my programmer friends think I'm a goblin because I use a language that doesn't look like javascript or python. Maybe I am, but that's not the point.
"I don't care about these esoteric languages, I just want something that works well. C# does everything I need anyway!" -says my friend who only ever programmed in C#. A lot of people won't look into the semantics of the language if it "looks weird". The creator of gleam actually said that switching to parens and braces made a lot of people suddenly like the language. The fact that you are here in a language community says to me that you are more invested in your craft than most. For wide-spread adoption tho, I think we need this.
To be clear, I'm not bittersweet about this. Static dispatch methods completely sold the syntax for me. Without those, I wouldn't like this syntax change though.

view this post on Zulip Alex Nuttall (Dec 10 2024 at 13:35):

witoldsz said:

P.S.1. Elm's formatter has that one distinct feature to allow first argument in the same line, so it would be a mix first and second version.

Could you edit to example to show the result?

view this post on Zulip witoldsz (Dec 10 2024 at 13:38):

Elm would allow (but not enforce) something like this:

    .walkWithIndex(Dict.empty {},
        (\dict, row, y ->
            row.walkWithIndex(Dict.empty {},
                (\rowDict, cell, x ->

view this post on Zulip Richard Feldman (Dec 10 2024 at 13:41):

witoldsz said:

Both do not look good… :( The main problem is embedding lambdas. Kotlin tried to solve it, I don't know – maybe Kotlin's style to drop lambda out of parens would help here…

I'm not familiar with this aspect of Kotlin - how does it work?

view this post on Zulip witoldsz (Dec 10 2024 at 13:49):

Kotlin do 2 things to help with lambdas:

I am trying to figure out anything more I can say positive about Kotlin :thinking: well… nothing comes to my mind at the moment…

view this post on Zulip Anthony Bullard (Dec 10 2024 at 13:55):

@witoldsz That's interesting, you say that. I work at a Fortune 500 that is built on Java and a LOT of Java devs pine for writing Kotlin. But a lot of the deficiencies of Kotlin you identify it shares with Java (some it may make worse with extensions). My biggest bugaboo (with Java, Kotlin, and THIS proposal) is the lack of locality in the source. Many languages aim to have features that save typing when you are using an IDE. I want a language that is easy to read whether it's in a full-featured IDE, a text editor with just syntax highlighting, or on GitHub - so I prefer _very strong_ locality.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 13:56):

And I do _somewhat_ worry that static dispatch will really turn into Java style "one type(class) per file" organization. Or really it'll become Scala without inheritance or Java's ecosystem.

view this post on Zulip witoldsz (Dec 10 2024 at 13:58):

example from Kotlin main page, "functional style" section

    val frequentSender = messages
        .groupBy(Message::sender)
        .maxByOrNull { (_, messages) -> messages.size }
        ?.key

I can't believe I am pasting Kotlin code in Roc discussion list :open_mouth:

view this post on Zulip witoldsz (Dec 10 2024 at 14:03):

Anthony Bullard said:

really it'll become Scala

I think it would be extremely difficult to brake and waste Roc so many times, so it could be compared to something like Scala. Not with the Richard's attitude towards being a friendly language!

view this post on Zulip Anthony Bullard (Dec 10 2024 at 14:04):

I was also about to express a worry about this being a move that might necessitate a move towards nominal typing, I read the doc again, and see that Richard has figured out the "what if I can't tell the type without an annotation" problem with methods. :rofl:

view this post on Zulip Anthony Bullard (Dec 10 2024 at 14:06):

witoldsz said:

I think it would be extremely difficult to brake and waste Roc so many times, so it could be compared to something like Scala. Not with the Richard's attitude towards being a friendly language!

Let me be clear , I specifically mean a language with nominal typing that organizes application around a single type per file where the data and methods are strongly coupled and co-located. instead of a "case class" or "data class" with instance and static methods, it'll be type aliases with functions that operate as methods.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 14:07):

Also, I _loved_ Scala coming from writing Java, PHP, and Javascript backends before :-P

view this post on Zulip Anthony Bullard (Dec 10 2024 at 14:09):

It's the reason I'm an FP guy now - it actually lead me directly to Elm - and then Haskell, Erlang, Elixir, F# - and then to trying to design and implement my own language Chakra - and now here to Roc.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 14:10):

It was similar enough to what I knew to make me comfortable, but led me towards functional concepts. And once you go down that rabbit hole - there is no coming back. Unless you are Dave Farley. Hell, even Uncle Bob writes in Clojure now

view this post on Zulip Alex Nuttall (Dec 10 2024 at 14:50):

I don't think the new syntax is ugly (nor do I think that formatted javascript is, necessarily), but I do think whitespace calling does a better job at visually expressing the structure of callback-heavy code

view this post on Zulip Eli Dowling (Dec 10 2024 at 14:58):

Anthony Bullard said:

The thing is, this is very different from the normal LSP (textDidChange -> Response) model, the response of which is usually an CompletionItem (adding some text after the cursor). I don't remember if you can rewrite before the cursor easily. That's usually Code Action territory

And yet totally possible!!
And why shouldn't we?
I have certainly thought the same thing, particularly early on in my FP journey, before I was frankly too drowning in the coolaid to mind... It's so annoying how easy it is to forget all the paper cuts!

view this post on Zulip Anthony Bullard (Dec 10 2024 at 15:00):

I think we’d be the first LSP to this in FP land

view this post on Zulip Eli Dowling (Dec 10 2024 at 15:14):

I think @Alex Nuttall's comparison perfectly captures why I personally don't love the static dispatch syntax.
Yeah, overall, I think it may be worth it, the familiar-to-the-punters syntax (which I don't really like), and the slightly increased ease of implementing abilities + inherently getting an equivalent with type parameters.

But man, that abilities code is just much harder to read. When most of what I do is read code... that kind of sucks.
And yeah, if the lsp added type annotations, that wld help a lot, but it certainly wouldn't help within a PR.

I guess my thought is, we can use an editor to make code easier to write, we can provide really intelligent completions and all that. but when I'm reviewing a PR, I don't have a language server, and that's often when quickly reading and understanding the code matters most. In that scenario, clearly tracking the flow of types and the purpose of functions, helps a lot, and static dispatch makes that meaningfully harder.

eg: inserting into a list can have totally different performance characteristics to inserting into a dict. And that is just much less clear in example two.

view this post on Zulip Eli Dowling (Dec 10 2024 at 15:19):

Anthony Bullard said:

I think we’d be the first LSP to this in FP land

I think so too, and yet it seems so obvious... maybe it's harder than we think.
I don't think it's actually possible right now with the current roc compiler. The parser not being able to handle the hanging pipe may break everything. Plus the way type errors are currently just "type is incompatible" rather than "This is type A, and later we expect type B"
But it could certainly be done, and improving both of those is something that has to be done at some point anyway.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 16:04):

I think it would be more “I have an identifier or literal, then I press dot after” type of thing that could then wrap it in a function call with the other parameter templated out

Like

”hello”. would give you a list of Str methods. Selecting len would result in replace the string literal with Str.len “hello”

view this post on Zulip Anthony Bullard (Dec 10 2024 at 16:06):

If that existed :joy:

view this post on Zulip Anthony Bullard (Dec 10 2024 at 16:08):

I think it would be more “I have an identifier or literal, then I press dot after” type of thing that could then wrap it in a function call with the other parameter templated out

Like

[1,2,3]. would give you a list of List methods. Selecting len would result in replace the list literal with List.len [1,2,3] or [1,2,3] |> List.len

view this post on Zulip Anthony Bullard (Dec 10 2024 at 16:09):

And for |> you can backtrack the pipe from the text so that it parses and lets you get the type and then you could use that to get a list when the > is typed

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:07):

One general comment that is not true for all code, but for a decent chunk. Related to these comments above:

Interestingly it doesn't contain many nested lambdas

I do think whitespace calling does a better job at visually expressing the structure of callback-heavy code

While I totally agree that whitespace does better with callback heavy code, I don't think that callback heavy code is general seen as good architecture even in functional languages. In some cases, it is highly important, but it is still generally advised to avoid callback heavy code. It is almost always considered harder to read and reason about. Part of this and related proposals is trying to remove the need for callback heavy code in more locations. For loops and purity inference being two great examples that remove callbacks.

view this post on Zulip Alex Nuttall (Dec 10 2024 at 17:33):

Yes that's fair, I suppose I should be writing the future roc example like this:

parse = \str ->
    var dict_ = Dict.empty()

    for (row, y) in str.toUtf8().splitOn('\n').enumerate()
        for (cell, x) in row.enumerate()
            dict_ = dict_.insert((Num.toI16(x), Num.toI16(y)), cell - '0')

    dict_

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:37):

Wow, that code really almost feels like rust now.....weird

view this post on Zulip jan kili (Dec 10 2024 at 17:37):

Or

parse = \str ->
    str.toUtf8
    .splitOn('\n')
    .walkWithIndex(Dict.empty {}, parseRow)

parseRow = \dict, row, y ->
    row.walkWithIndex(Dict.empty {}, parseCell)
    .insertAll(dict)

parseCell = \rowDict, cell, x ->
    rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:39):

Probably want this:

parseRow = \dict, row, y ->
    row.walkWithIndex(dict, parseCell)

Not sure why I feel it is necessary to fix perf in an example like this, but it feels required to me.....

view this post on Zulip Alex Nuttall (Dec 10 2024 at 17:40):

That mistake is from my original example btw :embarrass: which should be

parse = \str ->
    Str.toUtf8 str
    |> List.splitOn '\n'
    |> List.walkWithIndex (Dict.empty {}) \dict, row, y ->
        List.walkWithIndex row dict \rowDict, cell, x ->
            Dict.insert rowDict (Num.toI16 x, Num.toI16 y) (cell - '0')

view this post on Zulip jan kili (Dec 10 2024 at 17:41):

Perf it up! I'll also try making it more Roc-thonic:

parse = \str ->
    str
    .toUtf8
    .splitOn('\n')
    .walkWithIndex(Dict.empty {}, parseRow)

parseRow = \dict, row, y -> row.walkWithIndex(dict, parseCell)

parseCell = \rowDict, cell, x -> rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:41):

ah yeah, guess I missed it in that denser block of the original code

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:42):

just have to delete .insertAll(dict) and that should be all good.

view this post on Zulip jan kili (Dec 10 2024 at 17:42):

I really really love Roc's whitespace and |>, but I love de-anonymizing multiline lambdas even more.

view this post on Zulip jan kili (Dec 10 2024 at 17:43):

Brendan Hansknecht said:

just have to delete .insertAll(dict) and that should be all good.

fixed

view this post on Zulip Alex Nuttall (Dec 10 2024 at 17:43):

I actually prefer more inlining in this narrow category. Adding function names just seems like indirection

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 17:44):

I love de-anonymizing multiline lambdas even more.

I find that I hit a mix here. One thing I really find annoying is dealing with naming (if I make it a top level) or dealing with variable collisions (If I nest the named lambda in the current function)

view this post on Zulip jan kili (Dec 10 2024 at 17:44):

Alex Nuttall said:

I actually prefer more inlining in this narrow category. Adding function names just seems like indirection

Now that your example doesn't necessarily contain multiline lambdas, I'm ambivalent in this case!

view this post on Zulip jan kili (Dec 10 2024 at 17:46):

With that bug fixed, here it is inlined:

parse = \str ->
    str
    .to_utf8
    .split_on('\n')
    .walk_with_index(Dict.empty {}, (\dict, row, y ->
        row.walk_with_index(dict, (\row_dict, cell, x ->
            row_dict.insert((Num.to_i16 x, Num.to_i16 y), cell - '0'))
        ))
    ))

Edit: with snake case, for fun!

view this post on Zulip jan kili (Dec 10 2024 at 17:47):

If those lambdas started getting longer and/or you start adding .andAlsoFoo() after them, then I prefer naming them so you can keep each function primarily at one level of abstraction.
Note: I'm not trying to bikeshed, just trying to hypothesize/test that the new syntax wouldn't introduce issues in this case.
Edit: Oops, that wasn't what you were discussing - nevermind! My comments on naming stand, but now they feel less relevant. Accidental bikeshedding is the most infectious bikeshedding!

view this post on Zulip Alex Nuttall (Dec 10 2024 at 17:53):

The whole lambdas actually need to be parenthesised, so:

parse = \str ->
    str.toUtf8()
    .splitOn('\n')
    .walkWithIndex(
        Dict.empty {},
        (\dict, row, y ->
            row.walkWithIndex(
                dict,
                (\rowDict, cell, x ->
                    rowDict.insert((Num.toI16(x), Num.toI16(y)), cell - '0')
                )
            )
        )
    )

view this post on Zulip jan kili (Dec 10 2024 at 17:55):

Ohhh gotcha, sorry I only skimmed your questions above. I'll add those missing parens and strikethrough my commentary that was answering a question you didn't ask.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 17:57):

Brendan Hansknecht said:

Wow, that code really almost feels like rust now.....weird

Or even Go, the way you have reassign a slice to its var after an append

view this post on Zulip Richard Feldman (Dec 10 2024 at 17:59):

with snake_case it would be:

parse = \str ->
    var dict_ = Dict.empty()

    for (row, y) in str.to_utf8().split_on('\n').enumerate() do
        for (cell, x) in row.enumerate() do
            dict_ = dict_.insert((x.to_i16(), y.to_i16()), cell - '0')

    dict_

view this post on Zulip Anthony Bullard (Dec 10 2024 at 17:59):

Why do lambdas need to be parenthesizsed?

view this post on Zulip Richard Feldman (Dec 10 2024 at 18:00):

incidentally, this is an example of a use case where I think the imperative style is easier to follow

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

I prefer reading the nested for version to the nested walkWithIndex version

view this post on Zulip Alex Nuttall (Dec 10 2024 at 18:02):

Anthony Bullard said:

Why do lambdas need to be parenthesizsed?

maybe they don't. My thought was that this list of arguments is ambiguous:
Dict.empty {}, \dict, row, y

view this post on Zulip jan kili (Dec 10 2024 at 18:03):

Richard Feldman said:

I prefer reading the nested for version to the nested walkWithIndex version

I wonder if that might hold true for any foo_after = foo_before.walk... block with nesting (or without).

view this post on Zulip jan kili (Dec 10 2024 at 18:04):

I expect a lot less walking in general.

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

maybe they don't. My thought was that this list of arguments is ambiguous:
`Dict.empty {}, \dict, row, y`

Yeah, I don't think it is strictly required, but it is a lot less confusing if it is parenthesized.

Like this only has one valid parsing. z has to be an arg to the original function. Otherwise, it would be trying to return the y, z tuple which requires parens as (y, z)
Dict.empty(), \dict, row, y -> y, z

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

That said, it becomes a horrid syntax for humans, so I would guess that we will require parens in practice. If we allowed avoiding them, it would probably only be for multiline llambdas passed as args (which use indentation to tell body so don't need parens)

view this post on Zulip Richard Feldman (Dec 10 2024 at 18:27):

it would actually need to be Dict.empty(), not Dict.empty {}, but yeah :big_smile:

view this post on Zulip Alex Nuttall (Dec 10 2024 at 19:13):

random question about dot syntax: in a world without pipes, will it be possible to apply tags like this?

list.append(el).Ok()

view this post on Zulip Alex Nuttall (Dec 10 2024 at 19:17):

or without the final brackets

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 19:33):

This is where I fell like parens or still using |> make the most sense, but I guess it could be a method call as well...
Ok(list.append(el)) or list.append(el) |> Ok

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 19:34):

This is something I definitely would need to play with in practice

view this post on Zulip Alex Nuttall (Dec 10 2024 at 19:46):

so potentially, mixed:

thing
.method()
.pass_to(\x -> x)
|> Ok()

prefix only:

Ok (
    thing
    .method()
    .pass_to(\x -> x)
)

or method:

thing
.method()
.pass_to(\x -> x)
.Ok()

view this post on Zulip Alex Nuttall (Dec 10 2024 at 19:51):

I suppose it could be just .pass_to(Ok)

view this post on Zulip Alex Nuttall (Dec 10 2024 at 19:52):

or call it .tag()

view this post on Zulip Richard Feldman (Dec 10 2024 at 19:52):

yeah certainly .pass_to would Just Work

view this post on Zulip Richard Feldman (Dec 10 2024 at 19:52):

it's an interesting idea to offer alternatives like .Ok() - seems worth a new #ideas thread to discuss!

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 20:06):

Oh yeah, we have pass_to. That looks nice

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 20:07):

but, .Ok() definitely could still make sense...

view this post on Zulip Eli Dowling (Dec 11 2024 at 01:39):

Or compared with come kind of pass to operator:

thing
.method()
.pass_to(\x -> x)
.pass_to(Ok)
thing
.method()
.> (\x -> x)
.> Ok

I think an operator looks cleaner.

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

until you decide that the line isn't that long and it want it inline:

thing.method().pass_to(\x -> x).pass_to(Ok)
thing.method().>(\x -> x).>Ok

view this post on Zulip Eli Dowling (Dec 11 2024 at 01:45):

And just for completeness sake, current syntax + loops

parse = \str ->
    var dict_ = Dict.empty()

    for (row, y) in str |> Str.to_utf8 |> List.split_on |> enumerate do
        for (cell, x) in row |> enumerate do
            dict_ = dict_ |> Dict.insert (Num.to_i64 x, Num.to_i16 y) (cell - '0')

    dict_

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

Not half bad. I think if the hightlighting was for roc, it would look pretty good

view this post on Zulip Eli Dowling (Dec 11 2024 at 01:48):

Brendan Hansknecht said:

until you decide that the line isn't that long and it want it inline

I actually still don't mind it tbh, it's a little odd.. but so is the pipe till you get used to it

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:16):

Richard Feldman said:

with snake_case it would be:

parse = \str ->
    var dict_ = Dict.empty()

    for (row, y) in str.to_utf8().split_on('\n').enumerate() do
        for (cell, x) in row.enumerate() do
            dict_ = dict_.insert((x.to_i16(), y.to_i16()), cell - '0')

    dict_

Wouldn’t you have to do Dict.empty({}) still? It still takes a param, I would think () would mean “the only arg is the method receiver “

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

@Anthony Bullard you're right, that's why some of Richard's recent examples have actually been Dict.empty()

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:29):

Which opens a door to zero argument functions, which I think is a good change

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:29):

How would you declare that?

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:29):

I'm gonna make a topic about it

view this post on Zulip Luke Boswell (Dec 11 2024 at 02:29):

Isn't it just sugar for {}

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:30):

That could work

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:30):

In a purely functional language a zero argument function is a called a constant :wink:

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

Yes, but we have zero arg functions inpractice today, like Dict.empty {}

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

The only reason it takes an arg is due to monomorphization

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

And the arg contains no data, so it really should be zero arg

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:32):

Ah, because a constant needs to have a fully specialized type?

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:33):

A top-level one that is

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

yeah

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:33):

I’m just getting a hair worried about all this sugar

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

Ayaz's wrote a doc on that a while back. On why we should keep it this way and require the function.

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

I’m just getting a hair worried about all this sugar

Yeah, roc is a simple core with a lot of evolving sugar

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

That it is also important to see that some sugar is being simplified and directly added to the language like early returns and a more robust try

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:35):

As this seems a little wonky. So the rule is: if a function has one argument, and it is {}, then it can be called with no args when used as a function?

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

Yeah, just like if a function returns one value and it is {}, it can be ignored

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

thus you can do:

Stdout.line! "test"

instead of:

{} = Stdout.line! "test"

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:35):

Ok. It’s just got to feel natural and intuitive.

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:36):

Yeah, {} is our Unit type which has special handling in some functional languages like Scala, F#, and I think OCaml

view this post on Zulip Anthony Bullard (Dec 11 2024 at 02:37):

I guess only Scala calls it Unit

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:42):

Ayaz's wrote a doc on that a while back. On why we should keep it this way and require the function.

I'm not Ayaz, but I think that the issue is more to do with us not wanting constant values to be monomorphized, not that functions need to have at least one argument.

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:42):

Meaning we should be good to go with zero arg functions if we were able to define them syntactically

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:43):

Brendan Hansknecht said:

thus you can do:

Stdout.line! "test"

instead of:

{} = Stdout.line! "test"

But this is a good orthogonal feature that makes swallowing the Dict.empty({}) -> Dict.empty() pill easier

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

us not wanting constant values to be monomorphized

constants are monomorphized. It is just that we want them to be monomorphized to exactly one specialization instead of opening the can of worms and weird bugs that come from a constant turning into multiple different specializations. Definitely saw some surprising behaviour when a number constant could be a U8, I32, and F32 at the same time.

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:50):

Oh yeah, that was the idea

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:51):

Okay yeah, when I say "not monomorphized" I mean "they get one specialization"

view this post on Zulip Sam Mohr (Dec 11 2024 at 02:51):

I appreciate the clarification!

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:08):

well Dict is one of the ones we can make work actually

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:08):

it just hasn't been done yet

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:08):

Ayaz made it work for numbers, and we can do the same thing for all the other builtins

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:08):

so in the future it can be dict = Dict.empty

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:09):

Anthony Bullard said:

As this seems a little wonky. So the rule is: if a function has one argument, and it is {}, then it can be called with no args when used as a function?

oh I actually think once we go to parens-and-commas calling style we should just have an actual concept of zero-argument functions

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:09):

zero-arg functions were rare back when we only had a concept of pure functions, but they come up all the time now that we have effectful functions :big_smile:

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:10):

basically any value that used to have the type Task is now a zero-arg effectful function

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:19):

and foo() is well-understood across languages to be a call to a zero-arg function

view this post on Zulip Brendan Hansknecht (Dec 11 2024 at 05:21):

so in the future it can be dict = Dict.empty

i actually think it would be nicer to keep it consistent with any userspace containers and just do Dict.empty().

view this post on Zulip Brendan Hansknecht (Dec 11 2024 at 05:21):

Not like the two parens matter much at all

view this post on Zulip Richard Feldman (Dec 11 2024 at 05:22):

fair point!

view this post on Zulip Ayaz Hafiz (Dec 11 2024 at 18:48):

there's also a weird thing of where to draw the line. You can go one way or the other and say okay, all constants can be generic, or constants have one type. Right now numbers are a special carveout in that x = 1 is generic, but x = A 1 is not. It's probably significantly easier to understand if it goes entirely one way or the other, and I'm not sure there is a need for constants to be generic.

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

Yeah, I think they should all be one type. I think it is fine to require x = 1 and y = 1.0.

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

I would assume most uses as two types are accidental

view this post on Zulip Richard Feldman (Dec 11 2024 at 18:52):

that wouldn't help, actually...1.0 is still generic :big_smile:

view this post on Zulip Richard Feldman (Dec 11 2024 at 18:52):

(it's generic over the 3 different fractional types we have)

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

Sure

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

I just mean that you have to write out the constant twice cause you are using it at two different concrete types

view this post on Zulip Richard Feldman (Dec 11 2024 at 18:53):

ah, gotcha

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

We don't automatically cast number types, so it is kinda strange to automatically cast numberic constants (though if we removed it, maybe I would find out I really miss it in practice)

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

I guess the difference is that we can make sure numeric constants are safe as a type at compile time

view this post on Zulip Ayaz Hafiz (Dec 11 2024 at 18:55):

The other difference is they're not really casts, they are instances x_1: U32 = 1, ..., x_n: ... = 1 for all unique types of x

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

Yeah, it is really n copies of the value in each different type. But from a user perspective, I think it is an implicit cast

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:57):

Doing the work of previewing Static Dispatch syntax (along with the proposed new lambda syntax) and I had some questions, here's what I have so far (this is only the beginning of this module):

Block : [
    Free(U64),
    File(U64, U64),
]

processInput : Str -> List Block
processInput = |str|
    dbg "processInput"
    bytes = Str.toUtf8(str.trim())
    blocks = List.withCapacity(bytes.len())
    List.walkWithIndex(
        bytes,
        blocks,
        |bs, byte, index|
            byteValue =
                Str.fromUtf8([byte])
                    .withDefault("???")
                    .toU64
                    .withDefault(Num.maxU64)

            if Num.isEven(index) then
                List.append(bs, File(index // 2, byteValue))
            else if byteValue > 0 then
                List.append(bs, Free(byteValue))
            else
                bs
    )
  1. Does any of this look "wrong"?
  2. I'm assuming dbg stays a statement and not a function?
  3. What's the right style for chaining Static Dispatch calls?

view this post on Zulip Luke Boswell (Dec 18 2024 at 22:59):

|str| -> I thought we were talking about not having the arrow here, only in the type

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:59):

That was a mistake, fixed

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:01):

A small note, as someone predicted, when I am doing this and I go back to the call site of this function, I kinda want to wrap the List(Block) type in an opaque ref so I can change this:

getPartOne = |str|
    PartOne.processInput(str)
    |> PartOne.compact
    |> PartOne.checksum

to this:

getPartOne = |str|
    PartOne.processInput(str)
        .compact()
        .checksum()

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:02):

And in this snippet:

Str.fromUtf8([byte])
                    .withDefault("???")
                    .toU64
                    .withDefault(Num.maxU64)

The actual Module that's being dispatched to changes three times - and it's not obvious reading the source alone that's happening, or what Modules are being used . No judgement, just an observation

view this post on Zulip Luke Boswell (Dec 18 2024 at 23:04):

and it's not obvious reading the source alone that's happening, or what Modules are being used

Maybe we could have a naming convention... Str.fromUtf8 might be clearer as Str.tryFromUtf8

view this post on Zulip Luke Boswell (Dec 18 2024 at 23:05):

Str.tryFromUtf8([byte])
    .withDefault("???")
    .tryToU64
    .withDefault(Num.maxU64)

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:05):

Yeah, I think we should have _some_ convention for functions not in the Result module that return a Result

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:16):

I also, for some reason I don't understand, find myself now wanting when, else, and even multiline lambas wanting an end keyword. Maybe because things are becoming a little Elixir-y?

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:22):

with snake_case and using methods in a few more places:

Block : [
    Free(U64),
    File(U64, U64),
]

process_input : Str -> List Block
process_input = |str|
    dbg "processInput"
    bytes = str.trim().to_utf8()
    blocks = List.with_capacity(bytes.len())
    bytes.walk_with_index(
        blocks,
        |bs, byte, index|
            byte_value =
                Str.from_utf8([byte])
                    .with_default("???")
                    .to_u64()
                    .with_default(Num.max_u64)

            if index.is_even() then
                bs.append(File(index // 2, byte_value))
            else if byte_value > 0 then
                bs.append(Free(byte_value))
            else
                bs
    )

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

Oh shoot, snake case

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:24):

with var and for:

Block : [
    Free(U64),
    File(U64, U64),
]

process_input : Str -> List Block
process_input = |str|
    dbg "processInput"
    bytes = str.trim().to_utf8()
    var blocks_ = List.with_capacity(bytes.len())

    for (index, byte) in bytes.iter().enumerate() do
        byte_value =
            Str.from_utf8([byte])
            .with_default("???")
            .to_u64()
            .with_default(Num.max_u64)

        if index.is_even() then
            blocks_ = blocks_.append(File(index // 2, byte_value))
        else if byte_value > 0 then
            blocks_ = blocks_.append(Free(byte_value))

    blocks_

view this post on Zulip Luke Boswell (Dec 18 2024 at 23:24):

nvm

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:25):

no, but it could be:

blocks = bytes.len().pass_to(List.with_capacity)

view this post on Zulip Luke Boswell (Dec 18 2024 at 23:26):

It's probably clearer the first way, I was a little confused

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

Oh yeah, the magical pass_to

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

I don't think I fully understand the reason for that

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

Is it just to get rid of |>?

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:27):

it's to have a |> that works with . chaining

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

e.g. compare:

Str.from_utf8([byte])
.with_default("???")
.to_u64()
.pass_to(transform_result_somehow)
.with_default(Num.max_u64)
Str.from_utf8([byte])
.with_default("???")
.to_u64()
|> transform_result_somehow
.with_default(Num.max_u64)

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

the second thing doesn't actually work because of precedence

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

it would actually be equivalent to

Str.from_utf8([byte])
.with_default("???")
.to_u64()
|> transform_result_somehow.with_default(Num.max_u64)

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

which wouldn't be what you want

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:29):

So every single Module that exposes a statically dispatchable type needs to implement pass_to, or this is some free compiler magic?

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:29):

free compiler magic

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:29):

I guess I should read the proposal again (like the 4th time :rofl:)

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:29):

I don't think it was in there, I think we arrived at it in the discussion afterwards

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:30):

but basically it works exactly like |>

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:30):

Oh, ok. I've definitely seen it thrown about.

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:30):

it's syntax sugar that's designed to look like a method

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:30):

and it can take multiple arguments, which get passed through

view this post on Zulip Joshua Warner (Dec 18 2024 at 23:30):

IMO pass_to eats a lot of weirdness budget

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:30):

Roc dropping |> right as JS is adding it :smile:

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:30):

so .pass_to(foo, arg1, arg2) is equivalent to |> foo arg1 arg2

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:30):

I really wish we could just resolve the precedence issue

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:30):

Joshua Warner said:

IMO pass_to eats a lot of weirdness budget

I really don't think it will haha

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:31):

.pass_to() feels like an operator wearing a function's clothes

view this post on Zulip Joshua Warner (Dec 18 2024 at 23:31):

We shall see!

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:31):

we'll see though!

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:31):

We shall see

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:31):

Just wanted to join in the fun ;-)

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:32):

Anthony Bullard said:

I also, for some reason I don't understand, find myself now wanting when, else, and even multiline lambas wanting an end keyword. Maybe because things are becoming a little Elixir-y?

Is someone gonna get mad If I actually suggest this in an #ideas convo?

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:32):

nope, go for it!

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:32):

first rule of #ideas is that any idea is fair game :big_smile:

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:33):

they're just ideas!

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:33):

Ok, I'll do that tonight. After hopefully my new docs template gets merged ;-) ;-)

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

interestingly, if you wanted to, you could make this whole function one walk:

Block : [
    Free(U64),
    File(U64, U64),
]

process_input : Str -> List Block
process_input = |str|
    str.trim().to_utf8().walk_with_index(
        [],
        |bs, byte, index|
            byte_value =
                Str.from_utf8([byte])
                    .with_default("???")
                    .to_u64()
                    .with_default(Num.max_u64)

            if index.is_even() then
                bs.append(File(index // 2, byte_value))
            else if byte_value > 0 then
                bs.append(Free(byte_value))
            else
                bs
    )

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:36):

you miss out on with_capacity that way though

view this post on Zulip Anthony Bullard (Dec 18 2024 at 23:36):

Yeah, that's why I skipped it. Even though it probably doesn't really matter for AOC

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:37):

oh can this be a map_with_index? :thinking:

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:38):

ah not quite

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:38):

because some get skipped

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:40):

iterators would make this possible: (not saying it's the best way to write it or anything)

Block : [
    Free(U64),
    File(U64, U64),
]

process_input : Str -> List Block
process_input = |str|
    str.trim().to_utf8().iter().enumerate().keep_oks(
        |(index, byte)|
            byte_value =
                Str.from_utf8([byte])
                    .with_default("???")
                    .to_u64()
                    .with_default(Num.max_u64)

            if index.is_even() then
                Ok(File(index // 2, byte_value))
            else if byte_value > 0 then
                Ok(Free(byte_value))
            else
                Err({})
    ).pass_to(List.from_iter)

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:43):

I think I like the for version the best of these

view this post on Zulip Richard Feldman (Dec 18 2024 at 23:44):

by a small margin

view this post on Zulip jan kili (Dec 18 2024 at 23:48):

Iterators solve list capacity planning? I haven't learned about iterators since Python.

Ah, I misread what you meant by "make this possible".

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 23:54):

No, they don't

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 23:55):

They sometimes can guess capacity, but it isn't a general solution

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 23:55):

For example keep_oks probably should not guess capacity

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 23:55):

Cause it might way over allocate


Last updated: Jun 16 2026 at 16:19 UTC