This may have already been discussed, but my search didn't show anything.
Now that there's discussion about making Roc look more similar to mainstream languages, I want to bring up something that I've _personally_ found unnecessary in both Elm and now Roc: the backslash before a lambda. Even Java, the verbose language _par excellence_, supports the simple (x) -> x * 2 syntax. JavaScript supports x => x * 2. I see how the whitespace calling option might have made this a bit funky, e.g.
hello x -> x * 2
versus
hello \x -> x * 2
... but with parentheses it's really quite clear:
hello(x -> x * 2)
The backslash seems like a superfluous and confusing piece of syntax to me, since
-> _without_ a backslash in function type signatures, and-> operator should be sufficient to establish that it's a lambda/function?Backslash \x -> is still easier than lambda x: in Python, so I wonder why does Python need it?
I always wondered that as well, since it's incredibly verbose :laughing:
We will probably start the move to parentheses by supporting spaces and parentheses for function calls. We need backslashes to avoid ambiguity in space-gapped function calls, so anonymous functions there will need to keep the backslash, but we should be able to remove the backslashes everywhere else
i wonder then if backslash could become a more general thing that just does right associativity
The paren removal operator?
I think that idea should be a separate thread :big_smile:
@Daniel Schierbeck what would this look like in more scenarios? for example:
it would be ideal to see these scenarios side by side with status quo in the parens-and-commas syntax, to get a better idea of how they compare :big_smile:
@Richard Feldman OK, tried to write up a somewhat comprehensive list of examples: https://gist.github.com/dasch/b11324ec50a5de9e3e460996b795b6e0
I think the most tricky example was functions as record values, e.g. { update: \state, input -> foo state input }. However, that does point to something I _also_ find really weird about Roc, which is the asymmetry between how functions have been defined vs called – and I guess that's what the parens proposal also addresses? If so, wouldn't it make more sense to use { update: (state, input) -> foo state input }. Syntactically and conceptually, the arguments are being passed in as a tuple. If there's no direct support for currying, isn't that what's happening anyway? So backslash-less functions can be defined as e.g. x -> ... for single-argument functions, {} -> ... for zero-argument functions, or (x, y) -> ... for multi-argument functions. This seems by far the most readable to me.
Richard Feldman OK, tried to write up a somewhat comprehensive list of examples: https://gist.github.com/dasch/b11324ec50a5de9e3e460996b795b6e0
Most of those look good, in cases like below, I do have a strong preference for the backslash:
# current
people
|> List.map \p -> p.age
# proposed
people
|> List.map p -> p.age
it's really nice to have a clear boundary for the start of the function
Daniel Schierbeck said:
wouldn't it make more sense to use
{ update: (state, input) -> foo state input }.
I think parentheses around parameters could also mitigate the need to put parentheses around whole lambdas in bracket-calling discussed here. It's kind of the javascript solution (though brackets can be dropped for single parameters there)
@Anton how much is this important to you for parens-based function calling? It seems like your problem is with removing the backslash for space-based function calls, which we can simply avoid by only enabling this for parens-based calls
As much as parens around the args seems to help with ambiguity, I think it is a problem when used with tuples. JS doesn't have tuples so it's not a problem for them, but we do
gleam uses #() :man_shrugging:
@Anton how much is this important to you for parens-based function calling?
With parens it could indeed be a tuple argument or a start of a function, I don't like the added thinking
I don't think it is added thinking?
List.mapIndexed(item, index as i -> List.repeat(item, i))
Let's ignore space-delimited argument calling syntax, as it would go away in static dispatch land (unless you really think we shouldn't ignore it)
With just parens calling world, if we don't surround the argument list with parens, then you just need to look for the arrow
It's not quite as clear as backslash I agree, but it's cleaner
The benefit of backslash is that it uniquely defines the start of a function def
But if it isn't necessary, it's just an extra character to type (ignoring our historical bias of having used it in Roc for a few years)
Just to rehash my example from the other thread one more time, here it is with bracket params (and :(a, b) for tuples):
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(:(x.to_i16(), y.to_i16()), cell - '0'))
)
)
nitpick: Dict.empty {} -> Dict.empty()
This example isn't great because it has a lot of callbacks, but I think that's not the fault of the example
Otherwise, I think this is very readable
I think the bracket params help precisely with the readability of callbacks though. Otherwise you have this:
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((x.to_i16(), y.to_i16()), cell - '0'))
))
))
or this:
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.toI16(x), Num.toI16(y)), cell - '0')
)
)
)
)
That first example I think has redundant parentheses for what would be necessary for parsing:
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')
)
)
And sans redundant parens, it's pretty clear to me where the callbacks are (given that this Elm syntax highlighting makes the backslashes pretty obvious)
Seems reasonable, but opinions differed on the other thread about whether they should be required (even if not technically required)
Which thread?
Was actually only thinking of @Brendan Hansknecht's response here, and he specifically talks about allowing that case anyway
Also paren grouped params to start a function in most positions require backtracking to ensure it's not just a tuple
Would tuple arguments actually be a problem? We could require that in order to take in and deconstruct a tuple you need to wrap it in another layer of parens, e.g. ((x, y)) -> dist(x, y) or something; but when would you not just turn that into a function with 2 args, or if you really need to pass tuples, to do coords -> (x, y) = coords; dist(x, y)?
It's not for if the tuple is argument being destructured mainly (though that's also maybe a worse problem), but you have to first look for a lambda and if you don't find the -> directly after the parenthesized statement, backtrack and then look for a tuple
I think a different tuple syntax would be a plus if there were bracket params:
:(a, b)~(a, b)\(a, b) :laughing:A tuple operator like that seems very rare among popular programming languages, that makes me hesitant
I don't think swapping the backslash for a pair of parens is a plus. There is no need for the closing parens from a parsing point, since the end of the argument list is marked with ->. When I see a backslash, I know that's the start of a lambda. Seeing a ( doesn't tell me what is beginning there, i would have to look for the -> to confirm my suspicion. A different tuple operator would be strange, more so than the \ for lambdas.
Plus if we are adding more parens for function calling with static dispatch, I don't think the right syntax is to introduce even more. Maybe a different char, like Rust (Smalltalk :wink: ) styled blocks. I also remember a general positive feedback in one of the zulip discussion to swap || to or, which would free up the pipe char. Not advocating for them though, I love our current lambda syntax.
to_rows = \input, delim ->
List.split_on(input, delim)
to_rows = (input, delim) ->
List.split_on(input, delim)
to_rows = |input, delim|
List.split_on(input, delim)
Edit: converted to snake_case. Lots of future syntax change to keep in my head.
A general note: I think it is a bad idea to make it so we are passing tuples to functions by default. It has significant meaning to layout and thus performance.
So with parens around the args syntax, I think this would be required to pass a tuple as:
fn = ((field1, field2)) ->
...
Or, as mentioned above, pick a different tuple syntax.
But I think also as mentioned above, it is probably less surprising to pick a different function syntax
I've been sick all day, so I haven't caught up on the whole thread, but I thought the original idea was just:
\ beginning a function we already have something else that could unambiguously delimit the start of the function (e.g. (\a, b -> becomes (a, b ->, = \a, b -> becomes = a, b ->, : \a, b -> becomes : a, b ->nums.map(\num ->\ would only add value in the specific scenario where it's preceded by a comma, e.g. , \a, b -> and everywhere else it would be redundant with some other delimiter that's already there anyway., (a, b -> ...) in which case we have an extra character (the closing paren) but in all the common situations we save a character, plus we reclaim some strangeness budget since no mainstream languages use \tl;dr I thought the idea was "in the parens-and-commas and methods world, delete \ and change nothing else" and that does seem like a reasonable idea to me, all things considered :big_smile:
What would list.walk look like? This?
my_list.walk(init_state, (state, elem -> ...))
yeah
that's probably the most common situation where you'd need an extra character, although I think in the world where we have for, walk would be less commonly used
I personally dislike it as a gut reaction (mostly just feels ambiguous), but I also understand that in the parens world, parens will be required more often around lambdas. So it makes senses to just embrace that.
I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.
There are many standard library functions that use callbacks beyond walk. Things that couldn't be replaced by for loops, like findFirst. I don't image that for loops would replace map and similar functions either (though they could).
I'm still hoping that they don't end up being required, somehow
In the list builtins, aside from loops, only update and map2 through map4 are effected by dropping the \
The others have the lambda in the second arg, so it wouldn't need extra parents in method call syntax.
my_list.update(7, (a -> a + 1))
Alex Nuttall said:
I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.
For context, roc is already planning to test out a parens and comma syntax instead of a white space calling syntax. This is a sub proposal of that.
Brendan Hansknecht said:
Alex Nuttall said:
I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.
For context, roc is already planning to test out a parens and comma syntax instead of a white space calling syntax. This is a sub proposal of that.
Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.
We probably don't need to opt for this either, but Kotlin allows for last-arg callbacks to be provided outside of the parentheses. Kotlin's are easier to parse because they use { and } for functions, but it should be unambiguous if we just require that the closing ) function call parens and the arg list of the function def are on the same line.
my_list.update(7) a ->
a + 1
my_list.update(7) a -> a + 1
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')
If we're worried about too many nested parentheses making reading harder
Alex Nuttall said:
Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.
Like Javascript before arrow functions :smile:
Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.
I think most cases no brackets would be needed cause there is no ambiguity.
Yes that's right, there are many less standard library functions requiring it than I thought
:thinking: is this already the exact syntax we use for functions in type annotations?
Yes
Caveat being they often appear filling in a type variable in a single set of parens vs being repeated many times in the literal
Eg List (a -> a) vs:
[ (a -> a + 1), (a -> a - 1) ]
If the point of the dots-and-parens syntax is to seem more familiar to newcomers, wouldn’t it make sense to lean on e.g. the JavaScript syntax rather than something bespoke? That would be x -> … and (x, y) -> …, instantly recognizable to the very large group of people who know JS. I think the commas-but-no-parens style is kind of the worst option, really – different from how the function will be _called_, so not symmetrical, but also not easy for newcomers to understand. I think the Elm style of x -> y -> … makes a ton of sense when the language supports currying, and has the added benefit that there’s only ever one “argument” for a function, but since currying is out of the picture, why not take the step fully towards the JS syntax?
btw elm doesn't use a -> b -> result for lambdas. It uses \a b -> result (function types do use A -> B -> Result tho).
Do you know how js handles tuple lambda arguments? Oh wait, I guess it doesn't have them, just arrays
Ah yes, sorry, mixed up the type signature vs call, which also confused me (it’s been years since I wrote anything in Elm)
Daniel Schierbeck said:
wouldn’t it make sense to lean on e.g. the JavaScript syntax rather than something bespoke? That would be
x -> …and(x, y) -> …, instantly recognizable to the very large group of people who know JS.
JavaScript doesn't have tuples. If that were the syntax, x -> ... and (x, y) -> ... could both be describing 1-arg functions.
Daniel Schierbeck said:
I think the commas-but-no-parens style is kind of the worst option, really – different from how the function will be _called_, so not symmetrical, but also not easy for newcomers to understand.
I think the idea that x -> ... and (x, y) -> ... is easy for newcomers to understand, but x -> ... and x, y -> ... is "not easy for newcomers to understand" is a bit of a stretch :wink:
Would anything above be simpler if tuple syntax changed to use {} to match records?
From the tutorial:
Visually, tuples in Roc look like lists (but with parentheses instead of square brackets). However, a tuple is much more like a record - in fact, tuples and records compile down to the exact same representation at runtime! So anywhere you would use a tuple, you can use a record instead, and the in-memory representation will be exactly the same.
Like records, tuples are fixed-length and can't be iterated over. Also, they can contain values of different types. The difference is that in a record, each field is labeled (and their position doesn't matter), whereas in a tuple, each field is specified by its position.
If we're introducing 2-10x parentheses into Roc apps, dissolving tuple syntax to become unlabeled records seems like it could be helpful. I also notice that {} currently means "zero-arg function input"/"empty function output", which feels just as related to an "empty tuple" as an "empty record".
{ x, y } already means something else in Roc
But does it REALLY though? If that's a shorthand for using local variable names to label the fields, then the distinction is just whether the backing type is labeled vs. positional. If the type is named, the choice is referenceable. If the type is anonymous, then Roc can have a preference for which choice it infers.
and { 10, "gallon", Hat } is currently unused space in syntaxland
Idk if we'd go so far as to say "If you're looking for tuples, Roc has positional records." but we could, if that would simplify the language overall.
This may be a separate thread, but many comments above seem to hit on conflicts between tuple parens and non-tuple parens.
I dunno, I personally wouldn't call it a simplification to the language if looking at { x, y } = in isolation, it was no longer possible to know whether that was destructuring a record or a tuple :sweat_smile:
I agree with Richard, and field punning is just too damn useful to lose(and the only viable option). And plus “positional records” would look just like a list but with squirlies :grinning_face_with_smiling_eyes:.
How come no one talks about using <> for tuples?
ducks
42 messages were moved from this topic to #ideas > Do we need tuples? by Brendan Hansknecht.
Prettier (javascript formatter) formats its arrow functions with parens around the parameters, even if there's only one parameter, like (x) => x + 1.
At first glance, avoiding parentheses may look like a better choice because of less visual noise. However, when Prettier removes parentheses, it becomes harder to add type annotations, extra arguments or default values as well as making other changes. Consistent use of parentheses provides a better developer experience when editing real codebases, which justifies the default value for the option. (docs).
Assuming we resolve the tuple ambiguity (#ideas > Change tuple syntax from () to {}? ) I'd propose doing the same, though I haven't thought through it all the way.
I assume that is needed due to inline types
I don't think it applies to roc due to types being on a different line
We also don't have any form of named args
Or default values
All of that is done through records
I've seen the suggestion of parens around function args a few times. Here's an example with backslash, without backslash, and without backslash but with parens added:
dict.map(\key, value -> key + value)dict.map(key, value -> key + value)dict.map((key, value) -> key + value)as discussed previously, the extra delimiter here will almost always be redundant, except in the specific case of there being a comma right before the function begins, which should be rare in the methods world
so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"
or am I missing something? :sweat_smile:
or is the concern that dict.map(key, value -> key + value) looks confusing because it's unclear whether key, goes to the lambda vs. being the first argument to .map?
Richard Feldman said:
so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"
To be fair, isn’t that the argument for parens and commas in general?
Can we make backslash free work with a performant parser? That seems like a lot of necessary backtracking
I'd say parens and commans are better for an IDE-driven experience because they let you discover APIs as you type using dot-based autocomplete
And they happen to be less scary to newcomers
As an added benefit, they are also easier to parse
I’d say parens and commas are only slightly related to better IDE experience
(deleted)
That’s static dispatch
Though yes, parsing is much easier
Anthony Bullard said:
Richard Feldman said:
so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"
To be fair, isn’t that the argument for parens and commas in general?
I think the main argument for parens and commas is that they are a prerequisite for dots and static dispatch, and that's where the benefits come
I think you can do static dispatch without parens and commas (can we say PNC?)
"Stop trying to make fetch happen!"
No but actually, I'm okay with PNC or something
"parens-based function calls" has been a lot to type every time
It’s just a lot to type. Like parens and commas :drum:
(deleted)
But we can do
list = [1,2,3]
list->map \n -> n +1
Or
list = [1,2,3]
list:map \n -> n +1
And it works the same
Anthony Bullard said:
Can we make backslash free work with a performant parser? That seems like a lot of necessary backtracking
should be doable without backtracking. It's the same problem as { foo, bar, baz } looking like a record until you hit = and then it's like "oh that was a pattern, I'll just store it as a pattern instead of an expr" - you don't have to re-parse, you just change how you're interpreting the data you just parsed
Ok, I guess I’m used to making more strictly recursive descent parsers. You’d have trouble normally saying “this parser will give you a Expr or a Pattern”. But I’m sure you just continue parsing until you get one?
Anthony Bullard said:
But we can do
list = [1,2,3] list->map \n -> n +1Or
list = [1,2,3] list:map \n -> n +1And it works the same
Anyway I think something like this is a way we could test PNC and SD separately
Anthony Bullard said:
Ok, I guess I’m used to making more strictly recursive descent parsers. You’d have trouble normally saying “this parser will give you a Expr or a Pattern”. But I’m sure you just continue parsing until you get one?
I think what we do right now is parse the one and then convert it to the other if we realize that's the situation we're in (e.g. convert an Expr to a Pattern or vice versa) but I think in a future version of the parser we could potentially be more clever and have a combined data structure that's essentially both, and then only canonicalization has a distinction between exprs and patterns based on what it's currently doing
e.g. the parser accepts x = foo as bar even though as can only appear in patterns, but then canonicalization encounters it and reports an error because as occurred in an expr position
makes the parser more fault-tolerant too, which helps the formatter
I actually was gonna propose we consolidate all parsing for records (types, literals, module params, etc.) to a single parser as well at some point. It makes us more tolerant and do less backtracking to parse in this way, and makes the parsing code easier to understand
Richard Feldman said:
or is the concern that
dict.map(key, value -> key + value)looks confusing because it's unclear whetherkey,goes to the lambda vs. being the first argument to.map?
I for one would read that as dict.map(key, (value -> key + value)), but maybe that’s because I’m used to these other languages where that’s definitely how it would work.
But even dict.map(\key, value -> key + value) would require me to pay extra attention, since I’m used to the comma in a parenthesized argument list being the argument separator. I mean, Roc can be its own thing, but if recognizable syntax is a goal or strategy, then I think the backslash would be a source of confusion.
yeah both good points
I would also note that parsing comma-separated args in backpassing is quite painful right now, and I was really hoping to sunset that logic with backpassing.
x, y -> foo is annoying to parse for the same reasons as x, y <- foo
I would strongly prefer some solution that doesn't leave the comma "naked". That could be either having some introducing character/sequence (replacing \), or requiring that the arg list be delimited by ()/[]/{}/whatever.
(I have no objection to single-arg functions being just x -> foo tho)
also a fair point
I wonder if this is why Rust ended up on | for lambdas :thinking:
I'm gonna be trying to resuscitate that |lambda| thread later for that reason.
just as concise as -> but without the ambiguity concern, and already mainstream from Ruby
I think | for lambda's helps with readability, it's easy to type, and it literally looks nice
the considerations are definitely worth revisiting in the context of parens-and-commas
And it keeps the door open to Kotlin-style outside-of-parens final callback args, which are pretty good for DSLs in addition to helping remove some layers of circle brackets
Though I don't know how much other people like that behavior
it's always felt conceptually weird to me that || in Rust also means 0-arg lambda, but somehow it's never confusing in practice
like it feels like it ought to be but somehow it isn't, which I find very strange :big_smile:
I know it's unambiguous because it's only an infix operator when it's in the infix position, but still...
Besides the issue with tab based indentation and clarity, I love the rest of |arg| for lambdas
oh what's the issue with tab based indentation and clarity?
We should keep a little m collection of the same module with all of these syntactic combinations
Richard Feldman said:
oh what's the issue with tab based indentation and clarity?
I was about to ask the same
More context directly above this message in the old thread:
Brendan Hansknecht said:
eg these two look too similar but mean very different things:
credits = List.map songs |song| "Performed by $(song.artist)" credits = List.map songs (song) "Performed by $(song.artist)"
ah right
credits = songs.map(\song -> "Performed by $(song.artist)")
credits = songs.map(|song| "Performed by $(song.artist)")
Oh, with parens it probably is fine
yeah the whole discussion only makes sense in the context of parens haha
So sounds good to use ||
Yeah, I know the discussions are about parens, but my brain hasn't rewired to parens yet.
{ first, second, third } = { Result.parallel! <-
first: || do_something!(),
second: || do_something_else!(),
third: || do_another_thing!(),
}()?
could work! :thumbs_up:
One weird question: Does PNC and this change mean anything for:
map : | List(a), (| a | b) | List(b)
:thinking:
Ok I hate it, plz no
Kill number 1
What did I just read.... :see_no_evil:
Oh, also, if we remove ->, what comes of =>?
I think type annotations could be more parenthetical with respect to type constructors. AKA complex types and tags:
Tag ext : [Left, Right(Str, U64), ..ext]
Container elem : { items : List(elem) }
Left could be Left(), Tag ext could be Tag(ext)
Or we go Rust/C++ and do Tag<ext> or Right<Str, U64>
The thing we agreed on is the ..ext allowing for spread-style extensions
It feels like we're heading in the direction of future Roc; the :wolf: Elm in :sheep: Rust's clothing
Brendan Hansknecht said:
Oh, also, if we remove
->, what comes of=>?
This is the main reason we still need the arrow, and I don't even think that we'd be better of without the arrow. It's extremely obvious what it means
Luke Boswell said:
It feels like we're heading in the direction of future Roc; the :wolf: Elm in :sheep: Rust's clothing
Literally what Gleam did
I leave this language the second <> are introduced
Okay, line drawn haha
Could be a good way to shed the non-believers
I don't have strong opinions on them one way or another. Any particular reason @Anthony Bullard ?
Keep the tyre kickers away
I just hate them aesthetically
Fair
I think Roc is beautiful right now
That’s why I keep trying to figure out how we could do static dispatch without PNC
Brendan Hansknecht said:
Oh, also, if we remove
->, what comes of=>?
I didn't even think of this, but actually | for delimiting functions instead of -> actually resolves the longstanding irritation of => functions' bodies being defined with -> syntax
main! = |args| ...
so I guess basic-cli Hello World would be:
main! = |_args|
Stdout.line!("Hello, World!")
or, if preferred:
main! = |_|
Stdout.line!("Hello, World!")
I don't know if there's a canonical name for this in Rust, but I propose that |_| be referred to as "touchdown!" :american_football:
if we didn't have it passing in args, it would be:
main! = ||
Stdout.line!("Hello, World!")
It's very elegant.
Richard Feldman said:
or, if preferred:
main! = |_| Stdout.line!("Hello, World!")
Love it
Kind of reminds me of the eye of sauron.
Anthony Bullard said:
We should keep a little m collection of the same module with all of these syntactic combinations
Thinking about this. Would it be costly to maintain a fixed (no new features, not meant for anything than formatting) version of the compiler that just has a configurable formatter for seeing projects in different proposed syntaxes? It would only parse Roc that was valid at the time the compiler was forked.
Any reason this couldn't just be https://github.com/gamebox/roc?
Sam Mohr said:
Any reason this couldn't just be https://github.com/gamebox/roc?
You mean I branch I maintain? Sure
Maybe hold this thought... I think Josh has been cooking
We've got a basic platform with the idea to provide the AST https://github.com/lukewilliamboswell/roc-platform-template-rust/tree/osprey-poc
Just an idea at this stage.
But it would be cool to have something like that parse a roc module, and then format it using our alternative syntaxes
Another long-term goal of this team is to make the roc compiler into a set of exposed libraries. That's nowhere near-term, but something that could help support this kind of goal in the future.
If your ok working with rust, you can use libroc today :grinning: aka the above platform concept.
Richard Feldman said:
Brendan Hansknecht said:
Oh, also, if we remove
->, what comes of=>?I didn't even think of this, but actually
|for delimiting functions instead of->actually resolves the longstanding irritation of=>functions' bodies being defined with->syntax
What should main!'s type annotation/signature be? I remember in your talk you said => was designed to indicate effectfulness in anonymous contexts.
Anthony Bullard said:
map : | List(a), (| a | b) | List(b):thinking:
As someone who's never used a Rust lambda, this is the only reference point I have for what : |args| might look like, and everyone said ew lol
I'm enjoying the schemes cooking in this thread :blush:
JanCVanB said:
Anthony Bullard said:
map : | List(a), (| a | b) | List(b):thinking:
As someone who's never used a Rust lambda, this is the only reference point I have for what
: |args|might look like, and everyone said ew lol
That was meant to be an idea about how type annotations _could_ change. Don't want that at all. I personally would like Arg1, Arg2 (->/=>) RetType to remain for function type annotations
Yeah, so it would be
map: List a, (a -> b) -> List b
map = |list, fn|
....
And called as
my_list.map(|a| a + 1)
Would everyone agree that we are resolved to adopt |arg1, arg2| bodyExpr syntax for lambdas? Or are there any dissenters?
Isn't it all function defs?
Anthony Bullard said:
I leave this language the second <> are introduced
Yeah, a lambda == a function in Roc
I'm working now on a fork of my AOC from this year rewritten in this new syntax
Anthony Bullard said:
Anthony Bullard said:
I leave this language the second <> are introduced
Yeah, a lambda == a function in Roc
I mean a lambda is a ANONYMOUS function in Roc. I don't know if there is really a difference there since assigning it to a variable is the only way for a function to have a name in Roc.
You can't annotate an anonymous function.
I don't mean to be pedantic, though - the Python portion of my background hears "lambda" and points only at the anonymous inline ones.
I would definitely say they are the same thing
You wouldn't call these different things:
x = 1
callThing(x)
callThing(1)
The explicit 1 is not an anonymous number
It just isn't saved in a variable
In roc, we only have lambdas
They may or may not get saved to a variable
JanCVanB said:
You can't annotate an anonymous function.
This is a decent point, but I think in Roc it's safe to say all functions are lambdas
Or what Brendan said :-)
What if you prefix that code snippet with x : F32, then do they feel different?
Nvm, in the map example above we have |a| a + 1 potentially specified by an outer function's annotation, so direct annotation isn't a differentiator. Roc functions are lambdas!
*"specified" here meaning (since I forget the official term for this) defined as something more specific than a -> b, such as Int -> Int
It especially rings true if you can "always" multiline like
my_list.map(
|a|
a + 1
)
(whether or not the formatter one-lines it)
Two things I really dislike about |a|:
-> and => arrows give a sense of directionality. You start with the arguments, and then pass it into the body. The pipe operator is similar. |a| has no sense of directionality, The a and a+1 are next to each other but it's not visually indicated how they relate to each other. When reading, I feel like I have to memorize what the syntax means instead of it being obvious.-> in definitions and in types. You still have to learn both syntaxes, and they're so different that it's hard to see which part of the definition corresponds to which part of the type. (We already have this problem with =>, but to a lesser degree, because the -> in the definition looks similar enough to the => in the type that you can still match them up visually.)I don't think we need the directionality. Functions bodies are either on one line or in blocks. If they're in one line, the body is invariably after the |args|, and if they're in a block, it has to be indented by 4 spaces (maybe a tab in the future). That means that |args| is always to the left of the function body. I think it'll be relatively obvious with |args| only being used in one place.
Rust has this exact dynamic of -> for type signatures and |args| for arg definitions, and it seems to have been working there for a while now. Not having the single arrow -> also avoids the confusion of function bodies not matching effectful type annotations.
-> suffix after the ||, though I think it's unnecessary. The importance of the || pair is that it's visually distinct from everything else in Roc (barring or, which doesn't seem to be a problem in Rust as Richard pointed out). If everyone wants the arrow, we can add it without losing the visual distinction of the start of the function bodyRichard Feldman said:
I know it's unambiguous because it's only an infix operator when it's in the infix position, but still...
I'm shocked I never realized || is used for both 'or', and zero-arg lambda's in rust. They always felt like different things to me. Now I know, I have no idea if I will start to notice it more :big_smile:
foo = |arg1, arg2| bar arg1 + arg2 is a two-arg lambdafoo = |arg1| bar arg1 + 2 is a one-arg lambdafoo = || bar 1 + 2 is a zero-arg lambdabaz = 1 |> foo ... is piping one arg to an n-arg lambdaWhat if the args delimiter was already right in front of us this whole time? Can the pipe operator expand/overload to hold the args at definition time?
foo = |arg1, arg2> bar arg1 + arg2foo = |arg1> bar arg1 + 2foo = |> bar 1 + 2baz = 1 |> fooFeels like a cute compromise, but likely too spooky.
Oof |arg1, arg2> looks so much worse than it did in my head.
Maybe with whitespace | arg1, arg2 > isn't awful - nvm it's even worse lol
No no, let him cook
|~ arg1 ~ arg2 ~>
jk. I consider my above thought experiment dead on arrival, unless someone's seriously intrigued by the "||+-> compromise" idea.
One thing that we need to be careful with here, from a parsing perspective, is that (I think!) this idea of having closures delimited by || is fundamentally incompatible with space-separated function calls.
Otherwise, we're going to end up confusing foo |bar| baz (i.e. intended to be foo(|bar| baz)) with the disjunction of those three terms. And similar confusions are possible between the empty-args || form and the boolean or operator.
This means this will be hard to adopt incrementally / experiment with.
This has to come after space-separated calls are deprecated + removed.
I think that's fine because at first it'll need to be |{}| anyway
we don't currently have a concept of "zero-arg functions"
so one way to think of it is that introducing zero-arg functions with this syntax would only be possible if space calling has been removed from the parser
Not just that. This syntax doesn't work at all (zero arguments, one argument, more arguments) if we have space calling.
Actually, I take that back. It's not incompatible. They just can't be used together in the same call.
Last updated: Jun 16 2026 at 16:19 UTC