Using similar narrow types

Hello,
I’d like to ask whether this should be possible or not.

The code:

viewMenuItemLink : ({ a | id : Int } -> Bool) -> { a | id : Int, title : String } -> Html Msg
viewMenuItemLink isActive ({ id, title } as item) =
    Components.menuListItemLink (isActive item)
        [ Events.onClick (SelectMenuItem id) ]
        [ text title ]

It produces following error.

I tried even to change one of narrow annotation’s type variable so that they differ:
viewMenuItemLink : ({ a | id : Int } -> Bool) -> { b | id : Int, title : String } -> Html Msg

But compiler does not like it neither:

It is not a big deal to just make those annotations same… but I was just wondering why it does not work like that…

Thanks in advance

4 Likes

This was pretty puzzling to me too, but I figured it out!

When you define a function like:

test : { a | foo : Int } -> Bool
test a = test.foo == 5

Then { a | foo : Int } means that it works with any record with and foo : Int field, as you expect.

But when you define a function like:

test : ({ a | foo : Int } -> Bool) -> { foo : Int } -> Bool
test fn a =
    fn a

What that says is not that the passed in function has to work with any record with a foo : Int field, but that the passed in function decides what fields it requires, beyond the foo : Int. So it would for example be valid to pass a { foo : Int, bar : Int } -> Bool function, and that of course wouldn’t work with the { foo : Int } record!

3 Likes

I find it somewhat weird that Elm doesn’t just ignore the bar field in this case - is there a specific reason as to why?

Oops, just re-read this and it makes sense now! So is there no way to type an argument such that it accepts a function taking { a | foo : Int }?


Found a related GitHub issue here: problem with extensible record type annotation on higher-order function · Issue #1959 · elm/compiler · GitHub

It’s not answering your question directly, but wouldn’t it be a possibility to “transform” your input to the “important” data via another function?

You have a function that expects some data and returns a bool:

isSomethingTrue : {foo : Int} -> Bool

Then you have your data inside of the model:

type alias Model =
  { bla : String
  , blubb : String
  -- This is the relevant data
  , foo : Int
  }

Now you need to extract the relevant data from the model:

toSomething : Model -> {foo : Int}

and then you can put it together?

test : (a -> {Int : String}) -> ({foo : Int } -> Bool) -> a -> Bool
test getSomething checkSomething a =
  a
  |> getSomething
  |> checkSomething

I’m not sure if this makes and is even what you’re looking for, sorry…

Thanks for spending time on that.
I dont quite undestand this part:

What do you mean by “…what fields it requires, beyond the foo : Int.”. Doesnt that test function accept as 1st argument a function that accepts map that has field foo : Int and does not care about other fields?

I tried:
image
As last getField1 invocation suggests, it does not need any more fields “beyond” field1 field.

But tying to fit “wide narrow type” to function that accepts “more narrow type” just does not work. (compiler error)

useShortAndLongType : ({ a | field1 : Int } -> Bool) -> { a | field1 : Int, field2 : Bool } -> Bool
useShortAndLongType getField1Fn longerType =
    getField1Fn longerType

I think that it would be easier to just invoke passed-in fn like this:

useShortAndLongType : (Int -> Bool) -> { a | field1 : Int, field2 : Bool } -> Int
useShortAndLongType getField1Fn longerType =
    getField1Fn longerType.field1

But looking at this now… If I would need to pass to getField1Fn more fields, another extracting function could make sense… Although just simply passing to getField1Fn more params could also be an option that in addition does not require any intermediate functions (but that would depend on frequency of using such getField1Fn function not to break DRY principle).

Ohhh, I might just understood what you are saying here… That the annotation: ({ a | foo : Int } -> Bool) says that it is a function that accepts one param which must contain foo : Int field. Wow, would not even try to look on that from the other way around…
But than such annotation would not make sense, would it?

I’ve been using Elm for 1.5 years and I got to learn something new too :smile:

Here’s a simpler example of the same thing.
If you define a List to be of type a, you’re saying that it can hold any type, which would be impossible, except for the empty list, for which it is “vacuously true”.

empty : List a
empty = []

But when you’re saying that a function takes List a, it’s the opposite - all lists are valid arguments.

length: List a -> Int
lenght = ...

So in the first case, you’re placing a constraint on the list, and in the second case you’re unconstraining the list.

1 Like

I see. So with that function annotation you really say that any function is valid when it accepts first argument as a map containing given fields e.g. the mentioned foo : Int

Thanks for clarification! :slight_smile:

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