Converting types

Hi!

I’m a little bit stuck and would appreciate some help. I’m writing an Elm application which has a somewhat large model, part of which is optional (it holds some configuration values). Basically it looks like this:

type alias Config =
    { color : String }


type alias MyModel =
    { some : Bool
    , other : String
    , config : Maybe Config
    }

My view function waits until the configuration field appears before doing much of anything:

view obj =
    case obj.config of
        Nothing ->
            Html.text "No config"

        Just cfg ->
            Html.text cfg.color

So far so good. However, as the application has been growing, the view function has also grown and now consists of dozens of helper functions. Whenever these functions need to use a config value, they need to figure out if there is a config or not:

view obj =
    case obj.config of
        Nothing ->
            Html.text "Waiting for config"

        Just cfg ->
            Html.text ("The color is " ++ color obj)


color obj =
    case obj.config of
        Nothing ->
            ""

        Just cfg ->
            cfg.color

This doesn’t make a whole lot of sense since I already know that I won’t be calling this function unless I have a non-Nothing configuration.

So what I’ve done is add an additional parameter to all functions that need a config:

view obj =
    case obj.config of
        Nothing ->
            Html.text "Waiting for config"

        Just cfg ->
            Html.text ("The color is " ++ color obj cfg)


color obj cfg =
    cfg.color

This works fine, but it seems a bit redundant to me – the config value gets passed twice, once as the Maybe argument in obj and once explicitly. So I’ve tried to come up with a solution that uses type aliases like this:

type alias Configured a =
    { a | config : Config }

But from what I can tell I now need to manually convert my MyModel object to a Configured MyModel object every time view is called which seems a bit silly.

Is there a more elegant solution for this? Something like a way to say “Use this record, but add a config value to it”? Or am I doing something fundamentally wrong?

Is the config a Maybe because it’s loading or for some other reason? If it’s because it’s loading you could have 2 models, an unloaded model and a loaded model.


You could also have a function like

withConfig : (Config -> Html msg) -> Maybe Config -> Html msg
withConfig fn maybeConfig =
    case maybeConfig of
        Nothing -> Html.text "Waiting for config"
        Just config -> fn config


view model =
    withConfig
        (\config -> Html.text ("The color is " ++ color model.someValue config)
        model.config

Exactly, it’s loading asynchronously.

I tried that. It did make my view code much nicer indeed, but my update function became quite a mess because now I had to be able to update two different types of models (and unfortunately my update function is rather complex).

That does seem more elegant but I think I would still need to pass the config as an extra parameter in there are further helper functions, wouldn’t I?

If the config is needed for every part of the view and is mostly readonly, you might consider using
https://package.elm-lang.org/packages/miniBill/elm-html-with-context/latest/

This allows you to pass the config in the Just case and then pull it out somewhere deep in the views html.

Side note/example: I’m using this mechanism for internationalization because passing translations down manually would be cumbersome.

Another option to reduce the feeling of redundancy is to pattern match in the function signature so you only expose some fields:

view obj =
    case obj.config of
        Nothing ->
            Html.text "Waiting for config"

        Just cfg ->
            Html.text ("The color is " ++ color obj cfg)


color { relevantField } cfg =
    if relevantField cfg.color else cfg.defaultColor

In this setup, the config is technically passed twice (because the whole object is passed), but the type system ensures that you only have access to the specified field, which eliminates any question of which version of the config you should access within the function.

But from what I can tell I now need to manually convert my MyModel object to a Configured MyModel object every time view is called which seems a bit silly.

I don’t think that sounds silly at all, to be honest. I think it sounds very reasonable, from the information you gave so far :slight_smile: Can you give a full example to show where exactly the problem in this approach can be found?

Sure! My view currently look something like this:

view : MyModel -> Html msg
view model =
    case model.config of
        Just cfg ->
            configuredView (configured cfg model)

        Nothing ->
            text "Nothing"

To convert the original object to a configured one, I use a type alias and a function like this one:

type alias Configured a =
    { a | cfg : Config }

configured : Config -> MyModel -> Configured MyModel
configured config obj =
    { some = obj.some
    , other = obj.other
    , config = Just config
    , cfg = config
    }

With that, my view helper functions can rely on having a configured object:

configuredView : Configured MyModel -> Html msg
configuredView obj =
   text obj.cfg.color

While this does look nicer to me, it needs to call the configured function every time the view is refreshed, trading runtime efficiency for a sense of beauty. I’m not convinced I like that.

In Elm, the view function usually creates hundreds of objects every time, by design. Calling div [] [] already created one object! A handful of custom objects more will not affect the performance. Your Browsers Garbage Collection System even has a special optimization for small short-lived objects.

So, I really doubt this will make any measurable impact on performance. If you worry about such tiny performance impacts, you will hinder yourself from writing good code. Therefore, the common advice is: At the point when your app is actually perceivable slow, use a profiler to measure where exactly the slowness comes from. It’s almost never where we think it is.

Your solution is beautiful, simple, and appropriate for what you’re doing. The other answers are way over the top, too complex (and likely even creating more temporary objects than your current code). This is the good solution and I have successfully done similar things in the past, and it always worked perfectly. So, try the beautiful way first, and most of the time, it’s not even that slow in practice. You already had the best solution in your head! :slight_smile:

1 Like