Stream: bugs

Topic: ✔ Can't rename a polymorphic function?


view this post on Zulip jan kili (Jan 20 2025 at 16:19):

I expect this to work, but it seems to be bugged. Is this actually intended behavior?

app [main!] { basic_cli: platform "./platform/main.roc" }

import basic_cli.Stdout

main! = \_ ->
    foo = mappy [1, 2, 3, 4] Num.toStr
    bar = mappy [" a", "b "] Str.trim
    foo |> Str.joinWith "&" |> try Stdout.line!
    bar |> Str.joinWith "&" |> try Stdout.line!
    Stdout.line! "success"

mappy = rename List.map
rename = \anything -> anything
...
But mappy needs its 1st argument to be:

    List (Num *)
...
But mappy needs its 2nd argument to be:

    Num * -> Str
...

Screenshot_20250120_092613.png

Edit: No-op refactor to simplify.

view this post on Zulip jan kili (Jan 20 2025 at 16:29):

Specifying rename : anything -> anything doesn't fix it.

view this post on Zulip jan kili (Jan 20 2025 at 16:30):

Specifying mappy : List a, (a -> b) -> List b yields

TYPE MISMATCH

This 2nd argument to `mappy` has an unexpected type:

6│      foo = mappy [1, 2, 3, 4] Num.toStr
                                 ^^^^^^^^^

This `toStr` value is a:

    Num * -> Str

But `mappy` needs its 2nd argument to be:

    a -> b

Tip: The type annotation uses the type variable `b` to say
that this definition can produce any type of value. But in
the body I see that it will only produce a `Str` value of a
single specific type. Maybe change the type annotation to be
more specific? Maybe change the code to be more general?

view this post on Zulip jan kili (Jan 20 2025 at 16:34):

I see that it will only produce a `Str` value :point_left: that definitely seems like a bug

view this post on Zulip jan kili (Jan 20 2025 at 16:39):

If I "run the program anyway with roc run" it does crash with

Running program anyway…

────────────────────────────────────────────────────────────────────────────────
Roc crashed with:

        Erroneous: specialize_symbol

Here is the call stack that led to the crash:

        .libc

Optimizations can make this list inaccurate! If it looks wrong, try running without `--optimize` and with `--linker=legacy`

while it doesn't crash if I first comment out either the foo lines or the bar lines.

view this post on Zulip Anton (Jan 20 2025 at 16:42):

@Ayaz Hafiz probably knows what's going on

view this post on Zulip Notification Bot (Jan 20 2025 at 16:51):

This topic was moved here from #beginners > Can't rename a polymorphic function? by JanCVanB.

view this post on Zulip jan kili (Jan 20 2025 at 16:53):

(upgraded to bug, for at least the I see that it will only produce a `Str` value problem)

view this post on Zulip jan kili (Jan 20 2025 at 16:56):

Not urgent, but looking forward to what @Ayaz Hafiz (or another polymorphism specialist) thinks should be intended behavior here!

view this post on Zulip Brendan Hansknecht (Jan 20 2025 at 17:50):

I'm pretty sure it should just work. It also should just work without the rename function just mappy = List.msp

view this post on Zulip jan kili (Jan 20 2025 at 18:42):

Glad to hear! Yeah mappy = List.map doesn't work either, which seems like a better starting point for debugging. (I do want the two-step version too.)

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:16):

yes, this is intended behavior

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:17):

mappy : List a, (a -> b) -> List b should yield an error, i just landed a PR to generate that error

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:17):

you need to wrap mappy in an explicit lambda for it to be polymorphic

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:17):

eg mappy = \l, f -> List.map l f

view this post on Zulip Brendan Hansknecht (Jan 20 2025 at 19:18):

This is due to let generalization? Didn't realize that affected lambdas

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:18):

yes

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:19):

everything that's not explicitly a function (syntactically) is not polymorphic. Everything that is not explicitly a function gets at most one type, and that type is determined by the first usage

view this post on Zulip Richard Feldman (Jan 20 2025 at 19:22):

wasn't this one of the situations where we could relax that if desired?

view this post on Zulip Brendan Hansknecht (Jan 20 2025 at 19:28):

Ayaz Hafiz said:

everything that's not explicitly a function (syntactically) is not polymorphic. Everything that is not explicitly a function gets at most one type, and that type is determined by the first usage

Ah, didn't realize it had to be syntactically.

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:32):

it can be relaxed if you perform eta-expansion

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:32):

i.e. write any value v of type a -> b to \a -> (v) a

view this post on Zulip jan kili (Jan 20 2025 at 19:32):

Does that mean that rename = \anything -> anything is impossible?

view this post on Zulip Ayaz Hafiz (Jan 20 2025 at 19:35):

yeah rename List.map doesn't work on its own if you want it to be polymorphic it needs to be \l, f -> (rename List.map) l f

view this post on Zulip jan kili (Jan 20 2025 at 19:37):

:sad:

view this post on Zulip jan kili (Jan 20 2025 at 19:38):

Is this due to Roc's design or the compiler's implementation?

view this post on Zulip Richard Feldman (Jan 20 2025 at 19:39):

Ayaz Hafiz said:

it can be relaxed if you perform eta-expansion
i.e. write any value v of type a -> b to \a -> (v) a

I think we should do this

view this post on Zulip Richard Feldman (Jan 20 2025 at 19:40):

it's surprising that it doesn't work, and then the workaround is to do eta-expansion manually, so seems like a good thing for the compiler to do for you automatically

view this post on Zulip Richard Feldman (Jan 20 2025 at 19:44):

to clarify: this is just when naming it in defs, right?

view this post on Zulip Richard Feldman (Jan 20 2025 at 19:44):

like when a function is being passed in as an argument, that's not necessary

view this post on Zulip jan kili (Jan 21 2025 at 22:01):

It sounds like this wasn't/isn't a bug. Shall I move the last few messages to a new #ideas topic?

view this post on Zulip jan kili (Jan 22 2025 at 19:46):

Friendly ping :) I'm iterating on a script with the equivalent of mappy1 & mappy2 to work around this, but it's a low priority for me.

view this post on Zulip jan kili (Jan 22 2025 at 19:51):

Richard Feldman said:

to clarify: this is just when naming it in defs, right?
like when a function is being passed in as an argument, that's not necessary

In case this answers your question, this gets the same type checking errors with Monday's nightly release:

app [main!] { basic_cli: platform "./platform/main.roc" }

import basic_cli.Stdout

main! = \_ ->
    List.map |> do_it |> Stdout.line!

do_it = \some_function ->
    foo = some_function [1, 2, 3, 4] Num.toStr |> Str.joinWith "&"
    bar = some_function [" a", "b "] Str.trim |> Str.joinWith "&"
    Str.concat foo bar

like

But `some_function` needs its 1st argument to be:

    List (Num *)

view this post on Zulip jan kili (Jan 22 2025 at 19:55):

Specifying

do_it : (List a, (a -> b) -> List b) -> Str

leads to a similar secondary message:

TYPE MISMATCH

This 1st argument to |> has an unexpected type:

10│      foo = some_function [1, 2, 3, 4] Num.toStr |> Str.joinWith "&"
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This `some_function` call produces:

    List b

But |> needs its 1st argument to be:

    List Str

Tip: The type annotation uses the type variable `b` to say
that this definition can produce any type of value. But in
the body I see that it will only produce a `Str` value of a
single specific type. Maybe change the type annotation to be
more specific? Maybe change the code to be more general?

but maybe the next nightly will include an improvement from Ayaz's PR?

view this post on Zulip Sam Mohr (Jan 22 2025 at 19:57):

I don't think his last PR will have fixed this

view this post on Zulip Sam Mohr (Jan 22 2025 at 19:57):

That PR was warning devs when they implied code was generic with a type signature that actually only operated on a single, specific type

view this post on Zulip Sam Mohr (Jan 22 2025 at 19:58):

The eta-expansion would need to be implemented separately

view this post on Zulip jan kili (Jan 22 2025 at 20:01):

Sam Mohr said:

I don't think his last PR will have fixed this

Yeah, confirmed with today's TESTING release and basic-cli@main - no change.

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 00:44):

>

do_it = \some_function ->
    foo = some_function [1, 2, 3, 4] Num.toStr |> Str.joinWith "&"
    bar = some_function [" a", "b "] Str.trim |> Str.joinWith "&"
    Str.concat foo bar

This case is separate to the previous examples - this requires a type system feature called "higher ranked types". The name is confusing but it basically means that some_function has to itself be polymorphic, and not be of a specific type. Normally, if I had a function

do_it = \some_function, l, f -> some_function l f

do_it List.map [] (\x -> x + 1)

some_function gets the concrete type List I64, (I64 -> I64) -> List I64 when it is called. But in the case you showed, some_function needs to always be polymorphic - I cannot call it with a I64 -> I64, I need to call it with something that's always an a -> a. This isn't supported in Roc and I would be surprised if it is added

view this post on Zulip jan kili (Jan 23 2025 at 00:50):

Ah, okay. Does that mean that type variables are only supported within function parameters that aren't functions themselves?
as in, do_it : (List a, (a -> b) -> List b) -> Str is unsupported
or here's a simpler one ~ do_it : (a -> Str) -> Str

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 00:56):

No, that works, but the type variable becomes a concrete type when the outer function (do_it) is called, not when the inner function is called

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 00:57):

another way to think about it is like

do_it = \some_function, throwaway_arg ->
  dbg throwaway_arg
    foo = some_function [1, 2, 3, 4] Num.toStr |> Str.joinWith "&"
    bar = some_function [" a", "b "] Str.trim |> Str.joinWith "&"
    Str.concat foo bar

throwaway_arg can be a, but some_function needs to be something like (forall b : List b, (b -> Str) -> List Str)

view this post on Zulip jan kili (Jan 23 2025 at 00:57):

Side note - I now see that this doesn't work either:

do_it : { foo : a } -> Str
do_it = \{ foo } ->
    Inspect.toStr foo
TYPE MISMATCH

This 1st argument to `toStr` has an unexpected type:

37│      Inspect.toStr foo
                       ^^^

This `foo` value is a:

    a

But `toStr` needs its 1st argument to be:

    val where val implements Inspect

Tip: The type annotation uses the type variable `a` to say
that this definition can produce any type of value. But in
the body I see that it will only produce an instance of the
ability `Inspect` of a single specific type. Maybe change the
type annotation to be more specific? Maybe change the code
to be more general?

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 00:58):

The tip at the bottom is not good but you need to say that a implements Inpsect. This should work

do_it : { foo : a } -> Str where a implement Inspect
do_it = \{ foo } ->
    Inspect.toStr foo

view this post on Zulip jan kili (Jan 23 2025 at 01:03):

Ayaz Hafiz said:

throwaway_arg can be a, but some_function needs to be something like (forall b : List b, (b -> Str) -> List Str)

@Ayaz Hafiz Am I doing it wrong?
Screenshot_20250122_180239.png

view this post on Zulip jan kili (Jan 23 2025 at 01:05):

Regardless, it sounds like I should start an #ideas topic for "Automatic eta expansion" based on Richard's suggestion above.

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 01:09):

well, i was saying roc doesn’t support that

view this post on Zulip jan kili (Jan 23 2025 at 01:12):

Ayaz Hafiz said:

No, that works, but the type variable becomes a concrete type when the outer function (do_it) is called, not when the inner function is called

In practice, type vars don't seem to work within parameters that are functions:

app [main!] { basic_cli: platform "./platform/main.roc" }

import basic_cli.Stdout

main! = \_ ->
    f Inspect.toStr
    |> Str.concat (f Num.toStr)
    |> Stdout.line!

f : (a -> Str) -> Str
f = \g ->
    g 10
TYPE MISMATCH

This 1st argument to `g` has an unexpected type:

12│      g 10
           ^^

The argument is a number of type:

    Num *

But `g` needs its 1st argument to be:

    a

Tip: The type annotation uses the type variable `a` to say
that this definition can produce any type of value. But in
the body I see that it will only produce a `Num` value of a
single specific type. Maybe change the type annotation to be
more specific? Maybe change the code to be more general?

view this post on Zulip jan kili (Jan 23 2025 at 01:13):

Which is understandable if it's unsupported, I'm just seeking what the support boundary means in practice.

view this post on Zulip jan kili (Jan 23 2025 at 01:14):

NEVERMIND, I'm wrong - fix coming

view this post on Zulip jan kili (Jan 23 2025 at 01:15):

This works just fine:

app [main!] { basic_cli: platform "./platform/main.roc" }

import basic_cli.Stdout

main! = \_ ->
    f Inspect.toStr 10
    |> Str.concat (f Num.toStr 10)
    |> Stdout.line!

f : (a -> Str), a -> Str
f = \g, x ->
    g x

view this post on Zulip jan kili (Jan 23 2025 at 01:16):

So the unsupported thing here seems to just be over-generalizing a function signature, which I don't mind.

view this post on Zulip jan kili (Jan 23 2025 at 01:22):

Regarding Richard's suggestion of automatic eta expansion, I'm unsure how to interpret your replies. My optimistic reading is that you haven't addressed it yet, but my pessimistic reading sees this sequence:

Ayaz Hafiz said:

... This isn't supported in Roc and I would be surprised if it is added

JanCVanB said:

...it sounds like I should start an #ideas topic for "Automatic eta expansion" based on Richard's suggestion above.

Ayaz Hafiz said:

well, i was saying roc doesn’t support that

view this post on Zulip jan kili (Jan 23 2025 at 01:26):

I'm specifically interested in the "when naming it in defs" case:

mappy = rename List.map
rename = \anything -> anything

view this post on Zulip Ayaz Hafiz (Jan 23 2025 at 02:06):

those conversations are interleaved about different things… in the messages quoted from me i was referring to higher rank types, richard was referring to eta expansion. i dont see an issue with eta expansion

view this post on Zulip jan kili (Jan 23 2025 at 23:57):

The clearest "bug(s)" here seem to just be unsatisfying error messages, but those are improving daily. I'm resolving this for now, with discussion hopefully continuing here: #ideas > automatic eta expansion, to rename polymorphic f'ns

view this post on Zulip Notification Bot (Jan 23 2025 at 23:57):

JanCVanB has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC