Type mismatch when calling passed auxilliary func, not when hardcoding the reference to it

Hi everybody,

i’m in the middle of rewriting something into Elm and i run into a weird thing where i get a Type mismatch when calling a function passed as an argument but not if i hardcode the reference(sorry for wrong term maybe) to the same function in the caller instead.

Following code is very simplified to reproduce the problem:

type Model
  = Counter1 Counter1Data
  | Counter2 Counter2Data

type alias Counter1Data =
  { count : Int
  , foo : String
  }

type alias Counter2Data =
  { count : Int
  , bar : String
  }

So my model is a tagged union where both Counter1 and Counter2 have similar but not the same data.

Now i have a view and two ancilliary functions which look like that:

model = Counter1 { count = 0, foo = "" }

view : Model -> Html Msg
view model =
  let
    updated = updateCounter model
  in
    case updated of
      Counter1 data -> Html.text (String.fromInt data.count)
      Counter2 data -> Html.text (String.fromInt data.count)

updateCounter model =
  case model of
    Counter1 data ->
      Counter1 (increment data)

    Counter2 data ->
      Counter2 (increment data)

increment : { a | count : Int } -> { a | count : Int }
increment a =
  { a | count = a.count + 1 }

So these two ancialliary functions just increment my model and like that it compiles fine and shows the incremented counter on the screen properly.

But when i change the code so that the increment function is not hardcoded in updateCounter but is instead passed to it it crashes with Type Mismatch error:

updateCounter updateFunc model =
  case model of
    Counter1 data ->
      Counter1 (updateFunc data)

    Counter2 data ->
      Counter2 (updateFunc data)


increment : { a | count : Int } -> { a | count : Int }
increment a =
  { a | count = a.count + 1 }

view : Model -> Html Msg
view model =
  let
    updated = updateCounter increment model
  in
    case updated of
      Counter1 data -> Html.text (String.fromInt data.count)
      Counter2 data -> Html.text (String.fromInt data.count)

And also ti crashes the same way when i’m trying to pass the increment function as lambda:

updateCounter updateFunc model =
  case model of
    Counter1 data ->
      Counter1 (updateFunc data)

    Counter2 data ->
      Counter2 (updateFunc data)

view : Model -> Html Msg
view model =
  let
    increment a =
      { a | count = a.count + 1 }
    updated = updateCounter increment model
  in
    case updated of
      Counter1 data -> Html.text (String.fromInt data.count)
      Counter2 data -> Html.text (String.fromInt data.count)

Also in Ellie here: https://ellie-app.com/qbPNjPpz7FSa1
Second case(crashing): https://ellie-app.com/qbQq7kdCH5ra1

What am i missing here? Why does it work in the first example but not in the second and third?

Thank you!

Also third Ellie with the third case(also crashing): https://ellie-app.com/qbQqkmCRKJda1

I couldn’t include more than 2 links in my first post…

The first thing I’m noticing when looking at your code, is that you don’t annotate your updateCounter function. Let’s do that first!

updateCounter : ({ a | count : Int } -> { a | count : Int }) -> Model -> Model
updateCounter updateFunc model =
  case model of
    Counter1 data ->
      Counter1 (updateFunc data)

    Counter2 data ->
      Counter2 (updateFunc data)
1 Like

How annotations work

The use of type definitions like a might seem flexible at first, but it’s easy to get mistakes. Let’s look at your increment code first:

increment : { a | count : Int } -> { a | count : Int }
increment a =
  { a | count = a.count + 1 }

This code says, “hey! You can use me on any object a as long as it has a count field! Doesn’t matter what!” and the compiler will take that into account. So if it sees your first implementation:

IMPLEMENTATION 1 THAT SUCCEEDS

updateCounter : Model -> Model
updateCounter model =
  case model of
    Counter1 data ->
      Counter1 (increment data)

    Counter2 data ->
      Counter2 (increment data)

The compiler looks at the two lines and thinks “oh right, I can fit Counter1Data in { a | count : Int }, so that works. Also, I can fit Counter2Data in { a | count : Int }, so that works.” That’s why the compiler succeeds.

Where it goes wrong

Let’s look at the 2nd Ellie example - the one that goes wrong. When you annotate the function, the compiler throws a different error.

IMPLEMENTATION 2 THAT FAILS

updateCounter : ({ a | count : Int } -> { a | count : Int }) -> Model -> Model
updateCounter updateFunc model =
  case model of
    Counter1 data ->
      Counter1 (updateFunc data)

    Counter2 data ->
      Counter2 (updateFunc data)

The compiler says “hey, that’s a Counter1Data, not a { a | count : Int }! That’s not the same!” Which is very confusing because it seems to contradict what we see in your first example. But it’s not the same thing!

You need to keep in mind that the compiler takes the definition of { a | count : Int } very seriously in both functions. In example 1, you may not use any other fields like foo or bar; only count can be used. In the second example, you may also not assume that { a | count : Int } has any other fields like foo and bar; hence you may not use updateFunc on a Counter1Data or a Counter2Data. (In your example, it ONLY refuses Counter2Data because the lack of annotation makes it assume that updateFunc is a Counter1Data -> Counter1Data type.)

2 Likes

An obvious example

For obvious reasons, we want updateCounter to fail if you pass the following function as its updateFunc argument, even though it’s perfectly legal according to the type annotation:

wrongUpdateFunc : Counter1Data -> Counter1Data
wrongUpdateFunc data =
    { count = data.count + 1, bar = "bar" }

This would obviously fail horribly! And since the compiler has to accept that updateCounter CAN take a function like this as its argument, you can’t just use it on a Counter1Data or Counter2Data type at the same time.

1 Like

Hi Bram, thank you very much for your replies. I updated the annotations and the error changed a bit but it still doesn’t make any sense to me.

The compiler says “hey, that’s a Counter1Data, not a { a | count : Int }! That’s not the same!” Which is very confusing because it seems to contradict what we see in your first example. But it’s not the same thing!

How come it’s not the same? I mean i get that they are not the same but Counter1Data is a record with two fields - count and foo. And { a | count : Int } is a record which has at least a field count of type Int right? Then Counter1Data matches with { a | count : Int } because it has that field.

You need to keep in mind that the compiler takes the definition of { a | count : Int } very seriously in both functions. In example 1, you may not use any other fields like foo or bar; only count can be used.

I don’t get the point here. The increment function isn’t using anything else than count field and compiler can see that and i would expect that is the reason why it’s allright with it in the first working example. Because it knows from the definitions that both Counter1Data and Counter2Data have the field. Btw i would expect(and it would make sense) if the compiler complaints when the function tries to manipulate fields which are not present in both types. But that is not the case, so it’s ok.

In the second example, you may also not assume that { a | count : Int } has any other fields like foo and bar; hence you may not use updateFunc on a Counter1Data or a Counter2Data.

This is confusing for me. In the second example the compiler can also see the definition of increment function(in fact it’s the same function) so it must know there is no code trying to manipulate fields which are not there. It only manipulates fields which are present in both Counter1Data and Counter2Data so what is the difference?

An obvious example

Yes, here it makes sense to me that the compiler would complain because i’m trying manipulate fields which are not present in all the types it works with.

1 Like

The compiler interprets { a | count : Int } as “whoever uses this function, can insert ANY record as long as it has a count field of type Int.”

So updateCounter : ({ a | count : Int } -> { a | count : Int }) -> Model -> Model means "whoever uses this function, can insert ANY function with ANY record as input and output as long as it has count field of type Int. Which is not the case for you: you mean to say “whoever uses this function, must insert a function that supports ALL records that have a count field of type Int.”

And that’s where the compiler complains (in a un-Elm-like unclear manner). Conceptually, you are saying that you want updateCounter to work even if I, as an ignorant programmer, insert my function wrongUpdateFunc into updateCounter.

So when you say updateFunc is of type { a | count : Int } -> { a | count : Int }, and you use it on Counter1Data, the compiler says “a is not Counter1Data”.

It’s basically saying “how do you know that updateFunc is compatible? What if it isn’t? You don’t know if a is Counter1Data or that the function is able to deal with it!”

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