I have been struggling a bit with the compiler. In the below example I don’t understand why nameNotOk is not a valid function. Is it my misunderstanding or is this something the compiler should be capable of, but is not (at least not yet)? Thank you!
module T exposing (T)
type T
= A { name : String, age : Int }
| B { name : String, value : Float }
nameOk : T -> String
nameOk t =
case t of
A r ->
r.name
B r ->
r.name
nameStillOk : T -> String
nameStillOk t =
let
name r =
r.name
in
case t of
A r ->
name r
B r ->
name r
-- Fails to compile
-- I don't understand why this won't compile
nameNotOk : ({ a | name : String } -> String) -> T -> String
nameNotOk f t =
case t of
A r ->
f r
B r ->
f r
-- Fails to compile
-- this one I kind of understand since the branching returns two types
nameAlsoNotOk : T -> String
nameAlsoNotOk t =
(case t of
A r ->
r
B r ->
r
)
|> .name
Hi. In case in nameNotOkr in A an B variants have different type. Thus compiler can not unify types. So all values passed to f should have the same type. Extensible record type annotation have to become concrete for compiler to be able to compile. It can not work with dynamic values.
Why does the compiler understand how to handle the types in ok but not notOk for the same function type?
It just seems like notOkshould be possible.
name : { a | name : String } -> String
name r =
r.name
ok : T -> String
ok t =
case t of
A r ->
name r
B r ->
name r
notOk : ({ a | name : String } -> String) -> T -> String
notOk f t =
case t of
A r ->
f r
B r ->
f r
Sure. f as a function in context of notOk can work with values of the same type only. But in case of notOk function f works with r of two different types. That’s not possible with Elm compiled.
notOk : ({ a | name : String } -> String) -> T -> String
notOk f t =
case t of
A r ->
f r -- In this case `r` has `{ name : String, age : Int }` signature
-- After this step compiler unifies `({ a | name : String } -> String)`
-- to be `({ name : String, age : Int } -> String)`
B r ->
f r -- In this case `r` has `{ name : String, value : Float }`
-- At this step compiler says that I'm expecting `f`
-- to have signature of `({ name : String, age : Int } -> String)`,
-- but you give me `({ name : String, value : Float } -> String)`,
-- so I can not do that.
Compiler always needs concrete types. Does that help?
I want to add something because I think the other answer is not completely right. For example, the following function does not compile either, even though there is only one type of record:
notOk2 : ({ a | name : String } -> String) -> { name : String, age : Int } -> String
notOk2 f r = f r
A type annotation on a function with type variables (like a here) always means that the caller of the function gets to decide what a is. For example, calling that function, I could pick { address : String } and use
getAddress : { address : String, name : String } -> String
getAddress r = r.address
as the first argument. This function cannot be used on the { name : String, age : Int } record on which notOk2 wants to use it.
By the way, the caller has to pick something concrete for a even if they fill that argument with a function that has “matching” type variables. Therefore, Elm has to unify the two uses of f in your function, i.e. the other answer is correct as well.
The compiler does not need to get the age field as the record is provided by the caller. The following version where the “intended” argument for f is inlined works just fine:
ok2 : { name : String, age : Int } -> String
ok2 r =
let
getName s = s.name
in
getName s
The problem really is that the original function
notOk : ({ a | name : String } -> String) -> T -> String
claims that is can be called with any a the caller wants; for example, my getAddress is valid. You can try that by defining
notOk : ({ a | name : String } -> String) -> T -> String
notOk f t = Debug.todo "not implemented"
getAddress : { name : String, address : String } -> String
getAddress r = r.address
test = notOk getAddress (A { name = "eike", age = -1 })