Stream: beginners

Topic: Unknown tag, known payload


view this post on Zulip Tanner Nielsen (Dec 21 2022 at 18:58):

Questions

I've been experimenting with Roc for the last several days, searching Zulip & GitHub, but I haven't found the answer to the following related questions:

  1. Is it possible to access a Tag's payload without knowing the non-payload portion of the tag?
  2. Is it possible to constrain a type such that is only accepts tags with a particular payload shape?

Motivating use case

I'm working on some generic functions that help model state machines and generate diagrams from them (similar to XState from the TypeScript world). I'd like the input that defines the state machine to be a list of tags, representing every possible state transition. For example:

matterStateMachine = stateMachine [
    Melt Solid Liquid,
    Evaporate Liquid Gas,
    Condense Gas Liquid,
    Freeze Liquid Solid,
]

This is a really concise way to simultaneously infer the desired types _and_ list every possible transition, a common technique in TypeScript. I can do a lot with this information alone:

  1. With every possible transition, I can infer every possible state
  2. With every possible transition, I can render a state diagram (e.g., as mermaid or even XState)
  3. With the inferred types, I can enforce that every state & transition is accounted for when rendering the diagram (converting state & transition tags to strings)
  4. With the inferred types, I can enforce that every state transition has an implementation

The problem is that I currently have no way to enforce the shape of the tag payloads. And even if I knew how to do that, I don't know how I could access these tag payloads without knowing the specific tags ahead of time (which won't work for a generic stateMachine function).

Any thoughts or advice on this issue?

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:10):

so if you want to enforce a shape, I'd use a record instead:

matterStateMachine = stateMachine [
    { name: Melt, from: Solid, to: Liquid },
    { name: Evaporate, from: Liquid, to: Gas },
    { name: Condense, from: Gas, to: Liquid },
    { name: Freeze, from: Liquid, to: Solid },
]

here the type of stateMachine would be:

stateMachine : { name : *, from : *, to : * } -> StateMachine

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:11):

we don't have this fully implemented yet, but in the future you could more concisely do it with a tuple:

matterStateMachine = stateMachine [
    (Melt, Solid, Liquid),
    (Evaporate, Liquid, Gas),
    (Condense, Gas, Liquid),
    (Freeze, Liquid, Solid),
]

here the type of stateMachine would be:

stateMachine : (*, *, *) -> StateMachine

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:14):

however, currently you'd have a tough time drawing diagrams from this because there's currently no way to convert a tag to a string

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:14):

we have a planned Str.inspect : * -> Str that would convert (for eaxmple) Foo to the string "Foo", which sounds like a reasonable fit for this use case

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:15):

Thanks for the fast reply @Richard Feldman ! Unfortunately I don't think this would infer the types that I need. Using the example with records, the inferred type would be

List {
    name : [Condense, Evaporate, Freeze, Melt],
    from : [Gas, Liquid, Solid],
    to : [Gas, Liquid, Solid],
}

This would allow for things like {name: Condense, from: Solid, to: Liquid}, which isn't desired.

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:16):

gotcha, maybe it would help to see what the TypeScript version looks like

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:16):

to see what constraints you're going for :big_smile:

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:17):

This is why I wanted to use a tag union as input, because the type that is inferred is very strict:

[
    Melt [Solid] [Liquid],
    Evaporate [Liquid] [Solid],
    Condense [Gas] [Liquid],
    Freeze [Liquid] [Solid],
]

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:18):

I can whip up something in TypeScript if that would help! Just doesn't map perfectly because tagged unions have to be emulated in TypeScript via untagged unions

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:20):

I am also aware of the Tag to Str issue :smile: For now I plan on just requiring the user to provide a function that maps the tags to strings, which might be helpful anyway if the diagram needs to use different text than just the tag name

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:29):

Here's a TypeScript translation (Playground link):

type Tag<Name extends string, Payload extends unknown> =
    [Name, Payload];

type Transition<Name extends string, From extends string, To extends string> =
    Tag<Name, [From, To]>;

function transition<Name extends string, From extends string, To extends string>(
    name: Name,
    from_: From,
    to: To,
): Transition<Name, From, To> {
    return [name, [from_, to]];
}

function stateMachine<Transitions extends Array<Transition<string, string, string>>>(
    transitions: Transitions,
    render: (transition: Transitions[number]) => string,
) {
    // ...
}

const matterStateMachine = stateMachine(
    [
        transition('Melt', 'Solid', 'Liquid'),
        transition('Evaporate', 'Liquid', 'Gas'),
        transition('Condense', 'Gas', 'Liquid'),
        transition('Freeze', 'Liquid', 'Solid'),
    ],
    transition => {
        // if you ctrl/cmd hover over `transition`, strict types are inferred
        // allowing every case to be accounted for
        return transition[0];
    },
);

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:38):

thanks! Does this TypeScript version capture all the constraints you're looking for? Or are there more constraints on top of that?

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:40):

It does! Specifically it infers the strict types and only allows Transition objects as input

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:42):

I could still give it transition('Condense', 'Solid', 'Liquid') though, right?

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:44):

You could if that's what the state machine was allowed to do, but that first input is essentially the definition of what's possible in the state machine. The render function (in the TypeScript example) would infer no such combination from the input shown above.

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:46):

hm, ok I'm missing a part of the motivation

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:47):

so you have your matterStateMachine which is defined by this array of transitions

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:47):

matterStateMachine is a constant which holds onto the data it was given and then can output a diagram

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:47):

what else can matterStateMachine do?

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:48):

I think the part I'm missing is why it matters to be able to constrain that type

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:48):

like in this example the array is created once, given a function that turns it into a string, and then never used again - so I'm wondering how else it might be used where the constraint might actually come up :big_smile:

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 19:52):

The render function is an example where the type inferred from the array of transitions is used to ensure that every case is covered (without needing to cover cases that aren't actually possible). The same could be done for implementing the actual transition function. I think I might be muddying the waters with an overly complex example... I'll try to boil it down better in a moment (thank you for your patience)

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:54):

no worries! It's something that hasn't come up yet, so I'm eager to understand :smiley:

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:57):

would this constrain the render function enough?

stateMachine : List { name, from, to }, ({ name, from, to } -> Str) -> StateMachine

(this is syntax sugar for the following)

stateMachine : List { name : n, from : f, to : t }, ({ name : n, from : f, to:  t } -> Str) -> StateMachine

view this post on Zulip Richard Feldman (Dec 21 2022 at 19:58):

my guess is that the answer is no, because render would have to account for certain possible combinations that should never come up

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:01):

Exactly. The above would still allow combinations I wouldn't want to account for

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:01):

so is the goal here just to render? Or are there other things that would be done with the state machine

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:01):

like would it be used in application code too, for example

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:02):

Right, the goal is also to have an associated implementation, which would also leverage the inferred types.

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:03):

Here's a smaller TypeScript example which might get us closer to the essence of what I'm after:

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:03):

function makeUnion<T extends string>(...options: Array<T>) {
    return options;
}

// Inferred type of `states` is `Array<'Solid' | 'Liquid' | 'Gas'>`.
// The important thing here is that I'm able to infer a very strict type
// but still constrain the input (e.g., I can't pass numbers or anything else).
// I can accomplish similar inference super powers in Roc, but I can't ALSO
// constrain the input.
const states = makeUnion('Solid', 'Liquid', 'Gas');

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:05):

The pattern here allows me to concisely define all the possibilities so they can be iterated over at runtime AND infer the strictest possible typing.

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:07):

Also, I'm sort of treating string literal types in TypeScript as roughly equivalent to tags (without a payload) in Roc here

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:13):

cool, so one idea (that has other downsides) is:

matterStateMachine = stateMachine [Melt, Evaporate, Condense, Freeze] transition render

transition : [Melt, Evaporate, Condense, Freeze], [Solid, Liquid, Gas] -> [Solid, Liquid, Gas]

render : [Melt, Evaporate, Condense, Freeze], [Solid, Liquid, Gas] -> Str

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:13):

so this is less concise, but I think satisfies all the constraints (unless I'm missing some!)

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:14):

stateMachine : tag, (tag, payload -> payload), (tag, payload -> Str) -> StateMachine

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:15):

oh I guess render would also need to be passed either the transition function or else its return value - either would work

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:15):

The conciseness could be solved with aliases, but indeed there is a constraint missing. I'm imagining what the transition implementation would look like:

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:16):

ah I see

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:16):

it has to handle combinations that you wouldn't want to support

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:16):

Exactly. Thanks for saving me the typing, haha

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:17):

ok, what if the tag was inferred from the transition? e.g.

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:17):

transition : [Solid, Liquid, Gas], [Solid, Liquid, Gas] -> [Melt, Evaporate, Condense, Freeze]

I guess that has the problem of having to specify e.g. how Solid, Solid should be labeled

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:21):

I'm not sure I'm following which tag is being inferred here

view this post on Zulip Richard Feldman (Dec 21 2022 at 20:22):

never mind, it's not a good design :laughing:

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:24):

I should clarify that I would be perfectly happy to lose inferring the tag union type from a list of possibilities if I had a way to constrain the input

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:24):

For example:

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:25):

stateMachine : []transition -> ...
   where transition is Tag {from: f, to: t}

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 20:32):

Hmm, maybe that doesn't make any sense. Let me think about that

view this post on Zulip Johannes Maas (Dec 21 2022 at 22:42):

I hope I'm not derailing things. This exchange is quite intriguing. @Tanner Nielsen, would you mind sharing the output of the TypeScript example you gave above? I'm guessing it's a visual graph?

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 23:04):

Thanks for the curiosity @Johannes Maas :smile: The TypeScript code I shared above was just to demonstrate the type inference I'm looking for, so it doesn't actually do anything... but yes -- the idea is that there would be another function that takes one of these state machines and produces a diagram, probably in mermaid and/or xstate format, for example: states-of-matter.png Not sure that's what you were looking for

view this post on Zulip Tanner Nielsen (Dec 21 2022 at 23:06):

That said, I've since come up with a better Roc implementation for what I want to. I'll share the prototype code later this evening. If it works well for what I want to use it for, I may make it a public library as well :smile:

view this post on Zulip Johannes Maas (Dec 21 2022 at 23:15):

Thanks for the graphic! That helped me understand what you're trying to achieve.

view this post on Zulip Johannes Maas (Dec 21 2022 at 23:17):

I'm getting the feeling that there is a "conflict" between type land and value land. It really is tricky to define what transitions are valid as values (that you can put into a function) and have that reflected in types (which ensure that other functions don't use invalid transitions). I hope this makes sense. :sweat_smile:


Last updated: Jul 06 2025 at 12:14 UTC