Help with unpacking of custom types

Hi,
I try to implement some lazy loading in my application and came up with the following (simplified) data structure:

type Item
    = LazyItem
        { id: Int
        , title: String
        }
    | LoadedItem
        { id: Int
        , title: String
        , detailData: String -- expensive to load
        }

Now of cause there are some functions that’ll can only work on a LoadedItem (as they need the detailData, but there are a lot of functions that could work on either variant. My listViewItem function for example could look something like this:

viewListItem: Item -> Html msg
viewListItem x =
        case x of
            LazyItem item ->
                tr [] [
                    td [] [text item.id],
                    td [] [text item.title]
                ]
            LoadedItem item ->
                tr [] [
                    td [] [text item.id],
                    td [] [text item.title]
                ]

As you can see, the case for either variant is exactly the same. How would I rewrite this function so it can handle both cases without repeating myself? I already came up with

viewListItem: Item -> Html msg
viewListItem x =
        let
            row : { a | id : Int, title : String } -> Html msg
            row item =  tr [] [
                        td [] [text <| String.fromInt item.id],
                        td [] [text item.title]
                    ]
        in
            case x of
                LazyItem item ->
                    row item
                LoadedItem item ->
                    row item

which is a little bit better, but still a lot of glue code to write. And if I ever want to add a third variant to the Item-type (which also has an id and title), I’d have to edit all functions that work with that type and add another case.
Is there some way to get rid of the case x of and get a function that can work on all Items that have the required fields?

I’d expect something like this to work, but I couldn’t figure out the syntax:

viewListItem: Item { a | id : Int, title : String } -> Html msg
viewListItem Item item =  tr [] [
                        td [] [text <| String.fromInt item.id],
                        td [] [text item.title]
                    ]
1 Like

Maybe

type alias Item
    =  { id: Int
        , title: String
        , lazyDetailData : Maybe String -- or another data type for this
        }

and then you can use type narrowing as you are trying to do?

1 Like

Can you show us the function(s) which is calling viewListItem?

You said that you have a lot of functions that could work on either variant, can you show such a function too? Because viewListItem actually only needs ListItem.

In case you have a list view were the LazyItem is used and a detail view were the LoadedItem is used, and you don’t have a lot of function which can operate on both, then I would use separate data types.

type alias ListItem = { id, title, aso... }

-- or

type ListItem = ListItem ListItemData

type alias ListItemData = { id: Int, title: String }

and a separate type for Item (or DetailItem):

type Item = Item ItemData

type alias ItemData = { id: Int, title: String, data: String }

Then you have to case x of less.

We use a slightly advanced Elm type modelling feature called Phantom types.


type Loaded = Loaded
type NotLoaded = NotLoaded

type Item isLoaded = 
   Item {id: Int
        , title: String
        , detailData: String -- expensive to load
        }

Then you can define accessor functions like this:


-- works on both loaded and unloaded
id : Item any -> Int
id (Item item) = item.id

-- so does this
title : Item any -> String
title (Item item) = item.title

-- only works on loaded items
detailData : Item Loaded -> String
detailData (Item item) = item.detailData

So then you can refactor your view function like this:

viewListItem: Item any -> Html msg
viewListItem item =
             tr [] [
                    td [] [text (id item)],
                    td [] [text (title item)]
                ]   

although I would usually put that data structure in a separate module, so the view code would often look more like

viewListItem: Item any -> Html msg
viewListItem item =
             tr [] [
                    td [] [text (Item.id item)],
                    td [] [text (Item.title item)]
                ]   
3 Likes

@ni-ko-o-kin
My idea was to store all items (they are actually web-pages), loaded and not yet loaded in the same Dict and wherever I need a list of those Items (table for the list view, menus, sitemap etc.) I would map them to generate the required view:

viewList : Model -> Html Msg
viewList model =
    table [] (model.pages
        |> Dict.values
        |> List.map viewListItem)

the detailView would of cause need to handle both cases separately:

viewDetail : Model -> Html Msg
viewDetail model = case (Dict.get model.currentPage model.pages) of
    Just a ->
        case a of
            LazyItem record ->
                showLoadingSpinner
            LoadedItem record ->
                showDetailView record
    Nothing ->
        showPageNotFound

The Phantom-Types @gampleman suggests look promising. I think that’s pretty much what I was looking for.
Thank you all for your feedback.

2 Likes

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