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:
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:
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?
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
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
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
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
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.
gotcha, maybe it would help to see what the TypeScript version looks like
to see what constraints you're going for :big_smile:
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],
]
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
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
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];
},
);
thanks! Does this TypeScript version capture all the constraints you're looking for? Or are there more constraints on top of that?
It does! Specifically it infers the strict types and only allows Transition
objects as input
I could still give it transition('Condense', 'Solid', 'Liquid')
though, right?
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.
hm, ok I'm missing a part of the motivation
so you have your matterStateMachine
which is defined by this array of transitions
matterStateMachine
is a constant which holds onto the data it was given and then can output a diagram
what else can matterStateMachine
do?
I think the part I'm missing is why it matters to be able to constrain that type
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:
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)
no worries! It's something that hasn't come up yet, so I'm eager to understand :smiley:
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
my guess is that the answer is no, because render would have to account for certain possible combinations that should never come up
Exactly. The above would still allow combinations I wouldn't want to account for
so is the goal here just to render? Or are there other things that would be done with the state machine
like would it be used in application code too, for example
Right, the goal is also to have an associated implementation, which would also leverage the inferred types.
Here's a smaller TypeScript example which might get us closer to the essence of what I'm after:
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');
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.
Also, I'm sort of treating string literal types in TypeScript as roughly equivalent to tags (without a payload) in Roc here
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
so this is less concise, but I think satisfies all the constraints (unless I'm missing some!)
stateMachine : tag, (tag, payload -> payload), (tag, payload -> Str) -> StateMachine
oh I guess render
would also need to be passed either the transition
function or else its return value - either would work
The conciseness could be solved with aliases, but indeed there is a constraint missing. I'm imagining what the transition
implementation would look like:
ah I see
it has to handle combinations that you wouldn't want to support
Exactly. Thanks for saving me the typing, haha
ok, what if the tag was inferred from the transition? e.g.
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
I'm not sure I'm following which tag is being inferred here
never mind, it's not a good design :laughing:
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
For example:
stateMachine : []transition -> ...
where transition is Tag {from: f, to: t}
Hmm, maybe that doesn't make any sense. Let me think about that
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?
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
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:
Thanks for the graphic! That helped me understand what you're trying to achieve.
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