The argument is not what I expect ( extensible record )

I tried to use extensible records, to call the same function (funcOnBase) on multiple subtypes. This is my code:

import Html exposing (text)

type alias Base a = { a | foo : Int }

type alias Sub1 = Base { bar : String }
type alias Sub2 = Base { baz : Float }

type SubTree = S1 Sub1 | S2 Sub2

funcOnBase : Base a -> Bool
funcOnBase b = b.foo > 0

funcOnSTree : (Base a -> Bool) -> SubTree -> Bool
funcOnSTree fnc x =
  case x of
    S1 s1 -> fnc s1
    S2 s2 -> fnc s2

main =
  text (Debug.toString <| funcOnSTree funcOnBase <| S1 { foo = -5, bar = "" })

Unfortunately I get this error:

The 1st argument to `fnc` is not what I expect:

16|     S1 s1 -> fnc s1
                     ^^
This `s1` value is a:

    Sub1

But `fnc` needs the 1st argument to be:

    { a | foo : Int }

Hint: Seems like a record field typo. Maybe bar should be foo?

Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!

The s1 value is a Sub1, which is Base { bar : String } = { foo : Int, bar : String }. Why is it not accepted as { a | foo : Int }?

1 Like

Interesting question.

One problem here is that when function funcOnSTree is used, it must be possible to replace the type parameter a with a single concrete type.

During different invocations type parameter can refer to different concrete types, so both of these work:

funcOnBase { foo = -5, bar = "" }
funcOnBase { foo = -5, baz = 3.7 }

But during a single invocation type parameter can’t refer to different concrete types.

Now if I change your function to this to fix this problem:

funcOnSTree : (Base a -> Bool) -> (Base b -> Bool) -> SubTree -> Bool
funcOnSTree fncA fncB x =
  case x of
    S1 s1 -> fncA s1
    S2 s2 -> fncB s2

I still get same error. So I don’t understand what’s really going on here?

Also this does work which shows that both Sub1 and Sub2 are accepted as Base a:

funcOnSTree : SubTree -> Bool
funcOnSTree x =
  case x of
    S1 s1 -> funcOnBase s1
    S2 s2 -> funcOnBase s2

I don’t understand why funcOnBase works while fncA/fncB doesn’t?

1 Like

The reason for this behaviour is a technical detail within Elms Type system. (and many other FP languages as well)

We actually have two different interpretations of Base a: Either a can be anything or something very specific.

Let’s use Forall a. Base a if we mean that a can be general and Base a if a is fixed.

Elm interpretes your function like this:

funcOnBase : forall a. ( Base a -> Bool )

funcOnSTree : forall a. ( (Base a -> Bool) -> SubTree -> Bool )

So actually we see that funcOnBase has a different type than the first argument of funcOnSTree.

Normally you don’t notice this, because the elm type system has a small trick called instantiation: It can replace

forall a. (Base a -> Bool) -> SubTree -> Bool

with

(Base a -> Bool) -> SubTree -> Bool

But now a is a fixed type. So once it comes to fnc s1 it fails and therefore instantiation does not work.

1 Like

Ok, so type parameter in function definition must be used in a way that when instantiation is done,
the resulting fixed type agrees across all uses? (Or something like that, I’m not sure how to say this correctly.)

For example following works because when type parameters a and b are instantiated, they will be same types both in fncA/fncB and in GenericSubTree?

type alias Base a = { a | foo : Int }

type alias Sub1 = Base { bar : String }
type alias Sub2 = Base { baz : Float }

type GenericSubTree x y = S1 (Base x) | S2 (Base y)

type alias SubTree = GenericSubTree Sub1 Sub2

funcOnBase : Base a -> Bool
funcOnBase b = b.foo > 0

funcOnSTree : (Base a -> Bool) -> (Base b -> Bool) -> GenericSubTree a b -> Bool
funcOnSTree fncA fncB x =
  case x of
    S1 s1 -> fncA s1
    S2 s2 -> fncB s2

Other FP languages support type classes, but in Elm I can not fix it that way.

I would like to write a generic map for the type SubTree wich works on Base, so putting the funcOnBase inside the 'let` block is not an option:

mapTreeBase : (Base a -> b) -> SubTree -> b
mapTreeBase fnc x =
  case x of
    S1 s1 -> fnc s1
    S2 s2 -> fnc s2

Could you please give some more details about how to use this Forall notation? I was not able to find anything about it.
If I put foral or Forall in the code, I get syntax error.

That forall is a concept used within the compiler, not something directly supported by Elm language.

What about my GenericSubTree example above? Would something like that work for you?

Thank you for the suggestion, but not really. That function is far from generic. Your funcOnSTree implementation requires fncA fncB instead of a single fnc parameter.

In this simplified example I have only two subtypes, but I would like to create a mapTreeBase with 7 of them.

Do you have only a fixed number of different possible types? Then using a custom type instead of generic Base a could work:

type alias Node =
    { foo: Int
    , data: NodeData
    }

type NodeData
    = Bar String
    | Baz Float
    | ...

One other option would be to call fnc {foo = s1.foo} (same for s2), so you’re always calling it on Base {}

1 Like

Yes, that’s correct.

You can help the compiler a bit:

toBase : Base a -> Base {}
toBase base =
  { foo = base.foo}

funcOnBase : Base a -> Bool
funcOnBase b = b.foo > 0

funcOnSTree : (Base {} -> Bool) -> SubTree -> Bool
funcOnSTree fnc x =
  case x of
    S1 s1 -> fnc <| toBase s1
    S2 s2 -> fnc <| toBase s2

Heres an Ellie implementing it that way

3 Likes

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