Stream: beginners

Topic: Tag scope


view this post on Zulip Anne Archibald (Feb 04 2024 at 09:44):

What is the scope of union tags?

If I create a tag union with a tag Gear in one module (function?), and another with Gear in another module, are they the same tag? Do they have to take the same payload? If not, how do I specify whether it's the Gear from Mechanical (with an iteger number of teeth payload)or the Gear from Camping (with a string description payload)? It doesn't seem like roc accepts Mechanical.Gear as a valid way to specify a tag.

view this post on Zulip Luke Boswell (Feb 04 2024 at 10:07):

I imagine they would be different unions and not unify even if they have the same name, because they have different payloads.

view this post on Zulip Luke Boswell (Feb 04 2024 at 10:08):

Should be easy enough to test. But I guess your looking for a more formal answer, which I don't really know much about.

view this post on Zulip Richard Feldman (Feb 04 2024 at 12:12):

tag unions work the same way as records: it doesn't matter where they're defined, what matters are the names of the tags and that the payloads are type-compatible

view this post on Zulip Anne Archibald (Feb 04 2024 at 14:22):

Richard Feldman said:

tag unions work the same way as records: it doesn't matter where they're defined, what matters are the names of the tags and that the payloads are type-compatible

Does this mean that roc can't infer types from the arguments to tags?

Specifically, if I use (Gear 7) somewhere, can roc object to me using (Gear "seven") elsewhere? (Or for a more likely error, Gear 7 "seven" and Gear "seven" 7?)

I mean, on the one hand when I use pattern matching on Gear I'd like roc to be able to infer the types of the arguments. On the other hand I would rather my program not suddenly explode because I imported a module that uses Gear with different types internally. How is this resolved?

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:25):

Anne Archibald said:

if I use (Gear 7) somewhere, can roc object to me using (Gear "seven") elsewhere?

nope, that's fine

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:25):

the type inference is based on the local to the usage site, also like records

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:25):

just like how record field names are arbitrary strings that are in a shared global namespace, so are tag names

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:26):

and just like how you can have a record with { email : Str } in one place and { email : Email } in another place, no problem, you can also have a tag union with [Email Str] and [Email Email] somewhere else, no problem

view this post on Zulip Anne Archibald (Feb 04 2024 at 14:26):

I guess what I'm asking is if I write

when thing is
    Gear a b -> ...

what, if any, constraints does that place on the types of a and b?

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:27):

just based on that code alone? none whatsoever

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:28):

like you can write:

\thing ->
    when thing is
        Gear a b -> ...

and it will infer the type of a and b based on how they get used in the ...

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:28):

and if they just get returned without anything else happening to them that would constrain their types, then their inferred types will both be type variables (probably also named a and b, since those are the first two auto-generated type variable names the compiler will choose), and if they don't even get returned, then their inferred types will be *

view this post on Zulip Anne Archibald (Feb 04 2024 at 14:34):

I guess I was thinking of Gear in this context as being like a constructor, which would constrain the types of its input. I have been making stream objects with Stream nextFunction initialState and assuming this would constrain the types of its arguments. Should I create a function makeStream = \nextFunction, initialState -> Stream nextFunction initialState so that I can provide this restriction?

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:35):

Anne Archibald said:

I guess I was thinking of Gear in this context as being like a constructor, which would constrain the types of its input.

ah, yeah so tags don't do that, they just tag data with a name :big_smile:

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:35):

like records, they are intentionally just about labeling and that's it

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:36):

if you want to constrain the types, then yeah making a function like that is a good start, but the best way to enforce that is with an opaque wrapper

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:36):

https://www.roc-lang.org/tutorial#opaque-types

view this post on Zulip Richard Feldman (Feb 04 2024 at 14:38):

so something like:

\nextFunction, initialState -> @MyOpaqueTypeName (Stream nextFunction initialState)

view this post on Zulip Anne Archibald (Feb 04 2024 at 14:40):

I experimented with opaque types (of necessity) when I thought an Ability would solve my polymorphism problem. In this case, though, I don't want to restrict creation and inspection of these objects to code in the Stream module - people elsewhere should also be able to write efficient Stream operations. I guess I can make an opaque type and then provide accessors that let people get at all its gubbins?

view this post on Zulip Richard Feldman (Feb 04 2024 at 15:01):

you can, although if you give access to every internal detail then every change to internal structure becomes a breaking change, so I generally default to exposing as little as seems necessary :big_smile:

view this post on Zulip Anne Archibald (Feb 04 2024 at 16:43):

Richard Feldman said:

you can, although if you give access to every internal detail then every change to internal structure becomes a breaking change, so I generally default to exposing as little as seems necessary :big_smile:

This brings me to my polymorphism problem :-p even using Ability streams with different internal state are not compatible unless I wrap them in a callable. And I'm not at all clear whether the optimisations my stream code is meant to allow can work with a callable wrapper in the way. (A chain of stream operations should be inline-able down to a single function that steps through the stream elements, with no recursion/looping anywhere inside.) Anyway, that's another thread. Thanks for clarifying how tags work!

view this post on Zulip Richard Feldman (Feb 04 2024 at 16:59):

sure, happy to clarify! :smiley:

view this post on Zulip Anne Archibald (Feb 05 2024 at 18:39):

I just checked, and you probably said so but:

While you can have all the tags "Gear", "Gear 7", and "Gear 7 8", they cannot exist in the same union type.

If I understand correctly, normally, if you have two union types, their, uh union is also a valid type. That is not true if any of their tags have the same spelling but different "signatures".

I guess, though, that one doesn't normally take the union of union types defined in different places. The exception would seem to be error return values - if one package defines HttpFailure to take an error code argument and another defines it without, that might give confusing problems using Result?

view this post on Zulip Brendan Hansknecht (Feb 05 2024 at 18:50):

Yeah, you would have to add a mapping or wrap tags or something to deal with the discrepancy if using two different libraries with the same error tag name but different contained data.

view this post on Zulip Brendan Hansknecht (Feb 05 2024 at 18:52):

It's really trivial to just wrap by library if you need to. someTask |> Task.mapErr LibraryWrapperTag

view this post on Zulip Brendan Hansknecht (Feb 05 2024 at 18:53):

I think long term it should be possible to make really clear errors for this case. Print the source code of both definitions, note they are incompatible, and link to a page that shows common solutions like wrapping and mapping.


Last updated: Jul 06 2025 at 12:14 UTC