Is it me or the compiler?

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 nameNotOk r 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.

1 Like

Thank you, but I am still confused.

Why does the compiler understand how to handle the types in ok but not notOk for the same function type?

It just seems like notOk should 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?

2 Likes

Yes, all is clear now.

Much appreciated.

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.

notOk2 does not work for different reason. It does not work because compiler can not get the age field for you from nowhere.

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

This compiles.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.