Stream: ideas

Topic: Same type and value level syntax.


view this post on Zulip Kesanov (Mar 25 2022 at 08:21):

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

view this post on Zulip Zeljko Nesic (Mar 25 2022 at 14:19):

Can you maybe elaborate? I am not sure I understand what is the problem, nor what are you trying to propose as a solution :)

view this post on Zulip Jared Cone (Mar 25 2022 at 16:01):

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?

view this post on Zulip Kesanov (Mar 25 2022 at 16:07):

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.

view this post on Zulip Richard Feldman (Mar 25 2022 at 16:16):

what syntax do you propose for tag union type variables?

view this post on Zulip Brian Carroll (Mar 25 2022 at 16:16):

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?

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 16:16):

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.

view this post on Zulip Richard Feldman (Mar 25 2022 at 16:17):

also, can you show multiline versions of the proposed syntax side by side with the current syntax?

view this post on Zulip Jared Cone (Mar 25 2022 at 19:15):

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.

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:18):

Your person isn't valid. It needs a name.

view this post on Zulip Jared Cone (Mar 25 2022 at 19:20):

yea I was just using the provided example, will change

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:25):

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 }

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:27):

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!

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:30):

I mean I think the fundamental multi-line change is in @Jared Cone's example above.

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:35):

# 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?

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:38):

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.

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:42):

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

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:47):

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.

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:50):

what would a multiline tag union type look like if it had a type variable?

view this post on Zulip Kesanov (Mar 25 2022 at 19:53):

You mean [ Cat ]*? That could be (Cat | _).
This nicely matches with

when .. is
  Cat | _ -> ..

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:54):

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

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:54):

oh I mean as in an open union

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:54):

The | _ makes it open.

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:55):

like [ A, B ]blah today

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:55):

If it has to be named, I guess it would be A | B | blah

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:56):

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

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:57):

is that still supported in this syntax?

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:57):

I don't think you would be able to distinguish Foo from just another piece of the union

view this post on Zulip Kesanov (Mar 25 2022 at 19:57):

Sure. (A | B) | (C | D) == A | B | C | D.

view this post on Zulip Kesanov (Mar 25 2022 at 19:58):

| is associative.

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 19:59):

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

view this post on Zulip Richard Feldman (Mar 25 2022 at 19:59):

exactly

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 20:00):

Cause it might be the case that I actually want a tag called SomeUnion inside of OtherUnion.

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:00):

right

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 20:01):

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

view this post on Zulip Kesanov (Mar 25 2022 at 20:03):

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).

view this post on Zulip Kesanov (Mar 25 2022 at 20:11):

I am not sure I understand the question. Syntax for which feature? Isn't the Task Str _ the syntax?

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:14):

yes, but the proposal is to use _ as the symbol that today is *

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:16):

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?

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:17):

in other words: today I can write:

  1. Task Str _
  2. Task Str *

and these mean different things

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:18):

if the proposal is to change the syntax for 2. to be Task Str _ then what's the proposed replacement syntax for 1?

view this post on Zulip Kesanov (Mar 25 2022 at 20:18):

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.

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:18):

I'm not sure if it's in there, actually - might need to be added!

view this post on Zulip Folkert de Vries (Mar 25 2022 at 20:21):

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.

view this post on Zulip Folkert de Vries (Mar 25 2022 at 20:22):

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 *"

view this post on Zulip Folkert de Vries (Mar 25 2022 at 20:23):

but then to explain what * is, you actually need to go into type variables and their names

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:25):

that's certainly possible

view this post on Zulip Kesanov (Mar 25 2022 at 20:26):

So what does Task Str _ currently mean? Does it make compiler suggest the correct type or something?

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:27):

it means "I'm choosing not to annotate this part of the type"

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:28):

you could add x : _ to any declaration of x and it would be the same as if you hadn't added an annotation

view this post on Zulip Folkert de Vries (Mar 25 2022 at 20:28):

the _ in that position has strong precedent, e.g. in rust or haskell

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:30):

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"

view this post on Zulip Kesanov (Mar 25 2022 at 20:30):

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.

view this post on Zulip Richard Feldman (Mar 25 2022 at 20:30):

we can just do that in the editor though - just highlight the _ you want to know and it'll tell you the inferred type

view this post on Zulip Folkert de Vries (Mar 25 2022 at 20:31):

yeah idris and agda go further. They also rely on editor support to fill those holes

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:51):

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

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:52):

F# does something like this:

SomeUnion a b :
    | First a
    | Second b
    | Third a b

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:53):

another consideration is how this looks in a function declaration

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:54):

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

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 21:57):

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:

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 21:58):

Also, 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 |

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:58):

if I put Foo into the repl in this world, what would it print?

view this post on Zulip Richard Feldman (Mar 25 2022 at 21:59):

I guess Foo : Foo | ... ?

view this post on Zulip Folkert de Vries (Mar 25 2022 at 21:59):

eww

view this post on Zulip Brendan Hansknecht (Mar 25 2022 at 21:59):

Some reason trailing | looks much much worse than trailing , .... so nvm.

view this post on Zulip Thomas Dwyer (Mar 25 2022 at 23:14):

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

view this post on Zulip Richard Feldman (Mar 25 2022 at 23:28):

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

view this post on Zulip Thomas Dwyer (Mar 25 2022 at 23:54):

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?

view this post on Zulip Richard Feldman (Mar 25 2022 at 23:57):

I actually started out with the pipe syntax and evolved it into the current syntax based on these issues :big_smile:

view this post on Zulip Jared Cone (Mar 26 2022 at 00:07):

I do like the * being an item in the tags and unions rather than the outside, such as [SomeTag, *], { someField : I32, * }

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:17):

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.

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:18):

Though I definitely think something like this could look fine [ SomeTag, ..OtherUnion ]

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:19):

Require the other union to always be last just like the *

view this post on Zulip Richard Feldman (Mar 26 2022 at 00:29):

so then what would it look like if you put Foo into the repl? Same as today?

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:34):

This: Foo : [ Foo, * ]

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:35):

Though I also think this would be reasonable: Foo : [ Foo, .. ]

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:35):

But kinda overloads the .. syntax.

view this post on Zulip Richard Feldman (Mar 26 2022 at 00:38):

so then the type of List.get would be:

get : List elem -> Result elem [ OutOfBounds, * ]`

view this post on Zulip Brendan Hansknecht (Mar 26 2022 at 00:40):

Looks right.

view this post on Zulip jan kili (Mar 26 2022 at 00:51):

@Kesanov and @Jared Cone thanks for these proposals - I like seeing this discussion, though I have little to add to it

view this post on Zulip jan kili (Mar 26 2022 at 00:51):

I generally like the idea of type alias syntax more closely matching definition intuitions, though the devil is in the details

view this post on Zulip Kesanov (Mar 26 2022 at 06:53):

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.

view this post on Zulip Kevin Gillette (Mar 27 2022 at 02:47):

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.

view this post on Zulip Kevin Gillette (Mar 27 2022 at 02:59):

Brendan Hansknecht said:

So like I think any of these are reasonable:

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?

view this post on Zulip Kevin Gillette (Mar 27 2022 at 03:03):

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?

view this post on Zulip Kevin Gillette (Mar 27 2022 at 03:23):

Richard Feldman said:

so then the type of List.get would 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?

view this post on Zulip jan kili (Mar 27 2022 at 05:32):

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.

view this post on Zulip Kevin Gillette (Mar 27 2022 at 05:35):

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!

view this post on Zulip jan kili (Mar 27 2022 at 05:36):

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.

view this post on Zulip jan kili (Mar 27 2022 at 05:38):

(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