I’ve written before regarding the conflict between using Elm’s Html.lazy and extensible records. Today I wish to highlight a similar mild conflict between Elm’s Never type and Html types that forces an unnecessary Html.map. To explain this though I may as well briefly explain the Never type in Elm, and how it is used in the context of the Html type. So I will start with that, and then I’ll show why the type system forces us to call Html.map in a situation where it’s not at all necessary.

Polymorphism, or better, generics, allows us to write functions over containers where we don’t really much care about the type of the elements of the container. So it’s straightforward to write a reverse function that can reverse a list of any type, so a list of integers, or a list of calendars, or bicycles whatever.

reverse : List a -> List a
reverse l = 
    case l of
        [] -> []
        x :: xs -> List.append (reverse xs) [x]

You could also use this to force a list to be empty because you are forcing it to be a list of any type:

emptyList : List a
emptyList =
    []

Note, that no other value would satisfy the type List a because if the list is not empty the values must have some type. You can also do this with the Never type, since there are no values that are of type Never the only list that can have type List Never is the empty list:

emptyList : List Never
emptyList =
    []

You can also accept either in as an argument to a function, this first one will accept any list, not just the empty list, and will turn all elements into 0.

appendNothing : List a -> List Int
appendNothing l =
    List.append (List.map (\_ -> 0) l) [1,2,3]

If you wanted to make sure that the list was empty you could use the Never type to enforce that:

appendNothing : List Never -> List Int
appendNothing l =
    List.append (List.map (\_ -> 0) l) [1,2,3]

Note though, that because Elm doesn’t know that List Never forces the list to be empty, you still have to call List.map even though the result will be the empty list.

Why would anyone wish to do this? Of course they would not, since the only value that satisfies List Never is the empty list, so it doesn’t carry any information, so you could always just avoid passing that in as a value at all. However, not all containers are the same. Some may carry information besides the elements it contains, and hence may contain information even when ‘empty’. In Elm we have the Html msg type, where the msg is the type of messages that could be sent to the Elm runtime when the user interacts with the rendered HTML. So here is the clever bit, Html with the type Html Never is Html that has been generated that cannot emit any messages at all. It could be as simple as Html.text "Hello world", but it might be, for example, your entire About page:

aboutContent : Html Never
aboutContent =
    ... 100 lines of complex Html.div and family

You could of course similarly write this with the type Html msg and the only kind of Html that could be produced would be one that doesn’t fix the message type, that is Html that cannot emit any messages.

aboutContent : Html msg
aboutContent =
    ... 100 lines of complex Html.div and family

You can see how this is much more useful than an empty list, because there are many Html values that do not emit messages.

Now, why might you wish to guarantee that a certain piece of Html cannot emit messages? One reason is that you’re going to wrap that in some other Html that will emit messages. Here is how you might do that using a simple any type Html msg:

renderDialog : Html msg -> Html Msg
renderDialog dialogContent =
    Html.div 
        [ Html.Attributes.class "non-interactive-dialog" ]
        [ Html.map (\_ -> Msg.Ignore) dialogContent
        , Html.button [ Html.Events.onClick Msg.CloseDialog ] [ Html.text "Close" ]
        ]

Now the type of this does not guarantee that the dialogContent cannot emit messages. Any part of the Html that does emit messages will be mapped to Msg.Ignore. But there are two things that are dis-satisfying with this definition:

  1. Type of dialogContent is not guaranteed to emit no messages
  2. Assuming that it does not emit messages the Html.map call will trawl over the entire dialogContent even though there are no messages to map.

We can get around the first of these by using Html Never, what we’d like to write is:

renderDialog : Html Never -> Html Msg
renderDialog dialogContent =
    Html.div 
        [ Html.Attributes.class "non-interactive-dialog" ]
        [ dialogContent
        , Html.button [ Html.Events.onClick Msg.CloseDialog ] [ Html.text "Close" ]
        ]

So now dialogContent is forced, by the type Html Never, to not emit any messages. However, this doesn’t compile. The reason is the same reason above we were forced to call List.map on the empty list, namely that Html Never is not compatible with Html Msg. So unfortunately this forces us to call Html.map even though we know the dialogContent will not be changed by the call to Html.map because we know it does not contain any messages:

renderDialog : Html Never -> Html Msg
renderDialog dialogContent =
    Html.div 
        [ Html.Attributes.class "non-interactive-dialog" ]
        [ Html.map (\_ -> Msg.CloseDialog) dialogContent
        , Html.button [ Html.Events.onClick Msg.CloseDialog ] [ Html.text "Close" ]
        ]

We have managed to get rid of Msg.Ignore since we can use any Msg since we are guaranteed that there are no messages to map. Still it means that the program will trawl over the entire dialogContent searching for messages to map even though we have used the type system to prove that it will not find any.

So an improvement in the compiler for Elm would either:

  1. Allow Html Never to be compatible with Html msg for any msg, or
  2. Turn any calls to Html.map where the second argument is of type Html Never into a no-op.

The first one would allow us to write the renderDialog function the way we ideally wish to without the use of Html.map. However, it feels like a special case for the type system to deal with, whereas the second one is basically just a compiler optimisation, it doesn’t change the language, only the implementation of the compiler. For that reason, I think I prefer the second option. Unfortunately if I understand the source of the compiler correctly this would not be trivial at all. The optimiser phase does not have access to the types of sub-expressions. It does have access to the type signatures of top-level definitions so you could possibly optimise the “easy cases” where basically the second argument to Html.map is (as in the above example) a straightforward reference to an argument of a top-level definition. But I think if you were going to add this optimisation it would be worth restructuring the compiler so that sub-expression type information was available to the optimiser because that (probably) opens up a lot of other optimisations that could be worth it.