I think that the current syntax of Roc is beautiful. While I’ve been known to say that the move to PNC envisioned as necessary to the Static Dispatch proposal is probably a good move for adoption, it does threaten to lose a lot of that beauty.
So I’d like to use this thread to explore alternative syntactic constructs that could be used for the static dispatch feature within the current syntax.
I’ll start by suggesting an obvious choice, that has deep memory in programming and doesn’t not fall far outside the existing syntactic space: ->.
This would be a new infix operator that must have NO white space between itself and its operands.
[1,2,3]->map \n -> n + -1
aSet->insert 42
23->to_u64
It would apply the left operand to the right operand as its first argument, additional arguments would follow. If the function is unary, the application would be complete.
And it has to be a static dispatch method.
So basically a different form of pipe
Riffing of the idea a little, I assume the NO white space is so it's discernable from other operators, why not just use double dash, that renders nicely in my editors.
[1,2,3] --> map \n -> n + -1
aSet --> insert 42
23 --> to_u64
Screenshot 2024-12-18 at 14.21.38.png
Brendan's comment made me think of the difference between an en and em dash from english. You use an em (longer) dash to break a sentence.
Brendan Hansknecht said:
So basically a different form of pipe
This is EXACTLY my thought, a “static dispatch pipe”
Luke Boswell said:
Riffing of the idea a little, I assume the
NO white spaceis so it's discernable from other operators, why not just use double dash, that renders nicely in my editors.[1,2,3] --> map \n -> n + -1 aSet --> insert 42 23 --> to_u64
I’m not sold on any one specific operator, : or :: could be used as well
Technically could even use a normal pipe and add a static dispatch sigil to the called method. That said, attaching to the core object probably makes it clearer
Also, I agree with the general feeling that these changes lose a lot of the feel and "soul" of current roc
Used an example from the static dispatch proposal
https://gist.github.com/lukewilliamboswell/546c391fc56bf2c825760d2d7fd9b3a4
static dispatch pipe could use double pipe
[1,2,3] ||> map \n -> n + -1
aSet ||> insert 42
23 ||> to_u64
I know that Rich is not a fan of operators over two characters, which this proposal has strongly leaned into. Roc doesn't currently have any, which helps with terseness.
These suggestions help preserve the aesthetic of current Roc, but are missing the big benefit of . being short and easy to type, as well as intuitive for people coming from other languages.
Maybe we can try to consider operators that are two or fewer characters and are easy to type?
I believe we've went through a lot of operators in one of the static dispatch proposal discussions.
I don't remember -> being mentioned. Besides ., -> and :: are the only operators I could see as a solution. To me:: would be confusing, since I think of OOP static methods on a class. But -> would work. It doesn't have spaces before it, so it is a 2 character operator. Not as easy as . , but would still satisfy the "lsp completion friendly" characteristic.
A beginner would confuse a 1 arg method for a field access tho, a la C / PHP.
# is this a costly method on a singly linked list,
# or a dirt cheap field access of a doubly linked list?
linkedList->last
I don't know how far the PnC syntax implementation is, but this would be a nice first iteration of the syntax that enables static dispatch. If we like it in anger, the last one. Personally I would be happy with both this and PnC .
(It feels weird to choose PHP syntax-hl for Roc :grinning_face_with_smiling_eyes: )
Since this aims to generally preserve whitespace calling/avoid parens while getting the benefit of type inference, maybe module inference would fill your wants?
allItems
|> _.mapTry \item -> validate item
|> _.len
I don't think just |> .len works because that's a field accessor.
I personally don’t understand stand-alone field accessors
They're good for data drilling in pipelines, or in mapping operations:
initialData
|> List.map .innerData
|> List.aggregate
|> .firstField
|> .secondField
I'm a big fan of this idea because you can also call functions in the normal style if you want:
_.add 1 2
(allItems->mapTry \item ->
validate item)->len
Is what I was thinking.
Parens only needed for disambiguation. Would not be needed for literals or identifiers
(allItems->mapTry |item| validate item)->len
With || lambas
initialData
|> List.map .innerData
|> List.aggregate
|> .firstField
|> .secondField
Would become
(((initialData->map innerData)->aggregate).firstField).secondField
I don't know about the parens here
I think it could be this
(initialData->map innerData)->aggregate.firstField.secondField
Or this
(initialData->map innerData)->aggregate
|> .firstField
|> .secondField
once you start chaining .a.b.c it starts looking like modules again
Personally I like the |> _.function
If we could have |> itself be "static dispatch aware" before a `.someMember name that would be awesome
And then there is NO NEW SYNTAX
It's just a "record field accessor" becomes that for a record, and if that type has no such fields (or is not a record) we fall back to static dispatch. Does that sound too complicated to implement @Sam Mohr ?
In that case Sam's example becomes
initialData
|> .map .innerData
|> .aggregate
|> .firstField
|> .secondField
Maybe all the "magic" is happening in the accessor? So we now have static dispatch and the beginning of "terse lambas"
That would also distinguish local functions which don’t need the dot in front presumably
Yep
The "record field accessor" becomes instead the "type member accessor"
Would there not be a conflict there?
Say you write a function \bar -> .foo bar, is bar a {foo: a} or is it an opaque type that has a function called foo in the module?
Only if a record has both a field and a function in it's origin module with the same name
Good point, You'd have to treat a record having a field as it having the accessor for foo -> a
Which opens a can of worms about did we just allow structural abilities to rely on fields? Or did we just find a convenient workaround to people creating getters?
The type of that functions is sort of like "a -> b where a implements foo : a -> b"
Which conveniently enough is the same as just .foo itself
But then you sort of lose the readability of having record types with {}
I quite like this idea, I want to quickly put out the suggestion that we not use the pipe operator for static dispatch and instead use some other operator.
Given static dispatch inherently implies the pipe.
Eg:
.> For static dispatch
|> for pipe
initialData
.> map .>innerData
.> aggregate
|> .firstField
|> .secondField
I have to go to work, but I can't wait for Anton, Brendan, Luke, Sam, and Richard (maybe even Ayaz) to tell me that this won't work. But I think it's consistent with an interpretation of row polymorphism
Eli Dowling said:
I quite like this idea, I want to quickly put out the suggestion that we not use the pipe operator for static dispatch and instead use some other operator.
Given static dispatch inherently implies the pipe.Eg:
.> For static dispatch
|> for pipeinitialData .> map .>innerData .> aggregate |> .firstField |> .secondField
The idea is that the pipe IS NOT doing the static dispatch the accessor itself is
But I really gotta go :-)
Sure but now we are typing |> . Every function call... A pretty hard no from me as far as economics.
ergonomics* (though economy works too :sweat_smile:)
This isn’t a long term solution maybe and it’s not every function call
Only to those functions implemented in the types own originating module
You can still do Whatever.function arg
Or arg |> .function
And maybe the pipe isn’t necessary from a literal or identifier
arg.function should be unambiguous
Except it’s unclear now if it is field access or a function call
Does that matter? The type solver should take to such a function as Derin gave above a record with a foo field or a static dispatch function foo that is unary and returns the same type
To me this is about “how can we test static dispatch without PNC?”
And I’ve said a lot - but I know this might not work or be a good idea. Just trying to keep the conversation going :wink:
Anthony Bullard said:
Or arg |> .function
or just .function arg
but i still feel like there are too many ambiguties with this syntax. maybe it should be _.function for static dispatch and .function for record property access
One note is that .> something is more chainable (with good auto complete) than something like |> _.something
And I don't think that -> alone is chainable at all.
One of the biggest benefits of .something() is how easily chainable and auto complete friendly it is. Not to mention familiar.
I feel like I really need to convert a medium sized app over to parens and commas. I think the apps I write today don't use enough opaque types to take advantage of it. That's actually my biggest concern with static dispatch. It only works for opaque types and really promotes adding way more of them.
Yes and opaque types are basically nominal typing
Anthony Bullard said:
I have to go to work, but I can't wait for Anton, Brendan, Luke, Sam, and Richard (maybe even Ayaz) to tell me that this won't work. But I think it's consistent with an interpretation of row polymorphism
This is now the CPR scene from the Abyss. I don't expect success, but I am here to help you feel like you did what you could. And if you find life in a dying body, that's a great thing
Anthony Bullard said:
It's just a "record field accessor" becomes that for a record, and if that type has no such fields (or is not a record) we fall back to static dispatch. Does that sound too complicated to implement Sam Mohr ?
Might be a little weird to do, but doable
so the idea is that this:
initial_data
|> .map .inner_data
|> .aggregate
|> .first_field
|> .second_field
is a big improvement over this?
initial_data
.map(.inner_data)
.aggregate()
.first_field
.second_field
Not a big improvement - just a way to eat our static dispatch cake and have our PNC-free Roc too
to me, neither of these feels like today's Roc, but if I had to pick one or the other I'd pick the second one
And if SD doesn’t work it’s a small bit to abandon ship on
I mean believe me, I have plenty of affection for and fond memories of the current syntax, but I think as soon as you take away the module qualification it unavoidably feels like something different to me
and at that point I'd rather optimize for what feels like it's the best overall design, not something that's a compromise of the best design and the status quo I'm nostalgic for :big_smile:
That’s totally fair
for that code example, it's .first_field.second_field right? Not .first_field().second_field()
oops yeah, fixed!
Ok, I hope this conversation was helpful someway. I think PNC will carry the day. I will shed one single tear, and get ready for the new world with open eyes and an open heart.
Oh, and I'll mark this as resolved.
Anthony Bullard has marked this topic as resolved.
I think after we try PNC, we'll truly see the consensus.
I think it will take use in practice to really get the feel.
Yerah, that's why I am trying out the syntax in some non-trivial applications (my AOC 2024 repo)
Last updated: Jun 16 2026 at 16:19 UTC