Extensible type hell

Extensible types are a great way to make reusable components. However, step outside of their basic usage and the type errors start to look very strange. We use extensible types in Components, as domain model getters and in our Endpoints for encoding/decoding. However, we have encountered, thrice now, a perplexing error when converting from concrete to extensible types.

This section exists because I spent two days scratching my head and later on observed both my work colleagues (concurrent financial systems expert and haskell guy) doing the same thing. The problem arises from a compiler error such as this:

TYPE MISMATCH
Line 11, Column 13
The definition of container does not match its type annotation.

The type annotation for container says it is a:

Main.PointContainer Main.ThreeDPoint
But the definition (shown above) is a:

Main.PointContainer { z : Int }
Hint: Looks like a record is missing these fields: x and y. Potential typos include:

x -> z
y -> z

So the compiler says: "The type annotation say it's the following"

Main.PointContainer Main.ThreeDPoint

But you are giving it this instead:

Main.PointContainer { z : Int }

Let's look at the relevant code (full demo1 hosted by ellie-app2):

module Main exposing (main)

import Html exposing (Html, text)


main : Html msg
main =
    let
        container : PointContainer ThreeDPoint
        container =
            make3D 1 2 3
                |> makeContainer
    in
    container
        |> (\{ point } -> printAPoint point)

type alias PrintablePoint a =
    { a | x : Int, y : Int }


type alias ThreeDPoint =
    { x : Int, z : Int, y : Int }


type alias PointContainer xyPoint =
    { point : PrintablePoint xyPoint
    }


makeContainer : PrintablePoint a -> PointContainer a
makeContainer printablePoint =
    { point = printablePoint }


make3D : Int -> Int -> Int -> ThreeDPoint
make3D =
    ThreeDPoint

So going back to our error, let's work out what is going on:

-- the error is in the 'container' function.
container : PointContainer ThreeDPoint
container =
    make3D 1 2 3          -- makes a ThreeDPoint = { x : Int, z : Int, y : Int }
        |> makeContainer  -- takes in PrintablePoint a = { a | x : Int, y : Int }
                          -- returns PointContainer = { point : PrintablePoint a }

What's the problem???

Turns out, it isn't really a problem:

container : PointContainer ThreeDPoint
container =
    make3D 1 2 3
        |> toPrintablePoint
        |> makeContainer

toPrintablePoint : ThreeDPoint -> PrintablePoint ThreeDPoint
toPrintablePoint =
    identity

Here, all we're doing is matching the types, make3D returns a ThreeDPoint but makeContainer accepts a PrintablePoint ThreeDPoint so we have to give it that type. However, since they are the same concrete record, we use use the identity function here... confused? So am I.

I played around with a few other things:


-- just add identity, Fail.
container : PointContainer ThreeDPoint
container =
    make3D 1 2 3
        |> identity
        |> makeContainer

-- add toPrintablePoint without a type annotation, Fail.
container : PointContainer ThreeDPoint
container =
    make3D 1 2 3
        |> toPrintablePoint
        |> makeContainer

toPrintablePoint =
    identity

The issue is definitely that the compiler is not able to resolve the two types. The full working demo3 demonstrates how adding a identity function solves the error.

In time, others smarter than I will be able to explain to me why this behaviour occurs as such, however, for the moment, if you are hitting type issues which you are sure should work, try explicitly converting the value to the type that's required even if the underlying record is the same.

Footnotes

1. Full broken version: https://ellie-app.com/f726SMFcpa1/0
2. By Luke Westby, a full featured elm compiler in your browser - https://ellie-app.com
3. Full working example : https://ellie-app.com/m9f8zgZZSa1/1

results matching ""

    No results matching ""