Stream: ideas

Topic: Binding type variables to multiple abilities


view this post on Zulip Ayaz Hafiz (Oct 08 2022 at 22:29):

Soon we'll have support for Dictionaries that implement their behavior via value hashing, and in order for their API to be generic, they'll need a way to say that they accept keys that support both the Hash and Eq ability. Right now we have ways to say that a type variable supports one ability, but not multiple. Here are a few ideas for what a syntax for that would look like:

Personally I find the "Plus" variety the most readable. Thoughts? Any other ideas?

view this post on Zulip Ayaz Hafiz (Oct 08 2022 at 22:31):

I also don't think the "Individual clauses" variety is too bad, but I think it would get much more noisy if we rename a has Hash to a supports Hash - in that case

a supports Hash + Eq

feels much more readable than

a supports Hash, a supports Eq

even before more type variables are involved

view this post on Zulip Brendan Hansknecht (Oct 08 2022 at 23:13):

I agree around noise and plus being the most readable of these options.

view this post on Zulip jan kili (Oct 09 2022 at 00:33):

I'm not advocating for it, but we should consider &

view this post on Zulip jan kili (Oct 09 2022 at 00:35):

view this post on Zulip jan kili (Oct 09 2022 at 00:36):

Mathematically, plus seems the best to me

view this post on Zulip jan kili (Oct 09 2022 at 00:36):

Linguistically, ampersand seems the best to me

view this post on Zulip Brendan Hansknecht (Oct 09 2022 at 02:32):

I noticed that in Json.roc, something Json has encode and decode. That is written like a has [Hash, Eq]. I would assume we want symmetry between has for requiring an ability and has for defining a type implements an ability?

view this post on Zulip Luke Boswell (Oct 09 2022 at 03:51):

When I read a has [Hash, Eq] it looks similar to Tags and so it feels like an OR so Hash | Eq. If it is to be read an an AND then my preference is for the &, particularly noting the multi-variable clauses you have presented.

view this post on Zulip Zeljko Nesic (Oct 09 2022 at 22:16):

How about a has (Hash, Eq) in the vain of Haskell's typeclasses annotation?

view this post on Zulip Ayaz Hafiz (Oct 11 2022 at 21:41):

Brendan Hansknecht said:

I noticed that in Json.roc, something Json has encode and decode. That is written like a has [Hash, Eq]. I would assume we want symmetry between has for requiring an ability and has for defining a type implements an ability?

I do like the symmetry, but I know others have found it confusing - and we've talked about switching to a keyword like "supports" in these situations, so you would instead have e.g.

Hash has
  hash : hasher, a -> hasher | hasher supports Hasher, a supports Hash

In that world I don't think k supports [Hash, Eq] would work too well, since we'd be conflating the "declaration" syntax with the "usage" syntax which folks could find confusing

view this post on Zulip Ayaz Hafiz (Oct 11 2022 at 21:41):

I'm going to spike out the Hash & Eq idea. The syntax is the easiest part to change, so we can experiment.

view this post on Zulip Ayaz Hafiz (Oct 12 2022 at 22:46):

The first pass implementation of the ampersand idea Hash & Eq is up at PR 4305. Here are some tests showing what that would look like, and error messages when you duplicate or forget to include certain abilities:

https://github.com/roc-lang/roc/blob/14ef05844a8416190a5ce202d9c2bfbd3787667b/crates/reporting/tests/test_reporting.rs#L11136-L11268

view this post on Zulip Brendan Hansknecht (Oct 15 2022 at 15:37):

Is any form of this implemented yet?

view this post on Zulip Ayaz Hafiz (Oct 15 2022 at 15:37):

PR above

view this post on Zulip Brendan Hansknecht (Oct 15 2022 at 15:42):

Maybe I am missing something, but I only see the single file diff. Not immediately seeing a way to go to the pr from that.

view this post on Zulip Ayaz Hafiz (Oct 15 2022 at 15:43):

it’s https://github.com/roc-lang/roc/pull/4305

view this post on Zulip Brendan Hansknecht (Oct 15 2022 at 15:46):

thanks

view this post on Zulip Kevin Gillette (Oct 25 2022 at 19:55):

From https://github.com/roc-lang/roc/pull/4305#discussion_r1003631445, per @Richard Feldman suggesting where as looking cleaner than | with ,:

With consistent prefix punctuation, this could look pretty clean with | and , with the following format:

f : {} -> a
  | a has Eq & Hash & Decode
  , b has Eq & Hash

(at least from my perspective, mixing prefix and suffix punctuation/tokens should be avoided)

Alternatively, it'd look pretty clean to have:

f : {} -> a
  | a has Eq & Hash & Decode
  | b has Eq & Hash

view this post on Zulip Brian Carroll (Oct 25 2022 at 21:13):

My one hesitation is that this is using & and | where & means 'and' but | doesn't mean 'or'.

view this post on Zulip Luke Boswell (Oct 25 2022 at 22:03):

What about something like this?

foo : a, b -> c
    | a has Eq, Hash, Decode
    | b has Eq, Hash

view this post on Zulip Richard Feldman (Oct 26 2022 at 00:49):

alternatives:

foo: a, b -> c
    | a implements Eq, Hash, Decode
    | b implements Eq, Hash
foo: a, b -> c
    where
        a implements Eq & Hash & Decode,
        b implements Eq & Hash

view this post on Zulip Richard Feldman (Oct 26 2022 at 00:52):

also there's the question of how they look in the more common single case:

insert : Dict k v, k, v -> Dict k v
    | k has Hash, Eq
insert : Dict k v, k, v -> Dict k v
    | k implements Hash, Eq
insert : Dict k v, k, v -> Dict k v
    where k implements Hash, Eq

view this post on Zulip Richard Feldman (Oct 26 2022 at 00:55):

I think it's easier to guess what | does when it's implements instead of has

view this post on Zulip Luke Boswell (Oct 26 2022 at 01:06):

So more like this? I think it feels clean. I prefer typing in the comma instead of the ampersand, and using the bar on a separate line is easy to read.

insert: Dict k v, k, v -> Dict k v
    | k implements Eq, Hash, Decode
    | v implements Eq, Hash

insert: Dict k v, k, v -> Dict k v
    | a implements Eq, Hash, Decode

view this post on Zulip Brendan Hansknecht (Oct 26 2022 at 01:13):

I'm not a fan of a | on each line. It feels like it would be some sort of nested where clause. Feels too separate to me. But I'm sure I'd get used to it eventually.

view this post on Zulip Brendan Hansknecht (Oct 26 2022 at 01:13):

Would prefer just to have the next line tabbed in

view this post on Zulip Brendan Hansknecht (Oct 26 2022 at 01:15):

something: x, y -> y
    | x implements Eq
      y implements Hash

view this post on Zulip Luke Boswell (Oct 26 2022 at 01:18):

Like this? also would this be where it lives, i.e. before the function implementation?

insert: Dict k v, k, v -> Dict k v
    | k implements Eq, Hash, Decode
    | v implements Eq, Hash
insert = \dict, key, value ->
    # code here
    # etc

insert: Dict k v, k, v -> Dict k v
    | a implements Eq & Hash & Decode
insert = \dict, key, value ->
    # code here
    # etc

insert: Dict k v, k, v -> Dict k v
    k implements Eq, Hash, Decode
    v implements Eq, Hash
insert = \dict, key, value ->
    # code here
    # etc

insert: Dict k v, k, v -> Dict k v
    a implements Eq & Hash & Decode
insert = \dict, key, value ->
    # code here
    # etc

view this post on Zulip Richard Feldman (Oct 26 2022 at 04:02):

something we haven't discussed:

insert: Dict k v, k, v -> Dict k v
    & k implements Eq, Hash, Decode
    & v implements Eq, Hash
insert = \dict, key, value ->

view this post on Zulip Kevin Gillette (Oct 26 2022 at 05:11):

Brendan Hansknecht said:

something: x, y -> y
    | x implements Eq
      y implements Hash

That would suggest it could also read (with poorer formatting) as:

something: x, y -> y
    | x implements Eq y implements Hash

Is that something we'd want to permit in the grammar, or would the spacing/line-endings be significant?

In the inlined form above, I might read that as "x implements (Eq y) which in turn implements Hash"

view this post on Zulip Kevin Gillette (Oct 26 2022 at 05:17):

Richard Feldman said:

foo: a, b -> c
    where
        a implements Eq & Hash & Decode,
        b implements Eq & Hash

Perhaps also:

foo: a, b -> c
    where a implements Eq, Hash, Decode
    where b implements Eq, Hash

(treat where as introducing a constraint on a single type, rather than a clause of arbitrarily many constraints)

view this post on Zulip Kevin Gillette (Oct 26 2022 at 05:38):

, feels to me like a more intuitive separator of traits which a type "implements" (since we're listing out the traits, and lists are naturally separated by a comma, at least in English).

& would seem appropriate to me if this were expressed not as traits possessed, but rather intrinsic properties... For example:

foo: a, b -> c
    where a is Eq & Hash & Decode
    where b is Eq & Hash

If we wanted to limit additional keywords, perhaps we could do something like:

foo: a, b -> c
    :: a -> Eq & Hash & Decode
    :: b -> Eq & Hash

:: could mean "sub-declaration", since it's not used for lists, and -> could arguably be consistent with the parameter declaration syntax if we take it to mean "evaluates to" (e.g. "foo evaluates to type c when applied, and b evaluates to a type which is both an Eq and a Hash when unified [during type unification]").

view this post on Zulip Anton (Oct 26 2022 at 06:30):

My personal favorite:

foo: a, b -> c
    where
        a implements Eq, Hash, Decode
        b implements Eq, Hash

view this post on Zulip Georges Boris (Oct 26 2022 at 10:34):

@Anton 's favorite looks super clean and easy to understand but is Roc white space sensitive in other places?

view this post on Zulip Luke Boswell (Oct 26 2022 at 10:56):

So more like this? Does it make any difference with the implementation there as well?

insert: Dict k v, k, v -> Dict k v
    where
        k implements Eq, Hash, Decode
        v implements Eq, Hash
insert = \dict, key, value ->
    # code here
    # etc

view this post on Zulip Brian Carroll (Oct 26 2022 at 11:24):

@Georges Boris Yes, Roc is whitespace sensitive in a small number of places, similar to Elm and Haskell. I think local variable definitions are an example.

view this post on Zulip Brian Carroll (Oct 26 2022 at 11:29):

I also prefer where. Sometimes I feel we are overdoing it with punctuation!

view this post on Zulip Folkert de Vries (Oct 26 2022 at 11:49):

where is also my favorite

I do wonder though if much is lost if we instead of

where
    k implements Eq + Hash + Decode,
    v implements Eq + Hash,

write

where
    k : Eq + Hash + Decode,
    v : Eq + Hash,

The : already means "has type", which is a correct intuition here I think (though it is of course different in a technical sense). the : key is actual punctuation, which should mean it's easy to reach (unlike say |, at least on my keyboard), and a good deal shorter than the implements word

view this post on Zulip Brian Carroll (Oct 26 2022 at 11:56):

Good idea

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:14):

Anton said:

My personal favorite:

foo: a, b -> c
    where
        a implements Eq, Hash, Decode
        b implements Eq, Hash

this idea doesn't necessarily require whitespace sensitivity, for what it's worth - since abilities don't have type parameters, Hash, Decode b implements already has enough information to tell where the break happens because b is lowercase, there wasn't a comma after Decode, and there was an implements after b

(although, having said that, I would want the formatter to immediately turn this into how Anton wrote it :laughing:)

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:16):

I'd say this one is my personal favorite too

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:17):

a concern I have about overloading : is that when you look at them in isolation, they look exactly like types - e.g. k : Hash is something you could write at the top level of any Roc program and have it mean something totally different than if you wrote it in the context of another type annotation :sweat_smile:

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:21):

one of the things I like about having a word instead of punctuation (e.g. has, supports, implements) for the token that appears before the abilities is that it reinforces that these are not about classification

view this post on Zulip Kristian Notari (Oct 26 2022 at 13:23):

Maybe it's the color-free syntax of the examples here but "implements" when reading fast seems another ability in the list, having some kind of symbol separating can help visually, even if it's not :

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:23):

e.g. I'm explicitly opposed to using is there even though it's concise, because it encourages treating abilities as classifications, which I think is a trap - I think it's way more fun than actually useful to spend a bunch of time thinking about how to classify different types, and one of my big concerns with adding abilities to the language in the first place is that a bunch of userspace classifications would pop up in the ecosystem

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:24):

I like that k implements Eq, Hash is very clear that "all this is saying is that k is responsible for implementing the logic for Eq and Hash"

view this post on Zulip Richard Feldman (Oct 26 2022 at 13:26):

it's syntactically about as far as you can get from "Dog and Cat both inherit from Animal" - which somehow sounds reasonable when you say it to a beginner (because it's logically true) - whereas like "Dog and Cat both implement Animal" makes me think "...what does that even mean?!" which is, I think, the correct response.


Last updated: Jun 16 2026 at 16:19 UTC