@jako (Jacob) posted a new topic, subject "Compiler trips up on extensible records?". It read:

   ## TL;DR
    
    The following type of construction yields compiler errors:
    [code] 
        type alias WithId a 
            = { a | id : String }
        
        type alias NameAndId =
            { id : String, name: String }
        
        type Msg a
            = WrapWithId (WithId a)
        
    [/code]
    
    In the REPL `x` and `y` defined below compare true
    [code] 
        > x : WithId NameAndId
        | x = { id = "id", name = "name" }
        |
        { id = "id", name = "name" }
            : WithId NameAndId
        > y : NameAndId
        | y = { id = "id", name = "name" }
        |
        { id = "id", name = "name" } : NameAndId
        > x == y
        True : Bool
        
    [/code]
    
    but comparing `u` and `v` yields a compiler error (TYPE MISMATCH)
    [code] 
        > u = WrapWithId x
        WrapWithId { id = "id", name = "name" }
            : Msg NameAndId
        > v = WrapWithId y
        WrapWithId { id = "id", name = "name" }
            : Msg { name : String }
        
    [/code]
    
    ## Original message
    
    Hi,
    
    I am fairly new to Elm, and I have encountered a compiler error, which seems wrong to me. Can anyone help? Below is code to reproduce the error. I have 3 files: `Main.elm`, `Other.elm`, and `Third.elm`.
    
    The compiler does not seem to like line 37: `t1 = Third.Third1 o1`. It complains:
    [code] 
        Detected problems in 1 module.
        -- TYPE MISMATCH -------------------------------------------------- src\Main.elm
        
        The 1st argument to `Third1` is not what I expect:
        
        37|     t1 = Third.Third1 o1
                                  ^^
        This `o1` value is a:
        
            Other.MyOtherMsg { name : String }
        
        But `Third1` needs the 1st argument to be:
        
            Other.MyOtherMsg Third.MyThird
        
        Hint: Looks like the id field is missing.
        
    [/code]
    
    If I comment out line 37 and run the code, I can see the value of `o1`. It is `Other1 { id = "myId", name = "name" }`. So at runtime `id` is present, but at compile time it is not?
    
    The `Main.elm` file is just the Buttons example given in the Elm guide with line 34-38 added.
    
    ### Main.elm
    [code] 
        module Main exposing (..)
        
        import Browser
        import Html exposing (Html, button, div, text)
        import Html.Events exposing (onClick)
        import Other
        import Third
        import Debug
        
        
        -- MAIN
        
        
        main =
          Browser.sandbox { init = init, update = update, view = view }
        
        
        
        -- MODEL
        
        type alias Model = Int
        
        init : Model
        init =
          0
        
        
        -- UPDATE
        
        type Msg = Increment | Decrement
        
        update : Msg -> Model -> Model
        update msg model =
          let
            mm = Third.newMyThird "myId"
            o1 = Debug.log "o1" <| Other.Other1 mm
            t1 = Third.Third1 o1
          in
            case msg of
              Increment ->
                model + 1
        
              Decrement ->
                model - 1
        
        
        -- VIEW
        
        view : Model -> Html Msg
        view model =
          div []
            [ button [ onClick Decrement ] [ text "-" ]
            , div [] [ text (String.fromInt model) ]
            , button [ onClick Increment ] [ text "+" ]
            ]
        
        
    [/code]
    
    ### Other.elm
    [code] 
        module Other exposing (..)
        
        
        type alias MyOther a =
          { a | id : String }
        
        
        type MyOtherMsg a =
          Other1 (MyOther a)
        
    [/code]
    
    ### Third.elm
    [code] 
        module Third exposing (..)
        
        import Other
        
        
        type alias MyThird =
          Other.MyOther { name : String }
        
        
        newMyThird : String -> MyThird
        newMyThird id =
          { id = id, name = "name" }
        
        
        type MyThirdMsg =
          Third1 (Other.MyOtherMsg MyThird)
        
    [/code]

To which @brian (Brian) replied

    Hey! Welcome to Elm! 
    
    Extensible records can be tricky to get right. With these definitions:
    [code] 
        type alias MyOther a =
            { a | id : String }
        
        type alias MyThird =
            Other.MyOther { name : String }
        
    [/code]
    
    You are saying "`MyOther` is a record with an `id` field. I do not care about the whatever else is in there." Then you are saying `MyThird` is a `MyOther` with only `name`—no `id`! **`MyOther` does not add `id`, it only matches on records that have it already.** With that knowledge, is the compiler error clearer?
    
    Mechanically speaking, you _could_ change the definition of `MyThird` to be `Other.MyOther { id : String, name : String }`. That would probably compile (although I have not tried it.)
    
    But in the bigger picture, what are you trying to do here? It appears you may want some kind of inheritance? Extensible records would not give you that. If you can say more about what you're trying to do, I (or someone else) may be able to suggest something that will work well for your situation.

After which, @jako (Jacob) added

    Thank you!
    
    I just tried to change the definition of the type `MyThird` to:
    [code] 
        type alias MyThird =
          { id : String, name : String }
        
    [/code]
    
    It still yields the same error. My understanding was, that I was extending the record type definition to include additional fields - see here.
    
    My use case is a bit hard to explain. I am trying to build a reusable, interactive table module (Other.elm here), which are used by several other components (Third.elm) in my main application (Main.elm). I am trying to impose only the restriction that the table rows (MyOther) have `id`'s in the table module.

Then @Philipp_Krueger (Philipp Krüger) said

    I've been experimenting a little more with this:  
    `MyThird` seems to represent a record of type `{ id : String, name : String }`. If I create record literals that's the only thing accepted:
    [code] 
        {-| works
        -}
        def1 : MyThird
        def1 =
            { id = "", name = "" }
        
        
        {-| fails
        -}
        def2 : MyThird
        def2 =
            { name = "" }
        
        
        {-| also fails
        -}
        def3 : MyThird
        def3 =
            { id = "", name = "", more = "" }
        
        
        {-| also fails
        -}
        def4 : MyThird
        def4 =
            { id = "" }
        
    [/code]
    
    So, to me, the compiler error does seem confusing.
    
    I have reduced this example a little further:
    [code] 
        module Main exposing (..)
        
        import Html exposing (text)
        
        
        type alias NameAndId =
            WithId { name : String }
        
        
        type alias WithId a =
            { a | id : String }
        
        
        type WrapWithId a
            = WrapWithId (WithId a)
        
        
        main =
            let
                mm : NameAndId
                mm =
                    { id = "", name = "" }
        
                t1 : WrapWithId NameAndId
                t1 =
                    WrapWithId mm
            in
            text ""
        
    [/code]
    
    I have renamed some things:
    
      * `MyOther` -> `WithId`
      * `MyOtherMsg` -> `WrapWithId`
      * `MyThird` -> `IdAndName`
      * `Other1` -> `WrapWithId`
    
    
    
    Notice, that you're wrapping `MyOther` twice, kind of. Would using `MyOtherMsg { name : String }` instead of `MyOtherMsg { id : String, name : String }` work for your usecase?
    
    In general I'd say: Stay away from extensible records as often as you can. You can also just enforce that your table rows have `id`s by wrapping the records, instead of extending fields:
    [code] 
        type alias TableRow content =
            { id : String
            , content : content
            }
        
    [/code]

@jako (Jacob) replied

    Okay! I had thought that 3 files were necessary to replicate the problem. If I modify `NameAndId` as you suggest - which would work for what I am doing - I still get the error:
    [code] 
        module Main exposing (..)
        
        import Html exposing (text)
        
        
        type alias NameAndId =
            -- WithId { name : String }
            { id : String, name: String }
        
        
        type alias WithId a =
            { a | id : String }
        
        
        type WrapWithId a
            = WrapWithId (WithId a)
        
        
        main =
            let
                mm : NameAndId
                mm =
                    { id = "", name = "" }
        
                t1 : WrapWithId NameAndId
                t1 =
                    WrapWithId mm
            in
            text ""
        
    [/code]
    
    Using a wrapper would be painful in the context of what I am doing, because I frequently need to be able to modify the records.

@Philipp_Krueger (Philipp Krüger) replied

    I ment my suggestion more along the lines of this:
    [code] 
        module Main exposing (..)
        
        import Html exposing (text)
        
        type alias IdAndName =
            WithId { name : String }
        
        type alias WithId a =
            { a | id : String }
        
        
        type WrapWithId a
            = WrapWithId (WithId a)
        
        
        main =
            let
                mm : IdAndName
                mm =
                    { id = "", name = "" }
        
                -- this signature is the crucial part
                t1 : WrapWithId { name : String }
                t1 =
                    WrapWithId mm
            in
            text ""
        
    [/code]
    
    (Sorry for this `NameAndId` and `IdAndName` confusion)

Then @jako (Jacob) said

    You are right. The code compiles with the modified signature. And the compiler allows me to access the `id` field on the record inside `t1`:
    [code] 
        main =
            let
                mm : NameAndId
                mm = Debug.log "mm" <| { id = "id", name = "name" }
        
                -- t1 : WrapWithId NameAndId
                t1 : WrapWithId { name : String }
                t1 = Debug.log "t1" <| WrapWithId mm
            in
            case t1 of
              WrapWithId m2 ->
                let
                  m3 = Debug.log "m3" m2
                in
                text m2.id
        
    [/code]
    
    Surely this must be a an error in the compiler.

To which @jako (Jacob) replied

    Related GitHub issue: https://github.com/elm/compiler/issues/2071

Then @dmy said

    I don't think the related GitHub issue #2071 is a bug, some comments have been added since to explain why.
    
    Also for your last example, if you remove all the aliases, you get:
    [code] 
        module Main exposing (..)    
            
        import Html exposing (text)    
            
            
        type WrapWithId a    
            = WrapWithId { a | id : String }    
            
            
        main =    
            let    
                mm : { id : String, name : String }    
                mm =    
                    { id = "", name = "" }    
                                                           
                t1 : WrapWithId { id : String, name : String }    
                t1 =    
                    WrapWithId mm    
            in    
            text ""   
        
    [/code]
    
    The `WrapWithId` constructor takes a record `a` with at least an `id : String` field and returns an expression whose type is `WrapWithId a`.
    
    You pass it a `{ a | id : String }`, it returns a `WrapWithId a`.  
    It does not return an expression with type `WrapWithId { a | id : String}`.
    
    So when you pass it a `{ id : String, name : String }`, it returns a `WrapWithId { name : String }`.
    
    * * *
    
    You can name the constructor differently to better understand:
    [code] 
        type WithId a = RequiresId { a | id : String }
        
    [/code]
    
    If you call `RequiresId` with a `{ id : String }`, you get a `WithId {}`.  
    Let's test in the repl:
    [code] 
        > type WithId a = RequiresId { a | id : String }
        
        > RequiresId { id = "" }
        RequiresId { id = "" } : WithId {}
        
    [/code]
    
    So if you call it with a `{ id : String, name : String }`, you get a `WithId { name : String }`:
    [code] 
        > RequiresId { id = "", named = "" }
        RequiresId { id = "", named = "" }
            : WithId { named : String }
        
    [/code]
    
    * * *
    
    Therefore your type in your example must be:
    [code] 
                t1 : WrapWithId { name : String }    
                t1 =    
                    WrapWithId mm    
        
    [/code]

To which @jako (Jacob) replied

    I must disagree. It is a bug. To see why, look at the following in the Elm REPL:
    [code] 
        > type alias NameAndId = { id : String, name : String }
        > type alias WithId a = { a | id : String }
        > x : WithId NameAndId
        | x = { id = "id", name = "name" }
        |
        { id = "id", name = "name" }
            : WithId NameAndId
        > y : NameAndId
        | y = { id = "id", name = "name" }
        |
        { id = "id", name = "name" } : NameAndId
        > x == y
        True : Bool
        
    [/code]
    
    Two records cannot compare true, unless they have the same type, i.e. `NameAndId` and `WithId NameAndId` are one and the same type.
    
    If I now define
    [code] 
        type Msg a
            = WrapWithId (WithId a)
        
    [/code]
    
    The REPL accepts the following two definitions:
    [code] 
        > u = WrapWithId x
        WrapWithId { id = "id", name = "name" }
            : Msg NameAndId
        > v = WrapWithId y
        WrapWithId { id = "id", name = "name" }
            : Msg { name : String }
        
    [/code]
    
    but then complains when I compare `u` and `v` (mind you `x` and `y` compared True):
    [code] 
        > u == v
        -- TYPE MISMATCH ---------------------------------------------------------- REPL
        
        I need both sides of (==) to be the same type:
        
        23|   u == v
              ^^^^^^
        The left side of (==) is:
        
            Msg NameAndId
        
        But the right side is:
        
            Msg { name : String }
        
        Different types can never be equal though! Which side is messed up?
        
        Hint: Seems like a record field typo. Maybe id 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!
        
    [/code]
    
    An simpler definition of the `Msg` type does not provoke this type of error:
    [code] 
        > type Msg2 a = Wrap a
        > u2 = Wrap x
        Wrap { id = "id", name = "name" }
            : Msg2 (WithId NameAndId)
        > v2 = Wrap y
        Wrap { id = "id", name = "name" }
            : Msg2 NameAndId
        > u2 == v2
        True : Bool
        
    [/code]

Then @dmy said

    I see. There is indeed an inconsistency.
    
    Another way to illustrate it is:
    [code] 
        type alias Named a = { a | name : String }    
            
        x : Named { name : String }    
        x = { name = "" }    
            
        y : Named {}    
        y = { name = "" }    
            
        z : { name : String }    
        z = { name = "" }  
        
    [/code]
    
    Then
    [code] 
        > x == z
        True : Bool
        > y == z
        True : Bool
        > x == y
        -- TYPE MISMATCH ---------------------------------------------------------- REPL
        
        I need both sides of (==) to be the same type:
        
        4|   x == y
             ^^^^^^
        The left side of (==) is:
        
            Named { name : String }
        
        But the right side is:
        
            Named {}
        
        Different types can never be equal though! Which side is messed up?
        
        
    [/code]

@jako (Jacob) replied

    I like that. Your example is even simpler. Can I use it to raise an issue on Github?

Then @dmy said

    I have already opened a new issue (because this is the preferred way to introduce new a SSCCE):  
    github.com/elm/compiler
    
    ####  Inconsistent types comparisons between records and extensible records
    
    opened 11:33AM - 24 Apr 20 UTC
    
    rlefevre 
    
    Quick Summary: There are some inconsistencies when comparing records types and records types built from extensible type aliases. SSCCE module Bug exposing (x, y,...

Then @dmy said

    Thank you for insisting @jako , this has lead to a short and indisputable example of the issue.  
    I have added a note in the GitHub issue to point to this thread.

After which, @brian (Brian) added

    Yes, thank you! I feel I was not actually that informative above and I'm glad you persisted here!

To which @jako (Jacob) replied

    In your experience, how long does it usually take before such a bug gets fixed? I am wondering whether to wait for a fix to arrive, attempt to redesign my application, or to give up on implementing my project in Elm.

So @dmy replied

    I would not rely on waiting for a fix, this could take some time, even years, particularly for such an issue that seems to be solvable by signatures changes.
    
    So I struggle to see how this could block you, I thought that the signature change solved your issue (didn't you set the post as “solved”?). Do you have an example of the code that you cannot change to make it work with a different signature?
    
    Also to go back to the first @brian post, designing types around extensible records in Elm is often a mistake better solved by another design. Extensible records are mainly useful in Elm for narrowing the types of functions, not to model the problem domain. Maybe give a try at another modeling before giving up, I'm sure a lot of folks would help if you give us more information about what problem you are trying to solve.

@jako (Jacob) replied

    My application is a data science application that displays several interactive tables in a SPA. Each table has different columns with possibly different data types. An earlier version of the app used dwyl/elm-input-tables, but that package has not been updated for Elm 0.19, and I had to hack it in too many places to suit my needs, so I decided to roll my own table module.
    
    I have separated all the table logic out into a Table.elm containing a model, a view function and an update function. In my main application I configure each table separately, and messages to each table are wrapped, so the main update function looks like
    [code] 
        case msg of
          Table1Wrap m ->
            { model | table1 = Table1.update m }
          Table2Wrap m ->
            { model | table2 = Table2.update m }
          ...
        
    [/code]
    
    The type of the messages to each table depend on the record corresponding to a row in the table. And this is where my problem arise. In my Table.elm a row record is an extensible record
    [code] 
        type alias Row a = { a | id : String }
        
    [/code]
    
    @Philipp_Krueger suggested in one of his posts that I used a row wrapper record type. I initially resisted this, because it would complicate updating the rows (now nested records), as per e.g. this.
    
    I marked @Philipp_Krueger's post as a solution because his code was able to compile with the signature change, but it does not solve the fundamental deannaprice@example.org. It does not suffice for my needs. Maybe I should unmark it as the solution?

So @dmy replied

    Ok, could you add the minimum required to make this skeleton based on your description representative of your issue?
    
    https://ellie-app.com/8HRLD8YZvVpa1

Then @francescortiz (Francesc Ortiz) said

    I would unmark the solution, because this problem is very persistent.
    
    We use atomic design at work, so we have a lot of records wrapping records honoring the atomic design structure. We have had to hack this many times, and it is impossible to know when we discarded a smart approach to a need because this compiler bug hinted us in the wrong direction. I assumed that this was a commonly known limitation of the elm compiler because the discrepancy becomes obvious if you use intellij-elm: it uses its own parser that does not have the bug and thus errors do not match when this arises.

Then @jako (Jacob) said

    Thank you, @francescortiz. I have unmarked @Philipp_Krueger's answer as the solution, but I would still like to recognise him as the person who helped me understand the core of the problem. I hope this bug gets fixed soon.

To which @jako (Jacob) replied

    At the moment I am trying to refactor my code, to see if a solution involving nested records is feasible. If I run into difficulties, I will give the Ellie a shot instead.

To which @dmy replied

    @francescortiz Have you reported a bug by the past? It seems that this issue has not been clearly reported, likely because not much people design their types around extensible records due to their limitations.
    
    As an aside, if you have found a suitable and useful use case for extensible records, it would be interesting to share it.
    
    @jako Yeah, if you get stuck, and share a minimal example with the Ellie, maybe we can help finding another solution.

@jako (Jacob) replied

    Have you written anything about atomic design in Elm, or do you have any good links to share? What Elm patterns are you using to make it work?

@Philipp_Krueger (Philipp Krüger) replied

    I don't know why I didn't think of mentioning it earlier:  
    In case anyone wants to isolate code that triggers elm compiler bugs, take a look at my project, elm-reduce.  
    You provide it with a shell-file that tests whether the compiler output contains the bug and tries to remove code irrelevant to the bug in a step-by-step fashion.
    
    I have let it bit-rot a little, because I'm missing newer compiler bug examples and use-cases recently (0.19.1 has been great :D). **Please contact me if you want to use elm-reduce**. I'd love to pick it up again and update it correctly to 0.19.1.

Then @francescortiz (Francesc Ortiz) said

    I haven written about it. But the post is about to expire, so I am going to to make a short explanation. With atomic design (https://bradfrost.com/blog/post/atomic-web-design/)8b7c:f320:99b9:690f:4595:cd17:293a:c069
    
      * We have simple atoms that don't require full Elm understanding. Good for a growing team.
      * Molecules and organisms can be stateful or stateles.
      * We use the OutMsg pattern for child to parent communication. I don't think wi will ever need parent to child communication, since this is just about rendering. We have made a library based on elm-reply, but with support for more than one reply and I think simpler and more explicit api.
      * Templates are also kind of simple, since they just put pieces together and proxy the data to each of the components of the template.
    
    
    
    Then, since all that was just rendering, regarding application structure, we have for layers:
    
      * Application: Takes care of the usual: session (or context), routing, etc.
      * APIs: Make the queries and provide functions to process the data. The goal is that when we do repaints as much as possible has been calculated in the update function (we do a lot of calculations per API call).
      * Page: It is a controller. It handles all the side effects and feeds the Template.
      * Template: Explained before.
    
    
    
    Benefits:
    
      * It is very easy to split the tasks.
      * For each task, the required knowledge base is scoped.
      * Re-usability.
    
    
    
    I think this is a good enough quick overview.

To which @system (system) replied

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


{'source': 'discourse', 'url': 'https://discourse.elm-lang.org/t/compiler-trips-up-on-extensible-records/5623', 'title': 'Compiler trips up on extensible records?', 'views': 1381, 'has_accepted_answer': False, 'word_count': 3002, 'like_count': 14, 'reply_count': 17, 'participant_count': 5, 'posts': 27, 'tags': '', 'author': '@jako (Jacob)', 'participants': '@jako (Jacob), @brian (Brian), @Philipp_Krueger (Philipp Krüger), @dmy, @francescortiz (Francesc Ortiz), @system (system)', 'image_url': [None], 'created_at': '2020-04-23T14:43:31.274Z', 'last_posted_at': '2020-05-21T06:17:06.909Z'}