Stream: ideas

Topic: Let-generalization - let's not?


view this post on Zulip Ayaz Hafiz (Dec 30 2022 at 20:17):

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!

view this post on Zulip Leander Behr (Dec 30 2022 at 22:37):

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:

view this post on Zulip Folkert de Vries (Dec 30 2022 at 22:46):

does it really come up often as a user? I think it's actually rare in most code

view this post on Zulip Folkert de Vries (Dec 30 2022 at 22:46):

what I mean is that this restriction is unlikely to be a problem for most roc programmers

view this post on Zulip Folkert de Vries (Dec 30 2022 at 22:48):

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

view this post on Zulip Leander Behr (Dec 30 2022 at 23:03):

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.

view this post on Zulip Folkert de Vries (Dec 30 2022 at 23:05):

yeah when this does bite you, understanding the problem and then fixing it is probably tricky

view this post on Zulip Richard Feldman (Dec 30 2022 at 23:44):

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:

view this post on Zulip Richard Feldman (Dec 30 2022 at 23:50):

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:

view this post on Zulip Richard Feldman (Dec 30 2022 at 23:52):

(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!)

view this post on Zulip Leander Behr (Dec 30 2022 at 23:55):

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.

view this post on Zulip Richard Feldman (Dec 30 2022 at 23:57):

true, but I think that's most likely going to be fine

view this post on Zulip Richard Feldman (Dec 31 2022 at 00:02):

my experience from seeing how rare and unintuitive type edge cases like this play out in Elm is:

view this post on Zulip Leander Behr (Dec 31 2022 at 00:06):

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.

view this post on Zulip Richard Feldman (Dec 31 2022 at 00:08):

I think that's the "proposal without extension" version in this document

view this post on Zulip Folkert de Vries (Dec 31 2022 at 00:08):

haskell also allows polymorphic recursion, so it has a type system that is strictly more powerful than what we have even today

view this post on Zulip Leander Behr (Dec 31 2022 at 00:12):

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

view this post on Zulip Richard Feldman (Dec 31 2022 at 00:12):

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

view this post on Zulip Richard Feldman (Dec 31 2022 at 00:13):

because you can always relax the restriction later without breaking anyone's code, whereas the reverse is not true

view this post on Zulip Leander Behr (Dec 31 2022 at 00:14):

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.

view this post on Zulip Richard Feldman (Dec 31 2022 at 00:14):

agreed!

view this post on Zulip Ayaz Hafiz (Dec 31 2022 at 00:21):

The proposal with extensions includes everything from the baseline too, so number literals would also be allowed as polymorphic

view this post on Zulip Ayaz Hafiz (Dec 31 2022 at 00:23):

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)?

view this post on Zulip Leander Behr (Dec 31 2022 at 00:26):

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.

view this post on Zulip Leander Behr (Dec 31 2022 at 00:28):

Because the literal 0 can't be polymorphic then, and so get_index isn't polymorphic. I think.

view this post on Zulip Leander Behr (Dec 31 2022 at 00:30):

So "polymorphism works for all values of function type and nothing else" looks pretty decent with the current set of examples

view this post on Zulip Leander Behr (Dec 31 2022 at 00:37):

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.

view this post on Zulip Ayaz Hafiz (Dec 31 2022 at 00:40):

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.

view this post on Zulip Ayaz Hafiz (Dec 31 2022 at 00:42):

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.

view this post on Zulip Leander Behr (Dec 31 2022 at 00:52):

Makes sense. I guess in my mind getIndex had type {} -> weak.


Last updated: Jun 16 2026 at 16:19 UTC