One of the weirdest part of roc is currently it's special type level syntax.
It is very confusing to use symbols / syntax at type level, that have different meaning at value level.
Context sensitivity and ambiguity is huge anti-pattern (unless the concepts are analogous).
Finally, it also closes the doors to potential dependent type extension in the future.
It's not very likely at this stage that roc will ever have full dependent types.
However, I can imagine a future where the type system is extended with a less powerful feature: array dimensions, const generics, ..
Having a unified syntax would significantly simplify such endeavor.
The closer the value and type level syntax is, the easier it is to:
How that looks like:
Here I will put some examples of what I mean.
I don't argue this is how the final syntax should look like.
But rather how an unambiguous and unified syntax looks like.
Examples:
Don't make operators act as arguments, i.e. list : List *.
Better approach is e.g.
list : List _
Don't overload existing operators, i.e. person : {name: Str}*.
Better approach is e.g. `
person : {_ & name : Str}`
Unify record syntax (&):
happyBirthday : {person & age : Int} -> {person & age : Int }
happyBirthday = {person & age } -> {person & age = age + 1}
Unify union syntax (|):
ChickenEgg : Chicken | Egg
chickenEgg : Chicken | Egg -> Problem
chickenEgg = Chicken | Egg -> Problem -- best
chickenEgg x = when x is Chicken | Egg -> Problem -- also solid
Can you maybe elaborate? I am not sure I understand what is the problem, nor what are you trying to propose as a solution :)
I completely agree with @Kesanov . I found it weird that type annotations use : but record field assignment also uses :. I also like the idea of using _ as wildcard instead of *. I'm not sure what's being portrayed in the Unify union syntax section though. Using | instead of [] since [] is already used for lists?
Exactly. The [a,b] is used for lists and therefore should not be used for union.
Since there is already the when .. is A | B -> .. syntax, it is natural to also use it at type level.
what syntax do you propose for tag union type variables?
Can you explain what you mean by each of the four lines in the chicken and egg example? How do they relate to each other?
I think that {person & age } -> {person & age = age + 1} is really confusing syntax.
I feel that that mixes many meaning into one line. It feels like a weird form of partial struct decomposition.
also, can you show multiline versions of the proposed syntax side by side with the current syntax?
Here's my interpretation of it:
**Old** # **New**
list : List * # list : List _
person : {age : Nat}* # person : {_ & age : Nat}
person : { # person : {_ &
age : Nat # age : Nat
}* # }
person = {age : 1} # person = {age = 1}
person2 = {person & age : 2} # person2 = {person & age = 2}
ChickenEgg : [Chicken I32, Egg I32] # ChickenEgg : | Chicken I32 | Egg I32
ChickenEgg : [ # ChickenEgg :
Chicken I32, # | Chicken I32
Egg I32 # | Egg I32
]
Multiline open record gets a little weird, but I do think I like it better. Also tag union seems too different from how the rest of the language prefers lists of things to be surrounded by opening and closing characters.
Your person isn't valid. It needs a name.
yea I was just using the provided example, will change
Oh, I meant I think it is supposed to be:
person : {_ & name : Str}
somePerson = { name: "Dave" }
somePersonWithAge = { name: "Diana", age: 16 }
happyBirthday : { person & age : Int } -> { person & age : Int }
happyBirthday = \{ person & age } -> { person & age = age + 1 }
happyBirthday somePerson # INVALID: this person does not have an age
happyBirthday somePersonWithAge # Returns: { name: "Diana", age: 17 }
can someone show multiline versions of the proposed syntax side by side with the current syntax?
I think it's important to see them side by side, and multiline versions of both!
I mean I think the fundamental multi-line change is in @Jared Cone's example above.
# Record Before:
Person : {
first_name: Str,
last_name: Str,
}*
# Record After:
Person:
_
& first_name: Str
& last_name: Str
# Union Before:
Animal : [
Cat,
Dog,
Wolf,
]
# Union After:
Animal :
Cat
| Dog
| Wolf
Though there obviously could be some minor variations. Should record/union be wrapped or have proceeding characters?
I am not actually sure how to interpret the happyBirthday syntax. It seems to have a weird mix of record update syntax and the newly proposed record syntax. So idk.
Then I guess for the final ChickenEgg example the change would be:
# Before:
ChickenEgg : [ Chicken, Egg]
someFunc : ChickenEgg -> [ Problem ]
someFunc = \x ->
when x is
Chicken | Egg -> Problem
# After:
ChickenEgg : Chicken | Egg
someFunc : ChickenEgg -> Problem
someFunc = \x ->
when x is
Chicken | Egg -> Problem
# Though it looks like they also want to support an implicit when which would lead to:
# I have no idea how this would work with actual pattern matching instead of everything returning problem
ChickenEgg : Chicken | Egg
someFunc : ChickenEgg -> Problem
someFunc = \Chicken | Egg -> Problem
Side note. I guess if you took this to the extreme, a record would just be a chain of & and a union would be a chain of |. No extra syntax around it. Also, _ just gets added to make it open.
-> This might get complex to parse when you have nested records and unions unless you name them all or have wrapping brackets still.
what would a multiline tag union type look like if it had a type variable?
You mean [ Cat ]*? That could be (Cat | _).
This nicely matches with
when .. is
Cat | _ -> ..
I assume no special change?
# Before:
SomeUnion a b : [
First a
Second b
Third a b
]
# After:
SomeUnion a b :
First a
| Second b
| Third a b
oh I mean as in an open union
The | _ makes it open.
like [ A, B ]blah today
If it has to be named, I guess it would be A | B | blah
what if what goes in there is another named type? Like today I can compose unions like this: [ A, B ]Foo as long as Foo is a type alias for a closed tag union
is that still supported in this syntax?
I don't think you would be able to distinguish Foo from just another piece of the union
Sure. (A | B) | (C | D) == A | B | C | D.
| is associative.
This is different. This is asking:
SomeUnion : A | B | C
OtherUnion : D | E | SomeUnion
Is SomeUnion a tag in OtherUnion or does it reference A | B | C
exactly
Cause it might be the case that I actually want a tag called SomeUnion inside of OtherUnion.
right
Theoretically would could add some sort of syntax that say "expand this, it isn't a tag", but yeah, I think it would need to be an addon
Ah I see. I am afraid that would need special syntax. Like D | E | SomeUnion..
However, as a bonus, it would work in pattern matching too (currently there is no support for that).
I am not sure I understand the question. Syntax for which feature? Isn't the Task Str _ the syntax?
yes, but the proposal is to use _ as the symbol that today is *
so the question is: given that _ is already used for something else today, if we change it to mean something else, what's the replacement syntax for the feature that is currently represented using _ today, since in this proposal _ will mean something else?
in other words: today I can write:
Task Str _Task Str *and these mean different things
if the proposal is to change the syntax for 2. to be Task Str _ then what's the proposed replacement syntax for 1?
Hm, I wasn't aware of that. I have only read the roc-for-elm..md from the zip, which might be outdated.
It does not mention the Task Str _ syntax AFAIK.
I'm not sure if it's in there, actually - might need to be added!
given discussion in beginners and now here, I actually am not sure * is a win overall. Almost always you end up needing to name that variable, in particular in type/alias definitions.
the * looks weird, so it always draws attention, but then the explanation is something like "oh that's type variable but it's unnamed. See if we don't need to name it, we can use *"
but then to explain what * is, you actually need to go into type variables and their names
that's certainly possible
So what does Task Str _ currently mean? Does it make compiler suggest the correct type or something?
it means "I'm choosing not to annotate this part of the type"
you could add x : _ to any declaration of x and it would be the same as if you hadn't added an annotation
the _ in that position has strong precedent, e.g. in rust or haskell
it also has a very similar meaning to what _ does in a pattern match: "I know there's something here, but I don't care what shape it is and I don't want to name it"
Hm, I think idris uses this syntax for it: x : ?a. You can then ask what the type of a is, and the compiler will tell you.
It's really nice, if you have multiple missing annotations at the same time.
Or it could be x : _a. I think there are many good possibilities.
we can just do that in the editor though - just highlight the _ you want to know and it'll tell you the inferred type
yeah idris and agda go further. They also rely on editor support to fill those holes
another consideration:
SomeUnion a b :
First a
| Second b
| Third a b
this is the syntax Elm uses, and I know from experience in Elm that a lot of people prefer a syntax for this where you get the same version control diff no matter which element you remove. In this proposed syntax, removing the first entry gives a multi-line diff (because you have to change | Second b to Second b, whereas removing any of the others gives a single-line diff
F# does something like this:
SomeUnion a b :
| First a
| Second b
| Third a b
another consideration is how this looks in a function declaration
e.g. today we have
walkUntil : List elem, state, (state, elem -> [ Continue state, Done state ]) -> state
instead I'm assuming this would become:
walkUntil : List elem, state, (state, elem -> (Continue state | Done state)) -> state
I feel like the union syntax in general works better with | Though I am fine with it still having a type variable outside the () or [].
So like I think any of these are reasonable:
A | B | SomeOtherUnion...[ A | B ]SomeOtherUnion( A | B )SomeOtherUnionAlso, I know it is a weird concept, but we could do trailing | like is done with lists and ,:
SomeUnion a b :
First a |
Second b |
Third a b |
if I put Foo into the repl in this world, what would it print?
I guess Foo : Foo | ... ?
eww
Some reason trailing | looks much much worse than trailing , .... so nvm.
I like the idea of the closed tag union syntax with the pipes, but not with the trailing pipes or comma (the closed ones are great though). what about leading with it, like ChickenEgg : * | Chicken I32 | Egg I32. Or maybe ChickenEgg :* Chicken I32 | Egg I32. The issue I see is the * being hard to pick out at a glance. The type level record declaration change looks clunky. The change to wildcards could make the possibility of Typed Holes hard to add if that ever comes up, as I know some people do like those
a downside of having the type variable up front is that it's often the least interesting part of the type, so you always have to read past it.
One of the reasons the current design sticks it at the end after a closing brace is that it's as unobtrusive as possible: ] becomes ]* or } becomes }* - it's a single character tucked away at the very end of the type
I don't think there is a good looking, unobtrusive way to describe open unions with pipes. would it ever be disadvantageous to completely remove delineation and use newlines instead (allowing for line wrapping via tabs of course)? that way you can still have the * at the end on its own line. something like the following:
ChickenEgg a :
Chicken a
Egg a
*
This wouldn't work well for single lines though. Below is an example of that
ChickenEgg a : Chicken a | Egg a | *
Though in the end I can't see how this seriously improves the programming experience (I think the asterisk is a good balance, and I like the brackets). Is this just bikeshedding?
I actually started out with the pipe syntax and evolved it into the current syntax based on these issues :big_smile:
I do like the * being an item in the tags and unions rather than the outside, such as [SomeTag, *], { someField : I32, * }
Yeah, just has the issue when it is named cause we can't tell if it is another tag or a separate union. Would have to be distinguished with other syntax.
Though I definitely think something like this could look fine [ SomeTag, ..OtherUnion ]
Require the other union to always be last just like the *
so then what would it look like if you put Foo into the repl? Same as today?
This: Foo : [ Foo, * ]
Though I also think this would be reasonable: Foo : [ Foo, .. ]
But kinda overloads the .. syntax.
so then the type of List.get would be:
get : List elem -> Result elem [ OutOfBounds, * ]`
Looks right.
@Kesanov and @Jared Cone thanks for these proposals - I like seeing this discussion, though I have little to add to it
I generally like the idea of type alias syntax more closely matching definition intuitions, though the devil is in the details
Richard Feldman said:
F# does something like this:
SomeUnion a b : | First a | Second b | Third a b
I think this syntax is good. There are already trailing commas, to extend this concept for starting element seems logical.
Richard Feldman said:
a downside of having the type variable up front is that it's often the least interesting part of the type, so you always have to read past it.
Not that I'm necessarily in favor of the leading *, but fwiw it's also the most concise part of the type. If it's at all important to visually distinguish between open and closed types in practice, then it's important for the openness indicator to be visible: that can be assured by having code-formatters, and style conventions spread longer definitions across multiple lines (ideal), or it can also be assured by putting the relevant token near the beginning of the declaration so that even unwrapped long lines (non-ideal) will still visually indicate openness without scrolling.
Richard Feldman said:
One of the reasons the current design sticks it at the end after a closing brace is that it's as unobtrusive as possible:
]becomes]*or}becomes}*- it's a single character tucked away at the very end of the type
The "tucked away" phrasing suggests to me that it's not necessary, though I know this not to be the case. Things that are important should be prominent, and, ideally, things that are unimportant should be invisible/non-existent if possible.
Brendan Hansknecht said:
So like I think any of these are reasonable:
A | B | SomeOtherUnion...
I was somewhat surprised by the trailing * part of tag unions and other types when I first saw it, but was especially surprised when I first saw {x: Int}a, thinking it to be fairly inelegant at first appearance. Further surprising to me was that the meaning changes if any space is present, i.e. {x: Int}a means something different than {x: Int} a, and thus the rule makes [A, B]SomeOtherUnion potentially a bit awkward for newcomers: it may end up being a "false friend" to other language's similar-syntax forms that have unrelated meaning, such as perhaps some variation on an "array of SomeOtherUnion" elements, were A and B might be length ranges or some such).
I have since learned that * is the most common modifier, when one is present at all, and that it's intended to allow for [A, B] to be the same as [A][B]. How often does the need arise to merge/unify types using this syntax? Could indeed a clearer syntax like [A, B, SomeOtherUnion...] or [A, B, *SomeOtherUnion] or any of the other examples mentioned, solve this need equally well? Are there important aspects of the current "type concatenation" that would be lost with such a change?
I don't really understand the nuanced difference between _ and *. I had thought Roc will always attempt to infer the most general type, thus if you use _, wouldn't Roc generally infer * anyways unless you're using some constraining aspect of the language (like how when constrains the result type to be closed)? Even then, wouldn't Roc reject an explicit * when Roc knows that only a closed set of values may be produced?
Richard Feldman said:
so then the type of
List.getwould be:get : List elem -> Result elem [ OutOfBounds, * ]
Strictly speaking, isn't that valid Roc syntax right now (or could it be close to valid syntax, since Roc accepts * in type position elsewhere)? Would it mean "OutOfBounds, or any single tag, but it doesn't matter what that tag is" ? If so, in effect, wouldn't that be very close to an open tag union anyways?
iow, is there actually value in the distinction between "a single thing that can be anything" and "any number of things that can be anything" ? Is there even a distinction between those two phrasings in a tag union or any other part of the type system?
Maybe minor in this discussion, but FWIW I think you're misunderstanding open tag unions in the same way I used to misunderstand them. Result something [ OutOfBounds ]* doesn't mean that any error (including OutOfBounds) is possible there - instead, it means that the only possible error there is OutOfBounds, and that that error can be passed on to functions that handle more errors than just OutOfBounds. The * means that these possibilities are allowed to be used in the context of broader possibility spaces.
JanCVanB said:
Maybe minor in this discussion, but FWIW I think you're misunderstanding open tag unions in the same way I used to misunderstand them. `
Ah, right. Thanks!
For example, if I wanted an error to only be handled by web-server-specific error handlers, I can guarantee that by writing Result something [ FileNotFound ]WebServerError, so that anything that touches it downstream must be scoped to that specific possibility space. It's a matter of correctness/intent/scoping, rather than a matter of unknown/unspecified outputs.
(I still get confused about tag unions, and idk if that example is legitimate... but that's how I think about them now)
Last updated: Jun 16 2026 at 16:19 UTC