It turns out the scope of polymorphism Roc supports today can result in unexpected runtime characteristics, like duplicate work being performed, and more dbg/expects being printed than you might expect!
Here is a proposal to limit the scope of polymorphism in Roc, so that only assignments that are syntactically functions (e.g. x = \y -> y) or number literals (e.g. n = 1) can be used polymorphically: https://rwx.notion.site/Let-generalization-Let-s-not-742a3ab23ff742619129dcc848a271cf
The section "Problems" gives concrete examples of the runtime problems mentioned above.
Any comments and suggestions would be greatly appreciate. I know the document is pretty long, so thanks for any time you spend reading it!
newb to roc and not an expert on fp in general, so this is to understand and clarify.
Is this correct?
Notes from my first read:
does it really come up often as a user? I think it's actually rare in most code
what I mean is that this restriction is unlikely to be a problem for most roc programmers
from an API perspective it is a bit unfortunate that we cannot have a generic definition of pi, but I guess using F64 for it is fine and for F32 it can be safely downcast
I just played a bit in another context and I think both the unrestricted and restricted rules are very intuitive to use. But I still think that the actual rule set of the restricted version is significantly more complicated. It's basically polymorphic always vs polymorphic sometimes right? And the sometimes is what you need to learn to fully understand the rules.
yeah when this does bite you, understanding the problem and then fixing it is probably tricky
Folkert de Vries said:
from an API perspective it is a bit unfortunate that we cannot have a generic definition of
pi
I may have misunderstood, but is that true? I thought that would work, since:
Num * to Frac * (which I believe is also still allowed, unless I missed something)Folkert de Vries said:
when this does bite you, understanding the problem and then fixing it is probably tricky
again, I could be wrong, but it seems like fixing it should be consistently easy: wrap the thing you want to be polymorphic in a thunk and call it wherever you want to use it :big_smile:
(granted, it may not be obvious that this is what you ought to do, but if - for example - a compiler error message could tell you to do that, it seems very easy to apply that fix!)
That's easy if you know that that's the fix. To know that it is and why it is you need to basically understand this discussion here. Just being told by the compiler what to do is fine but would leave me personally unsatisfied. Without knowing the actual rules it would be hard to predict when you need to use the workaround I think.
It's not super complicated but it's an extra thing to learn.
true, but I think that's most likely going to be fine
my experience from seeing how rare and unintuitive type edge cases like this play out in Elm is:
The next question would then be what the restrictions should be exactly. The proposal gives two options. From quick testing in Haskell without looking it up properly it appears to only allow polymorphism for functions but that includes all function values. So that would be a third option.
I think that's the "proposal without extension" version in this document
haskell also allows polymorphic recursion, so it has a type system that is strictly more powerful than what we have even today
Richard Feldman said:
I think that's the "proposal without extension" version in this document
It doesn't have a special case for definitions from number literals
as a general design rule, when there are significant benefits and it's unclear how much the drawbacks will come up in practice, the best default choice is to try out the most restrictive option and see how it goes in practice
because you can always relax the restriction later without breaking anyone's code, whereas the reverse is not true
I think doing it for functions only but for all functions is the simplest and most intuitive rule. That doesn't necessarily mean it's the best choice though.
agreed!
The proposal with extensions includes everything from the baseline too, so number literals would also be allowed as polymorphic
By “for all functions” do you mean any value whose type is a function, or the version where only values that look like functions syntacticallyare allowed (as in the extension)?
By “for all functions” I mean any value whose type is a function. And I think (I may be very wrong, I'm just dabbeling in haskell playground) this is the way haskell does it.
And actually, if it's done this way but doesn't have a carve-out for number literals, then the motivating example for the extension also does not compile without needing to restrict polymorphism to only functions that look like functions syntactically.
Because the literal 0 can't be polymorphic then, and so get_index isn't polymorphic. I think.
So "polymorphism works for all values of function type and nothing else" looks pretty decent with the current set of examples
I'm having a really hard time coming up with an example that causes expensive work duplication if nothing can be polymorphic but functions. But maybe I'm just not creative enough.
Maybe I'm missing something, but the literal 0 can be polymorphic without needing an explicit carve-out for number literals (although there is a carve out for number literals) because it will become polymorphic after the function getIndex is generalized. The inferred type of getIndex will always be {} -> Num *, where Num * is "this is any kind of number" and has polymorphic behavior, with a concrete instance each place getIndex is used.
The motivating example for the extension is an instance where duplicate work can be performed, since today, getIndex would be compiled to two different concrete types (getIndexI64 and getIndexNat) as shown in the example below. So the idx = List.walkUntil ... calculation would happen twice, when you really want it to happen once.
Makes sense. I guess in my mind getIndex had type {} -> weak.
Last updated: Jun 16 2026 at 16:19 UTC