elm / compiler

Compiler for Elm, a functional language for reliable webapps.
https://elm-lang.org/
BSD 3-Clause "New" or "Revised" License
7.54k stars 664 forks source link

problem with extensible record type annotation on higher-order function #1959

Open mfeineis opened 5 years ago

mfeineis commented 5 years ago

Quick Summary: The following program should type-check as the extensible record type matches the model type, interesting enough it does without type annotation but annotating it leads to a confusing error message claiming that Model doesn't match with { id : Int } which isn't true.

SSCCE

module Main exposing (main)

type alias Model = { id : Int, value : String }

main : Program () Model ()
main =
    Platform.worker
        { init = \_ -> ( { id = 1, value = "" }, Cmd.none )
        , subscriptions = always Sub.none
        , update =
            \_ model -> 
                ( { model | value = calcValue model toInfer }
                , Cmd.none
                )
        }

-- Without the type annotation it works but breaks when it's there
calcValue : Model -> ({ a | id : Int } -> String) -> String
calcValue withId calc =
    calc withId

toInfer : { a | id : Int } -> String
toInfer { id } =
    String.fromInt id

produces a type mismatch error

type mismatch
Line 25, Column 10
The 1st argument to `calc` is not what I expect:

25|     calc withId
             ^^^^^^
This `withId` value is a:

    Model

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

    { a | id : Int }

Hint: Seems like a record field typo. Maybe value should be id?

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!

Additional Details

Here is an Ellie reproducing the problem.

Changing the type signature to calcValue : {a | id: Int} -> ({ a | id : Int } -> String) -> String works.

carmour24 commented 5 years ago

I appear to be falling foul of this same issue, unfortunately without an apparent workaround. In my instance I want to use a custom type to contain various record types and then use extensible records to extract the required field.

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

type alias Model =
    { currentChange :  Change }

initialModel : Model
initialModel =
    { currentChange = PersonUpdate {name = "Christopher", address = "Glasgow"} }

type alias Named a =
    { a | name : String }

type alias Person =
      { name: String, address : String }

type alias Business =
     { name: String, employeeCount : Int }

type Change
    = PersonUpdate Person
    | BusinessUpdate Business

type Msg
    = UpdateCurrentChange Change

changeToString : (Named a -> String) -> Change -> String
changeToString stringFromNamed change =
    case change of
        PersonUpdate person ->
            stringFromNamed person

        BusinessUpdate business ->
            stringFromNamed business

update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateCurrentChange change ->
            { model | currentChange = change }

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick (UpdateCurrentChange (PersonUpdate {name = "Chris", address = "Glasgow"}))  ] [ text "Chris" ]
        , button [ onClick (UpdateCurrentChange (BusinessUpdate {name = "Tramway", employeeCount = 100})) ] [ text "Tramway" ]
        , div [] [  text <| changeToString (\named -> named.name) model.currentChange ]
        ]

main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Producing the following error:

Type Mismatch
Line 47, Column 29
The 1st argument to `stringFromNamed` is not what I expect:

47|             stringFromNamed person
                                ^^^^^^
This `person` value is a:

    Person

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

    { a | name : String }

Hint: Seems like a record field typo. Maybe address should be name?

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!

Essentially the same issue as above, however when the calls to the function parameter occur multiple times in the same higher order function (as in the case statement in the example) then the workarounds of modifying or removing the signature mentioned above no longer work.

There is an Ellie with the issue here: https://ellie-app.com/77C7SXCNFMza1

Also, I've seen mentioned elsewhere that the purpose of extensible records is to allow access only to a subset of a larger record type so if this use of them for extracting data from multiple record types with the correct fields isn't intended functionality maybe this would be a candidate for a new, more specific, error message.

Apologies if adding to this issue isn't the correct protocol, I can add a new issue if necessary.

rlefevre commented 5 years ago

See https://groups.google.com/forum/#!topic/elm-discuss/BR3J1saXJUU.

zwilias commented 4 years ago

Yeah, to reiterate, both of the examples given in this issue boil down to quantification.

In @mfeineis example:

calcValue : Model -> ({ a | id : Int } -> String) -> String
calcValue withId calc =
    calc withId

I could pass in a function { foo : Int, id : Int } -> String, and according to your type signature, it should be accepted. Clearly, it should not, because Model has no foo : Int field.

In languages that allow explicit quantification, this could be expressed like so: Model -> (forall a. { a | id : Int } -> String) -> String, which means that the function you pass in should work for "any imaginablea", not just for "some specified a"

So, the examples here don't work for the same reason that this code doesn't work:

foo : (a -> a) -> Int -> Int
foo f x = f x