what if we made match consistent with if and have it just use curly braces instead of a special -> and , thing? e.g.
...we do this:
match a {
Blue { 47 }
Green { 19 }
Red { 12 }
lower { 1 }
[1, 2, 3, .. as rest] { 123 }
3.14 { 314 }
}
seems like it's just as easy to read, it's one less piece of syntax to learn, and if you want a multiline branch you're already all set up with the curly braces
Feels super weird at first look, but I'm sure that is a first look bias. Also, my brain still thinks in WSA where it looks exceptionally broken.
Still feels kinda alien though.
Kind of smooth, and definitely aligned with other changes. The current match syntax could be an outlier alongside brace blocks.
Here's an updated sample with tuples (and records):
match_time = |a| match a {
Blue { 47 }
Green { 19 }
Red { 12 }
lower { 1 }
[1, 2, 3, .. as rest] { 123 }
3.14 { 314 }
(1, 2, 3) { 123 }
{ foo: 1, bar: 2 } { 12 }
}
The last two (especially the last one) make me feel a little squeamish
I wonder if this would benefit from an additional per-branch keyword, like an on prefix.
or just a second similar use of if.
or when.
Or maybe just an -> ;-)
Since for me the issue is the space between the pattern and the expression
Like, I can read that last branch, but I think many others would struggle with it
Are we already heading for that with braces in other ways?
if x == { foo: 1, bar: 2 } { 12 } else { 24 }
(though this would get multilined apparently)
Yes, I think so
Seems like an arrow does the job better :woman_shrugging:
If we're already having the formatter add braces, why not have the formatter add arrows?
I think I'll already be leaning entirely on the formatter and my editor for tabs and indentation
Seems reasonable to have it add braces and arrows for me too
I think the motivation is to avoid having a semantically-triple-overloaded symbol.
How about we steal the => that everyone else uses
Then it's only used twice
For matching and effectful function types
I really like that that would overload both arrows in equally "whatever" ways :smiley:
match_time = |a| match a {
Blue => { 47 }
Green => { 19 }
Red => { 12 }
lower => { 1 }
[1, 2, 3, .. as rest] => { 123 }
3.14 => { 314 }
(1, 2, 3) => { 123 }
{ foo: 1, bar: 2 } => { 12 }
}
Silly idea for completeness: tilde arrow ~>
I feel like the real learning is that whoever designed ASCII and the modern keyboard should've prioritized more useful symbols that are easy to type
That's partly why keywords work so well for languages, I think
I often chuckle at how early Japanese adoption of emoji has forever exposed other cultures to what their snacks look like.
Anthony Bullard said:
Here's an updated sample with tuples (and records):
match_time = |a| match a { Blue { 47 } Green { 19 } Red { 12 } lower { 1 } [1, 2, 3, .. as rest] { 123 } 3.14 { 314 } (1, 2, 3) { 123 } { foo: 1, bar: 2 } { 12 } }The last two (especially the last one) make me feel a little squeamish
the last one never really comes up in practice; I don't see people doing pattern matches on plain records (they're always wrapped in something else)
and for tuples I usually see matching on tags for one or both of the tuples, e.g.
match (names.first(), emails.first()) {
(Ok(name), Ok(email)) { foo(name, email) }
(_, _) { bar() }
}
some examples from Luke's roc-realworld branch
match Env.var("DB_PASSWORD") {
Ok(password) { Password(password) }
Err(VarNotFound) { None }
}
get_by_slug! : Articles, Str => Result (List U8) [NotFound, InternalErr Str]
get_by_slug! = |{ client, prepared }, slug|
match ArticlesSql.get_article_by_slug!(client, prepared.get_article_by_slug, slug) {
Ok([row]) {
{
slug: row.slug,
title: row.title,
description: row.description,
body: row.body,
created_at: row.created_at,
updated_at: row.updated_at,
favorited: row.favorited,
favorites_count: row.favorites_count,
tag_list: row.comma_separated_tags.split(","),
author: {
username: row.author_username,
bio: row.author_bio,
image_url: row.author_image_url,
following: row.author_following,
},
})
.encode(Json.utf8.transform(CamelCase))
->Ok
}
Ok([]) { Err(NotFound) }
Ok([..]) { Err(InternalErr("Multiple articles found for the slug ${slug.inspect()}")) }
Err(db_err) { Err(InternalErr(db_err.inspect())) }
}
hm, that first branch there makes me question whether we want to always require braces, even if there's a record (separately from the arrow vs. nothing vs. alternatives question) :thinking:
like Foo { { x, y } } or Foo -> { { x, y } } or anything else has the problem of nested braces
but it feels weird to make an exception for records only, when other expressions could omit the braces too :sweat_smile:
I guess if and else branches would have the same consideration
There is no strict reason for braces if we require commas :wink:
also if we don't haha
and it's still annoying to me how Rust has commas sometimes and braces other times
but it might be easier to think about what we should do in the case of if
and then do that too with match
because if also currently formats to always have braces, and so would also run into the if condition { { x, y } } else { { a, b } } problem
The thing that sold me on braces is that they can be required for statements but optional/removed for expressions, so you can write a lot of code without them. This is an interesting idea but goes against that.
One thing I liked about the old -> days was that for both their uses (function definition and matching), you're binding variables in a pattern and then using those bindings in the following block. So what if we changed match to use the new | | syntax so it's the same as functions?
match a {
| Blue | 47
| Green | {
x = 19
x
}
| x | x
| [1, 2, 3, .. as rest] | {
123
}
| (1, 2, 3) | 123
| { foo: 1, bar: 2 } | 12
}
If we're going "low whitespace sensitivity", then this probably is ambiguous if a branch's body could be a pattern or an expression
match a {
| Blue | Red | Green | Yellow
}
Is this matching 3 different colors and returning Yellow, or matching twice?
Ah, that's ambiguous between | marking functions and | being an "or" in patterns.
Are you allowed to use | in the pattern in a function definition?
Like, do we already have this problem?
I don't think there's a reason we'd want to prevent that
Though you're right that we have to be space sensitive for function args, that's unfortunate
Unless we move to or
Split that topic into a separate thread: #ideas > `|` ambiguity in function defintion
Are we talking about function type annotations?
The args to a function
Because a function definition does not allow alternates
Oh, well, that works
It'll suck to make parsePattern have to have a flag for "disallow_alternatives"
But I think thats' the correct choice
Someone correct me on this, but I think it's a restricted set of patterns there
If we go with or, I think we could allow it, right? But yes, | would not allow it for function args
Basically idents, tags with payloads, destructures....and that's it?
For now yes
But yes, I think | <pattern> | with or for alternates works
But Sam gets big mad when we bring that up :rofl:
SAM ANGRY
in addition to function arguments, technically everything in front of an = assignment is a pattern.
with that in mind, I think the only place patterns are restricted is that | (or or) is only allowed in match, and top-level declarations can only use identifiers (e.g. you can't do (foo, bar) = at the top level)
I'd love if that last restriction were true
This is valid right now:
module [x, y]
{ x, y } = { x: 1, y: 2 }
It makes function lifting for @Isaac Van Doren hard
Ooooo
regarding the "matches use lambda syntax" idea - wouldn't that syntax not have the spaces inside the |s?
so:
match a {
|Blue| 47
|Green| {
x = 19
x
}
|x| x
|[1, 2, 3, .. as rest]| {
123
}
|(1, 2, 3)| 123
|{ foo: 1, bar: 2 }| 12
}
I mean, ewwwww
I don't think we should allow destructures on the top-level
Okay, that's great, we'd need the interpreter to get rid of them for function lifting to be reasonable to implement
Richard Feldman said:
regarding the "matches use lambda syntax" idea - wouldn't that syntax not have the spaces inside the
|s?so:
match a { |Blue| 47 |Green| { x = 19 x } |x| x |[1, 2, 3, .. as rest]| { 123 } |(1, 2, 3)| 123 |{ foo: 1, bar: 2 }| 12 }
I definitely think canonical style for this syntax would be no spaces
I'm actually here for this syntax!
I will make sure that's a parse error
I don't hate this syntax
I'm cool with it as long as the syntax highlighting looks like the last two branches
I hate that I'll have to update my PR again, but.....that's life on the edge
where the |s are highlighted like keywords, but the destructure delimiters like ( and { inside it are highlighted differently
We'd definitely want or here, which I'm okay with
And you can disambiguate ors in patterns because they do have spaces!
# 2 branches
match a {
|Blue| Red |Green| Yellow
}
match a {
# 1 branch with 3 options
|Blue | Red | Green| Yellow
}
(this is terrible)
Ok, We'll have to talk to @Eli Dowling about that
@Sky Rose luckily the formatter would add newlines for you
I think we will go with:
match a {
|Blue or Red or Green| Yellow
}
I think having or in function args shouldn't be blocked, just for top-level defs
I can't imagine how or in function args makes sense
Can you give me an example?
well it doesn't really make sense outside match
like in the same way that if I write my_fn = |Ok(foo)| ... it's just not handling the possibility that the function gets passed Err
Here's an example:
| Tag1(x) <or> Tag2(_, x)| do_stuff(x)
It would make sense in something like Elixir where it's dynamic dispatch and multiple function heads and stuff like that
get_user_email = |Admin({ email, .. }) or Guest(email)| email
right but like
oh wait
so you're thinking you make it exhaustive
yes
huh, I'd never considered that!
So you are destructuring multiple tags and extracting a payload member with the same type in each...
I would never think to do that, but I guess that works
so I guess the idea is that you would do it right there in the | instead of immediately having a match which does the same thing
Yep
I think that's actually pretty sweet
yeah I like that it's one less rule to learn
I'm thinking about how we didn't want to block val.(|arg| lambda(arg))
it's just like "yeah you can use this wherever, you just gotta be exhaustive like always"
Stuff just works together
So it's like just one extra feature on top of what we currently support there
yeah
I'm down
We can just parse patterns and let Can get angry about bad ones
Not sure if guard patterns make sense haha, those aren't exhaustive
Makes my life easier
I'll update my PR in the morning
If you don't allow this in a match or function definition, you have to have duplicate branches.
match user {
|Admin({ email, .. })| do_stuff(email)
|Guest(email)| do_stuff(email)
}
or maybe an extra variable assignment
email = match user {
|Admin({ email, .. })| email
|Guest(email)| email
}
do_stuff(email)
And then hopefully get Record patterns done
I'm about to turn into a pumpkin, see y'all tomorrow !
Sam Mohr said:
This is valid right now:
module [x, y] { x, y } = { x: 1, y: 2 }It makes function lifting for Isaac Van Doren hard
just to close the loop on this - back in the day when Folkert and I were working on the initial versions of these things, I remember thinking "this will be fine because we can just desugar it into the equivalent of:
$record0 = { x: 1, y: 2 }
x = $record0.x
y = $record0.y
...but then it turns out this feature sees such little use in practice that it doesn't really seem worth the trouble anytime soon
especially because of the "ok smart guy now how do you write a type annotation for that" problem
It'd be more useful in practice if you could do interesting compile-time calculations top level
so maybe someday someone might decide to implement it just to remove the restriction, but I definitely think for 0.1.0 (and possibly indefinitely) we should just say "identifier patterns only top-level assignments"
Sky Rose said:
It'd be more useful in practice if you could do interesting compile-time calculations top level
it's certainly possible, and now we're on a path to find out! :smiley:
You can write a type annotation for it though:
{ x, y } : { x : Str, y : U64 }
{ x, y } = { x: "abc", y: 123 }
right, sorry - really I mean like how does the compiler deal with that :sweat_smile:
actually I guess it's the same desugaring thing
yep
oh right, there's another problem though that I'm just remembering
I have written that code before... not clean, it was on the path to UI state management
SomeTag(x) = get_single_tag_wrapper(y)
that's valid, if strange
as long as get_single_tag_wrapper returns a [SomeTag(Whatever)]
It's pretty much all "valid, if strange"
it has to be exhaustive, yes
so then what even is the syntax for the type annotation there?
[SomeTag(x)] : [SomeTag(Str)]
SomeTag(x) = get_single_tag_wrapper(y)
Yep
Not great
yeah :sweat_smile:
Before we commit to or in patterns, I wanna pitch the idea for + instead of or, but to avoid derailling this conversation, let's confine any discussion in the other thread #ideas > `+` instead of `|` in patterns
If someone could point out the final position on this, I'll update my realword PR so we can merge the syntax in
It sounds like we've got a consistent syntax for patterns in functions args and match statements
Richard Feldman said:
match a { |Blue| 47 |Green| { x = 19 x } |x| x |[1, 2, 3, .. as rest]| { 123 } |(1, 2, 3)| 123 |{ foo: 1, bar: 2 }| 12 }
it's this, plus changing from | to or for alternatives in patterns
Using this lambda-like syntax makes me think something like this would be possible:
handle_guest = |Guest({email, ..})| foo(email)
handle_admin = |Admin(email)| bar(email)
some_function = |user| match user { handle_guest, handle_admin }
I updated the PR https://github.com/rtfeldman/roc-realworld/pull/1
It kind of feels a little like we're swimming in | characters now. Maybe it's the lack of syntax highlighting... but the patterns all look like lambdas
Here's an example
match (method, path) {
|(GET, "/articles/${slug}")| auth_optional!(|opt_user_id| db.articles.get_by_slug!(opt_user_id, slug))
|(GET, "/articles")| auth_optional!(|opt_user_id| db.articles.list!(opt_user_id, req.params()))
|(POST, "/articles")| auth_json!(|user_id, article| db.articles.insert!(user_id, article))
|(PUT, "/articles/${slug}")| auth_json!(|user_id, article| db.articles.update!(user_id, slug, article))
|(DELETE, "/articles/${slug}")| auth!(|user_id| db.articles.delete!(user_id, slug))
|(GET, "/articles/feed")| auth!(|user_id| db.articles.feed!(user_id, req.params()))
|(POST, "/articles/${slug}/comments")| auth_json!(|user_id, comment| db.comments.create!(user_id, slug, comment))
|(GET, "/articles/${slug}/comments")| auth_optional!(|opt_user_id| db.comments.list!(opt_user_id, slug))
|(DELETE, "/articles/${slug}/comments/${id}")| auth!(|user_id| db.comments.delete!(user_id, slug, id))
|(POST, "/articles/${slug}/favorite")| auth!(|user_id| db.articles.favorite!(user_id, slug))
|(DELETE, "/articles/${slug}/favorite")| auth!(|user_id| db.articles.unfavorite!(user_id, slug))
|(GET, "/tags")| to_resp(db.tags.list!())
|(GET, "/profiles/${username}")| to_resp(db.users.get_by_username!(username))
|(POST, "/profiles/${username}/follow")| auth!(|user_id| db.users.follow_username!(user_id, username))
|(DELETE, "/profiles/${username}/follow")| auth!(|user_id| db.users.unfollow_username!(user_id, username))
|(GET, "/user")| auth!(|user_id| db.users.get_by_id!(user_id))
...
}
For the common cases it looks pretty good.
match req.method {
|"GET"| Ok(GET)
|"POST"| Ok(POST)
|"PUT"| Ok(PUT)
|"DELETE"| Ok(DELETE)
|"OPTIONS"| Ok(OPTIONS)
|_| Err(UnrecognizedMethod(req.method))
}
match client.query!(cmd) {
|Ok([row])| Article.fromRow(row).(Ok)
|Ok([])| Err(NotFound)
|Err(db_err)| Err(InternalErr(db_err.inspect()))
}
match Env.var!("LOG_LEVEL") {
|Ok(level_str)| {
LogLevel.from_str(level_str) ? |UnsupportedLevel|
InitFailed("Invalid LOG_LEVEL env var: ${level_str}")
}
|Err(VarNotFound)| default_log_level
}
I think I like -> better at this point
Altho I do really like the idea of both that and the braces being optional and inserted automatically by the formatter
Joshua Warner said:
Using this lambda-like syntax makes me think something like this would be possible:
handle_guest = |Guest({email, ..})| foo(email) handle_admin = |Admin(email)| bar(email) some_function = |user| match user { handle_guest, handle_admin }
I don't think that could work since function arg patterns need to be exhaustive
Is there a reason we don't like fat arrow?
=>
It's only used in function signatures currently.
I'm gonna be honest I think pipe for match is really really visually confusing when mixed with pipe for functions.
And always having braces would just be so noisy.
I feel like a separator of some kind is a must for visual clarity
I also agree that => would be a great option still. I think |pattern| makes sense because then both function defs and pattern defs are the same shape that means "eat these args and use them to spit out this value"
So the similarity is intentionally outlined in the syntax reuse
re match x { |pattern| expr }
imo, any syntax with brackets around pattern in the match arm is redundant because you have a single "argument" which is a pattern (if there are multiple patterns in the arm it becomes even weirder)
also, lack of a delimiter such as ->, reminds me the problem with (arg1, arg2) -> result vs fn(arg1, arg2, result):
(a rust based example since I'm not sure how to explain my confusion via roc syntax)
match x { pat1, pat2 => expr } vs match { pat1, pat2 , expr }
re match and lambda consistency
I always felt a bit uncomfortable about similarity of match arms and lambdas, wanting to have a syntax that would work in both cases, but it seems it's a good idea to distinguish them because of the exhaustion problem Sam mentioned
in short, I'm leaning towards status quo
also, what about if guards?
If guards shouldn't be a problem
They are of the shape <pattern> if <expr> <arm-expr>
I was going to make the same complaint @Sam Mohr it feels like it would be nice if it matched lamda syntax.
This feels like a case where old roc syntax is just a clear winner:
when (method, path) is
(GET, "/articles/${slug}") -> auth_optional! \opt_user_id -> db.articles.get_by_slug! opt_user_id, slug
(GET, "/articles") -> auth_optional! \opt_user_id -> db.articles.list! opt_user_id, req.params
(POST, "/articles") -> auth_json! \user_id, article -> db.articles.insert! user_id, article
(PUT, "/articles/${slug}") -> auth_json! \user_id, article -> db.articles.update! user_id, slug, article
(DELETE, "/articles/${slug}") -> auth! \user_id -> db.articles.delete! user_id, slug
(GET, "/articles/feed") -> auth! \user_id -> db.articles.feed! user_id, req.params
(POST, "/articles/${slug}/comments") -> auth_json! \user_id, comment -> db.comments.create! user_id, slug, comment
(GET, "/articles/${slug}/comments") -> auth_optional! \opt_user_id -> db.comments.list! opt_user_id, slug
(DELETE, "/articles/${slug}/comments/${id}") -> auth! \user_id -> db.comments.delete! user_id, slug, id
(POST, "/articles/${slug}/favorite") -> auth! \user_id -> db.articles.favorite! user_id, slug
(DELETE, "/articles/${slug}/favorite") -> auth! \user_id -> db.articles.unfavorite! user_id, slug
(GET, "/tags") -> to_resp(db.tags.list!)
(GET, "/profiles/${username}") -> to_resp(db.users.get_by_username! username)
(POST, "/profiles/${username}/follow") -> auth! \user_id -> db.users.follow_username! user_id, username
(DELETE, "/profiles/${username}/follow") -> auth! \user_id -> db.users.unfollow_username! user_id, username
(GET, "/user") -> auth! \user_id -> db.users.get_by_id! user_id
// ... existing code ...
If guards shouldn't be a problem
I mean, how weird they will look like in a real app with different proposals?
Honestly, I think any unaligned match statement like the one you showed Eli is super hard to read without some form of alignment of the arms of the matches
Trying to break down why it works much better for me:
-> is 2 chars so it doesn't get lost in the soup of (|}(}}| (I seriously struggle to tell what is a ( and what is a| when they are close together.
-> is directional so I don't get confused by what is the end of a match vs what is the start of a lambda.
There is cohesion. -> means "start a new sub expression" in both matching and lambda.
I'd much rather see one of these
when (method, path) is
(GET, "/articles/${slug}") ->
auth_optional! \opt_user_id -> db.articles.get_by_slug! opt_user_id, slug
(GET, "/articles") ->
auth_optional! \opt_user_id -> db.articles.list! opt_user_id, req.params
(POST, "/articles") ->
auth_json! \user_id, article -> db.articles.insert! user_id, article
(PUT, "/articles/${slug}") ->
auth_json! \user_id, article -> db.articles.update! user_id, slug, article
(DELETE, "/articles/${slug}") ->
auth! \user_id -> db.articles.delete! user_id, slug
(GET, "/articles/feed") ->
auth! \user_id -> db.articles.feed! user_id, req.params
when (method, path) is
(GET, "/articles/${slug}") -> auth_optional! \opt_user_id -> db.articles.get_by_slug! opt_user_id, slug
(GET, "/articles") -> auth_optional! \opt_user_id -> db.articles.list! opt_user_id, req.params
(POST, "/articles") -> auth_json! \user_id, article -> db.articles.insert! user_id, article
(PUT, "/articles/${slug}"). -> auth_json! \user_id, article -> db.articles.update! user_id, slug, article
(DELETE, "/articles/${slug}") -> auth! \user_id -> db.articles.delete! user_id, slug
(GET, "/articles/feed") -> auth! \user_id -> db.articles.feed! user_id, req.params
I totally agree, but I do feel like it is a valid test for cases where we have dense content, sometimes it's not so easily formattable for clarity
og also has a pleasing formatting feature the arrows sit nicely at the end. vs ||:
match (method, path) {
|(GET, "/articles/${slug}")| auth_optional!(|opt_user_id| db.articles.get_by_slug!(opt_user_id, slug))
|(GET, "/articles")| auth_optional!(|opt_user_id| db.articles.list!(opt_user_id, req.params()))
|(POST, "/articles")| auth_json!(|user_id, article| db.articles.insert!(user_id, article))
|(PUT, "/articles/${slug}")| auth_json!(|user_id, article| db.articles.update!(user_id, slug, article))
It definitely feels like we're pushing for a syntax here that leads to very few syntactic primitives, which I don't think is a win in and of itself. I'd rather have more variety in syntax to indicate uniquely what I'm looking at rather than a homogeneous syntax that makes glancing harder
Why the giant gap in your example?
I formatted the whole thing and only took the first few. Fixed
for the sake of completeness:
match (method, path) {
|(GET, "/articles/${slug}")|
auth_optional!(|opt_user_id| db.articles.get_by_slug!(opt_user_id, slug))
|(GET, "/articles")|
auth_optional!(|opt_user_id| db.articles.list!(opt_user_id, req.params()))
|(POST, "/articles")|
auth_json!(|user_id, article| db.articles.insert!(user_id, article))
|(PUT, "/articles/${slug}")|
auth_json!(|user_id, article| db.articles.update!(user_id, slug, article))
}
I guess the newlines would need { and } meaning this would read a bit more clearly actually
match (method, path) {
|(GET, "/articles/${slug}")| {
auth_optional!(|opt_user_id| db.articles.get_by_slug!(opt_user_id, slug))
}
|(GET, "/articles")| {
auth_optional!(|opt_user_id| db.articles.list!(opt_user_id, req.params()))
}
|(POST, "/articles")| {
auth_json!(|user_id, article| db.articles.insert!(user_id, article))
}
|(PUT, "/articles/${slug}")| {
auth_json!(|user_id, article| db.articles.update!(user_id, slug, article))
}
}
Why? it's only a single expression still
I would too miss a visually distinct delimiter between the pattern and the expression. it's another thing that -> is much easier to write in my layout than =>.
We wouldn't have compleat symmetry between patterns and lambdas anyway. Making them look the same would ask of an explanation why:
match, whereas patterns in lambda parameters would need to be exhaustive on their own.All that is to say, if they are so different, there's no harm in making them look different.
I'd rather have more variety in syntax to indicate uniquely what I'm looking at rather than a homogeneous syntax that makes glancing harder
oh yeah, it's also about grepability. in this sense, e.g. => in rust is very unambiguous
Here are some examples with guards:
|<pattern>| <guard> <expr>
It isn't visually clear where the expressions begin. And these patters aren't event that long.
match a {
|OK(num)| if num > 3 Big
|OK(num)| if num <= 3 Small
|Err(_)| Other
}
This (and the formatter?) would push ppl to use braces for every case, which is an improvement, but still not as good I think.
match a {
|Ok(num)| if num > 3 { Big }
|Ok(num)| if num <= 3 { Small }
|Err(_)| { Other }
}
This looks better to me, because the | delimits the pattern and expression.
|<pattern> <guard>| <expr>
match a {
|Ok(num) if num > 3| Big
|Ok(num) if num == 3| Small
|Err(_)| Other
}
But don't get me started on code where we return a lambda from the match (Which idk how realistic of a use case is)
match a {
|Ok(num) if num > 3| |num| num +100
|Ok(num) if num == 3| |num| num + 10
|Err(_)| |num| num
}
I don't think braces and newlines help the | pattern syntax with or without guards, since the lambdas are easy to confuse with patterns. Even using helper functions could be confused with patterns.
match a {
|Ok(num) if num > 3| {
add10 = |number| num +10
// return or use it later
}
|Ok(num) if num == 3| {
|num| num + 10
}
|Err(_)| {
|num| num
}
}
And here is the current version
match a {
Ok(num) if num > 3 -> Big
Ok(num) if num == 3 -> Small
Err(_) -> Other
}
I agree with Norbert here, I think guards kind of change the calculus on this for me
Simply removing the arrow just doesn't work. The |...| syntax could be made to work but feels much noisier to me than an arrow.
The match syntax with -> is also almost identical to Gleam's case expression which is very readable and clear
Yeah, this seems way too noisy to me
I definitely prefer a version with an arrow to all of the options proposed so far
ok, how about this?
-> over pipes, no commas-> the formatter inserts itI like that!
That's probably my favorite, I'd like to suggest keeping the commas as optional ignored syntax in a branch-terminal position that is stripped by the formatter
So that would be:
match e {
Admin({ email, .. }) | Guest(email) if email.ends_with("gmail.com") -> email
}
are there any ambiguity problems with a guard and omitted ->?
match a {
Ok(num) if num > 3 { Big }
Ok(num) if num == 3 { Small }
Err(_) Other
}
it's either unintentionally missed else branch or a guard. how would formatter work in this case?
the lack of an else means the only correct way to interpret it would be with a guard
although I guess if the whole expression were evaluating to {} then technically it could be an if-without-else :laughing:
the lack of an
elsemeans the only correct way to interpret it would be with a guard
yes, but if I type the following:
match a {
Ok(num) if num > 3 { Big }
Err(_) Other
}
what was my intention? this
match a {
Ok(num) -> if num > 3 { Big } else { Small }
Err(_) -> Other
}
or that?
match a {
Ok(num) if num > 3 -> { Big }
Err(_) Other
}
it's likely the second variant makes more sense... so you can add another Ok arm immideatly after the first Ok and it will look better than nested branching... ok, no concerns then
match a {
Ok(num) if num > 3 -> Big
Ok(num) -> Small
Err(_) -> Other
}
that's interesting. are there any cases when if/else expr inside of the pattern arm is better for readability than a guard and an additional arm? useless question tho
I think this is ambiguous without explicit ->s:
b = | a | {
match a {
Ok(num) if num > 3 { return Big }
Err(_) Other
}
}
Would this result in a non-exhaustive pattern error due to Ok appearing with only a guard clause, or in a "you are not returnig anything if the num > 3 branch isn't taken" error?
Suggested fix for case 1)
b = | a | {
match a {
Ok(num) if num > 3 -> { return Big }
Ok(_) Small
Err(_) Other
}
}
Suggested fix for case 2)
b = | a | {
match a {
Ok(num) -> if num > 3 { return Big } else { return Small }
Err(_) Other
}
}
actually, there's a problem that can't be solved by the formatter:
match a {
Ok(num) if num > 3 { Big }
_ Other
}
if it resolves to
match a {
Ok(num) if num > 3 -> Big
_ -> Other
}
then there are no compiler warnings. however the intention might have been
match a {
Ok(num) -> if num > 3 { Big } # ouch, I forgot about the else case!
_ -> Other
}
Would this result in a non-exhaustive pattern error due to Ok appearing with only a guard clause, or in a "you are not returnig anything if the num > 3 branch isn't taken" error?
that's exactly what I was talking about above. but there's no real problem with non-exhaustive patterns. in any case, your (or mine from above) example has no false positives regarding exhaustiveness.
the real problem is when the formatter thinks that the match arms are exhaustive when it's probably not.
it's probably fine to say in this exact example we don't try to fix it with the formatter, considering it will almost certainly come up in practice approximately zero times ever :big_smile:
My example isn't about the formatter. It's "what's the error?" which depends on how the -> would be inserted. But the lack of -> makes it ambigous.
yeah, sorry - I mean we just say that's an error
as opposed to something we allow (with a warning) and then just fix with the formatter
If we keep using arrows in match, can should we use =>? It will help greppability and balance the symbol overloading from single+triple to double+double.
personally it's always annoyed me that Rust has -> for functions and => for match because until I got used to it, I had to remember which was which
I don't think I've ever grepped for => in Rust; I'm not sure what value that actually has in practice as opposed to in theory :sweat_smile:
->for functions and=>formatch
Fair, but Roc has => for functions too - I wonder if that eases it.
yeah that's part of the reason I wanted to try not using an arrow at all :sweat_smile:
not having to remember which of the two arrows to use
with -> over . I think it's easy because C, C++, and PHP all use -> in exactly that way among mainstream languages, and ReasonML uses it among functional languages, and no language I've ever heard of uses => as a substitute for .
but yeah, for match branches it's kinda like "well, ML family languages use -> but Rust uses => and Roc has both -> and => in other places, so..."
but I do think if we had the parser accept both and had the formatter silently correct it to the other one, that would address that problem
Yeah, if keyboard ergonomics or muscle memory are a significant concern.
Using -> and => for purity in function types does seem to be driving a surprising number of other syntax decisions :thinking:
I agree, but it's such a great fit for that one use case I'd rather have it driving other syntax decisions than change it :big_smile:
Oh, I'm not arguing against purity inference. I'm arguing for either: (1) not allowing that syntax to dictate so much syntax elsewhere, or (2) perhaps using something different to indicate purity in types.
yeah I think (2) sounds like a mistake
"I like my arrows; stop trying to take away my arrows"
the way I see it, -> and => is like a 10 out of 10 unbelievably great fit for that use case
and the others are like "well, there are a lot of ways we could go with this..."
and I think it's fine if the 10 out of 10 one, which comes up all over the place in code bases, is the variable we don't adjust
I would rather pick one of the "this could work" options for other syntaxes than change -> and => to something that's worse
To be fair, despite my concern of symbol overloading, we're already doing better on that:
Before:
-> for all functions in types-> for function definitions-> for match branchesNow:
-> for some functions in types-> for local function dispatchIMO -> as a notion of "transformation" (from the thing on the left to the thing on the right) is a much more important notion than => vs -> for purity
if there's a specific syntax that seems better than -> and => for purity in function types, I'm open to being convinced, but I think that would need its own #ideas thread :big_smile:
I think I'm arguing more for (1) above, aka "just keep using arrows elsewhere and it'll be fine"
i.e. -> in match
In a certain sense, it's still "correct" since the act of matching (picking which branch you want) is definitely still pure
This does bring up a possible ambiguity between local function dispatch and match branches with if guards.
match foo {
Ok(a) if a->is_error()->foo()
_ -> other()
}
Is that parsed as:
Ok(a) if a -> (is_error()->foo())
Or:
Ok(a) if (a->is_error()) -> foo()
?
yep, great point
well the easiest fix is to use => for this like Rust does :big_smile:
As an amendment to my previous point about enhanced greppability, what I'm really thinking of is, in a modal editor, doing something like spawning a cursor for each branch by selecting the =>s and then selecting/editing adjacent expressions in a batch.
totally, and I do that sort of thing all the time in general - but in Rust I've never done it with => specifically
Also, readability/parsability is ten times more important than editability.
yeah, even without the ambiguity, Ok(foo) => a->b is nicer to read than Ok(foo) -> a->b
Just let me know if I need to update the parser/formatter again :rofl:
Sam Mohr said:
get_user_email = |Admin({ email, .. }) or Guest(email)| email
Just letting you know this won't work in function args with the || syntax while also keeping | for pattern alternatives
Yep
I'm not sure how important that feature really is, but it makes or a better option
I feel like it is probably better to require putting that on the next line
Still wouldn't require a when is
Or match
Do you mean requiring the alternating pattern to be in a match statement, or maybe an assignment?
AKA banning it from being in the arg?
Ban this in general.
get_user_email = |Admin({ email, .. }) | Guest(email)| email
Require:
get_user_email = |user| {
Admin({ email, .. }) | Guest(email) = user
email
}
Like I don't think we need to user or, I think it is fine to just ban using | in function args (or I guess requiring parens to use | in args)
I think we move to or, then that becomes an extra piece of indirection that would just be annoying
I'm not saying that we should move to or
Just that this seems to be a problem specifically with | meaning multiple things
Yep
I don't have a strong preference either way for or vs | but I do like that or is more self-documenting for a feature that's not super commonly used (although not exactly rare either)
I also don't feel super strongly about or being allowed in patterns outside match, but I do like that it's one less rule for people to learn
as in, patterns work exactly the same way everywhere except at the top level where you only get identifiers and that's it (at least for now)
but both of those do seem to be points in favor of or
except at the top level where you only get identifiers
Oh hmm, didn't realize this was true?
If it's an infallible destructure, why can't you?
The Rust-based Roc compiler allows patterns at the top level
Two points against that probably aren't actually blockers:
Richard says that he and Folkert implemented them before
It makes for really awkward type annotations
Ahhh yeah, agreed on that
I guess I wouldn't be sad
I just think they should be unsupported in 0.1.0
It would be nice to support it
That's fine
they can always be added later as a nonbreaking change
I think the main reason to do it would be to remove a rule to learn
not bc I think it would actually see a lot of use
if it were easy then sure, why not? But it's not easy, so I say hold off :big_smile:
Last updated: Jun 16 2026 at 16:19 UTC