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.
Specifying rename : anything -> anything
doesn't fix it.
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?
I see that it will only produce a `Str` value
:point_left: that definitely seems like a bug
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.
@Ayaz Hafiz probably knows what's going on
This topic was moved here from #beginners > Can't rename a polymorphic function? by JanCVanB.
(upgraded to bug, for at least the I see that it will only produce a `Str` value
problem)
Not urgent, but looking forward to what @Ayaz Hafiz (or another polymorphism specialist) thinks should be intended behavior here!
I'm pretty sure it should just work. It also should just work without the rename function just mappy = List.msp
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.)
yes, this is intended behavior
mappy : List a, (a -> b) -> List b
should yield an error, i just landed a PR to generate that error
you need to wrap mappy
in an explicit lambda for it to be polymorphic
eg mappy = \l, f -> List.map l f
This is due to let generalization? Didn't realize that affected lambdas
yes
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
wasn't this one of the situations where we could relax that if desired?
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.
it can be relaxed if you perform eta-expansion
i.e. write any value v
of type a -> b
to \a -> (v) a
Does that mean that rename = \anything -> anything
is impossible?
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
:sad:
Is this due to Roc's design or the compiler's implementation?
Ayaz Hafiz said:
it can be relaxed if you perform eta-expansion
i.e. write any valuev
of typea -> b
to\a -> (v) a
I think we should do this
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
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
It sounds like this wasn't/isn't a bug. Shall I move the last few messages to a new #ideas topic?
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.
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 *)
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?
I don't think his last PR will have fixed this
That PR was warning devs when they implied code was generic with a type signature that actually only operated on a single, specific type
The eta-expansion would need to be implemented separately
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.
>
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
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
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
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)
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?
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
Ayaz Hafiz said:
throwaway_arg
can bea
, butsome_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
Regardless, it sounds like I should start an #ideas topic for "Automatic eta expansion" based on Richard's suggestion above.
well, i was saying roc doesn’t support that
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?
Which is understandable if it's unsupported, I'm just seeking what the support boundary means in practice.
NEVERMIND, I'm wrong - fix coming
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
So the unsupported thing here seems to just be over-generalizing a function signature, which I don't mind.
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
I'm specifically interested in the "when naming it in defs" case:
mappy = rename List.map
rename = \anything -> anything
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
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
JanCVanB has marked this topic as resolved.
Last updated: Jul 06 2025 at 12:14 UTC