Destructuring records in Elm with "as" word

Viewed 64

I'm doing the Elm exercises in Exercism again and there's a thing unclear to me so far. How does the "as" destructuring work? In the beginning I didn't understand anything. After read Yang Wei's Elm destructuring (or pattern matching) cheatsheet, more specifically this part:

myRecord = { x = 1, y = 2, z = 3}

computeSomething ({x, y} as wholeRecord) =
    -- x and y refer to the x and y fields of the passed in record
    -- wholeRecord is the complete record
    -- i.e. x and wholeRecord.x refer to the same field
    -- but z is only accessible as wholeRecord.z

A lot of things became more cleat to me. The problem happened when I tried to make it by myself. So, the Ticket, Please! problem imports three types, called Ticket, Status and User and I should write a functions that receives a ticket and an user and returns a modified ticket, as the given example:

assignTicketTo (User "Danny")
    (Ticket
        { status = New
        , createdBy = ( User "Jesse", 3 )
        , assignedTo = Just (User "Alice")
        , comments = [ ( User "Jesse", "I've been waiting for 6 months!!" ) ]
        }
    )
-- => Ticket
--        { status = InProgress
--        , createdBy = ( User "Jesse", 3 )
--        , assignedTo = Just (User "Danny")
--        , comments = [ ( User "Jesse", "I've been waiting for 6 months!!" ) ]
--        }

The big problem comes here, because after descruture the Ticket variable and use as to attribute it to a different variable (as below), the language tells me that the new variable has a different type, i.e. it is not a Ticket.

assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user ({ status, assignedTo } as ticket) =
    if (status == New) then
        { ticket | status = InProgress, assignedTo = Just user }
    else
        ticket

What am I doing wrong? I read the answer about Type mismatch when trying to destructure type in Elm, but the only thing that I can imagine I could possibly be doing wrong, the desctructuring, is not correct either:

assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user (Ticket { status, assignedTo } as ticket) =
    if (status == New) then
        { ticket | status = InProgress, assignedTo = Just user }
    else
        ticket

Edit: The error messages are:

This is not a record, so it has no fields to update!

45|         Ticket { ticket | status = InProgress, assignedTo = Just user }
                     ^^^^^^
This `ticket` value is a:

    Ticket

But I need a record!

and

The 2nd argument to `assignTicketTo` is weird.

43| assignTicketTo user ({ status, assignedTo } as ticket) =
                         ^^^^^^^^^^^^^^^^^^^^^^
The argument is a pattern that matches record values of type:

    { c | assignedTo : a, status : b }

But the type annotation on `assignTicketTo` says the 2nd argument should be:

    Ticket
1 Answers

The problem here is that Ticket is not a record type. It's a custom type with a single variant containing a record. Its definition (which is essential context that you should have posted in the question) is:

type Ticket
    = Ticket
        { status : Status
        , createdBy : ( User, Int )
        , assignedTo : Maybe User
        , comments : List ( User, String )
        }

To destructure this you have to first unwrap the custom type, then unwrap the record and give just that a name:

Ticket ({ status, assignedTo } as ticket)

And then to return a Ticket again you also have to wrap the record back up in the Ticket constructor after updating it. The full working function therefore is:

assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user (Ticket ({ status, assignedTo } as ticket)) =
    if status == New then
        Ticket { ticket | status = InProgress, assignedTo = Just user }

    else
        Ticket ticket

Why the Ticket type has been designed in this way I don't know. It seems unnecessarily convoluted and invites confusion. So please don't feel too bad about that!

Related