Why are unbound type variables forbidden for functions in records?

165 views
Skip to first unread message

Martin Cerny

unread,
Jan 20, 2017, 6:06:49 AM1/20/17
to Elm Discuss
Hi all,
I was trying to do some Elm Voodoo and I stumbled upon a funny thing. It is probably deeply wrong, but I want to understand why it is wrong :-)

What I was trying to do was to define a type like this:

type alias Convertor a =
   
{ convert : b -> a
   
}


Here I get "Type alias `Convertor` must declare its use of type variable b"
Now, I understand, why you cannot have 

type alias X a =  { field1: a, field2: b }

But with the source type of functions, things are IMHO different than with values. You cannot write values of unbound types and you could not decide whether two instances of X are really the same type. 
But you can easily write functions that have unbound source types - like this one:

convertString: a -> String
convertString x
=
   
(toString x) ++ "_Foo"


And since all of functions with this signature really have the same type at JavaScript level, two instances of 'Convertor a' would always had the same type.

Now if I had
c: Convertor String
c
= {convert = convertString}
the whole thing seems type-safe...

So my question is:
Is this syntax forbidden, because it is an obscure feature that is not worth supporting, or would this syntax really allow for some type unsafe code?

Thanks!

Martin


Maxime Dantec

unread,
Jan 20, 2017, 10:56:23 AM1/20/17
to Elm Discuss
Imagine a list of `Convertor a` like this :

convertors : List (Convertor x)
convertors
=
  convertor1
:: convertor2 :: []

mapConvertors
= List.map (\c -> c.convert "foo") convertors


You can see here, that you cannot map over `Convertor::convert` if you don't know what to give to the function.

Is it clear ? I can be more thorough if you want.

Martin Cerny

unread,
Jan 20, 2017, 11:41:48 AM1/20/17
to Elm Discuss
Hi,
thanks for the answer. I however think I am missing something here - why wouldn't the compiler know what to give to the function? If I understand Elm correctly, it does not create multiple instances of functions with type variables for each type they are used with (as C++ would do). Instead Elm acts  akin to Java's type erasure: there is only one copy of a generic function and it is supposed to handle any input (and Elm indeed generates only one copy of the function). So in your example, all 'c.convert' functions accept anything (so the "foo" string is not a problem) and return an X so the result is 'List X'.

Or is this just a quirk the current Elm compiler implementation?

Thanks
Martin

Maxime Dantec

unread,
Jan 20, 2017, 11:45:23 AM1/20/17
to Elm Discuss
Oh I see. I didn't noticed the Basic.toString. This is the only function in Elm that accepts anything anyway. If you swap that particular function to anything else it cannot work.

Nick H

unread,
Jan 20, 2017, 1:52:48 PM1/20/17
to elm-d...@googlegroups.com
All type variables need to be mentioned on the left side of the type alias definition. But that doesn't mean you need to bind them. This compiles fine:

type alias Convertor b a =
    { convert : b -> a
    }

c: Convertor b String

c = {convert = convertString}


In other words, the unbound type variable needs to be mentioned in the type signature, even if you are wrapping more abstractions over it.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Martin Cerny

unread,
Jan 20, 2017, 2:06:42 PM1/20/17
to Elm Discuss
Thanks for the ideas, but neither is satisfactory for my (crazy and contrived) use case :-)

@Maxime:
Once again, I maybe missing something, but Basic.toString is not the only function that does that. My particular aim was to combine native functions and functions that simply ignore the parameter which are both examples of functions that can accept anything.

Also, more broadly you could have definitions such as:
type alias Mapper a =
   
{ map : List b -> (b -> a) -> List a
   
}

now - due to the erasing implementation of Elm, I can execute the map function safely for any type of input as long as the first two parameters match. Now this latter case would require the compiler to infer things like
m : Mapper Int

x
= m.map ["a" "b"]
--x: (String -> Int) -> List Int

Which may and may not be the exact same thing the compiler already does for regular functions.
There might even be a non-crazy use case for that :-)

@Nick:
I am aware of that possiblity, but adding type variables is not an option in my use case. I need to be able to call things like (highly contrived for brevity),
func: Convertor a -> Int -> String -> (a,a)
func conv i s
=
 
(conv.convert i, conv.convert s)

which I could not do if I bind the type variable outside the individual subexpressions.

In case you are wondering why would I want such madness, I was toying with the idea of having automatic serialization/deserialization (JSON/XML etc.) of arbitrary non-function types (records and possibly also union types) in almost pure Elm, with only a single magic native function. The idea was like that:
--pure Elm type
type
TypeInfo a = ...

--JavaScript magic
getTypeInfo
: a -> TypeInfo a

--pure Elm
decoder
: TypeInfo a -> Json.Decode.Decoder a

However, I ended up requiring the functions of the aforementioned weird form in TypeInfo a so it probably cannot work. I am aware of cases that could not be solved in principle without help from compiler even if everything worked as I hoped it would (and maybe there are other problems I missed), but I had fun trying :-)

Still curious, if this idea could work in general or if it implies some problems for the whole type system...

Thanks
Martin
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Joey Eremondi

unread,
Jan 20, 2017, 2:14:22 PM1/20/17
to elm-d...@googlegroups.com
I'm still grokking what you want, but I think what you're describing is higher-rank polymorphism.

This is unlikely to end up in Elm any time soon: it's pretty advanced, and it breaks type inference. If you *really* want a language that has it, try Haskell(GHCJS) or PureScript.

But I suspect there is a way to get the results you want in Elm, the road there might just be different.

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Nick H

unread,
Jan 20, 2017, 2:20:59 PM1/20/17
to elm-d...@googlegroups.com
OK, I understand... you're up in the stratosphere somewhere :-)

I can't offer anymore insight, but to back up Maxime. toString and the equality operators are the only functions that take arguments of unspecified type and return a value with a fixed type.

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Joey Eremondi

unread,
Jan 20, 2017, 2:22:52 PM1/20/17
to elm-d...@googlegroups.com
toString and the equality operators are the only functions that take arguments of unspecified type and return a value with a fixed type

Except for functions that ignore their argument.

It's perfectly valid to do
intoInt : a -> Int
intoInt _ = 3

But it's probably not what you're looking for.
Reply all
Reply to author
Forward
0 new messages