An init function that initializes nothing and how to initialize with hidden types?


#1

I’m confused by the init function in 0.19 .

  1. Assuming everything is ‘empty’ when a web page is first loaded/reloaded (big assumption?), can I have an init function that initializes nothing given that its type definition ends -> ( Model, Cmd Msg) ?

In other words, what’s the equivalent of Cmd.none for the model?
(I think part 2 helps to explain why I’m asking this)


  1. If I’m using a package like elm/file which does not expose it’s File type, how can I initialize the model with a ‘null’/empty File?

Let’s say we have,

type alias Model =
    {
         file : File
    ,    fileList : List File
    ,    text : String
    }

In init we can say fileList = [] and text = "", but what about file?
I tried simply file = File, but the compiler says,

I cannot find a `File` constructor:
64|   ( { file = File, files = [], text = "" }, Cmd.none )  

and, file = File.File gives,

The `File` module does not expose a `File` constructor  

So we know that File isn’t a record. But this doesn’t help much with init.


#2

If you don’t need to have a file when the program starts, you could use file : Maybe File to initialize without it, setting file = Nothing. Then you can set the file-state once you have it :sunny:

Another way would be to turn your Model into a custom-type - maybe like this:

type Model
    = NoFile
        {}
    | WithFile 
        { file : File
        , fileList : List File
        , text : String
        }

Let me know if I’ve misunderstood something :slight_smile:


#3

@opvasger already mentioned the two options I’d also consider.

I just want to add that it’s not possible to not initialize your Model - you need to design your Model so that it can have an initial value which makes sense.


#4

@opvasger and @malaire, many thanks. I’m sorry for the delay in replying, I’ve been playing with your suggestions (in a sea of interruptions).

@malaire — it’s good to know that Elm requires full initialization of the Model. A much more stable approach than what I was trying to do :slight_smile:

@opvasger,

Using Maybe looks good but it means (I think) that all the functions that consume it must be modified to handle a Maybe input. When I got to the point where I was trying to put a Maybe (Task) into a package function requiring simply a Task, I gave up. @mezuzza’s StackOverflow answer to Right way to forcibly convert Maybe a to a in Elm, failing clearly for Nothings has me confused/concerned.

The ‘Model with a custom type’ solution looked simpler. It required a couple of let...in constructs here and there to destructure the model, and for a while the refactoring was going well, until I got to the following:

type Model
    = NoFiles
        {}
    | WithFiles
        { file : File
        , fileList : List File
        , text : String
        }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...
        TextLoaded string ->  
            let
                [something in here to get the model record]  
            in
                ( WithFiles { file = record.file
                            , fileList = record.fileList
                            , text = string ++ record.text 
                            }
                            , Cmd.none 
                )

To update the model I need to know it’s current values/state. So I need to extract the record payload from the Model’s Constructor {} structure. And that’s when the fun starts. In a nutshell, how?

I can destructure the model with expressions like Constructor _ or Constructor a and use this in case statements where it’s the Constructor that is relevant. But here I want to do it the other way around: the bit I want to ‘ignore into a type variable’ is the Constructor part, so that I can do something with the record.

In place of [something in here to get the model record] I tried

    ( WithFiles record ) = model
    ( NoFiles nothing ) = model

but the compiler complains that each line doesn’t account for the other situation.
It doesn’t like ( ignoreThis record ) = model either.

I then tried

record =
    case model of
        NoFiles empty ->
            {}
        WithFiles someFiles ->
            someFiles

which seemed very reasonable until I got the error message:

This `someFiles` value is a:
    { file : File, fileList : List File, text : String }
But all the previous branches result in:
    {}

Of course I might try replacing {} with a complete record with empty values, but then I’m right back round to where I started because I’m unable to set file to an ‘empty’ File because I don’t have access to File as a constructor.

I’m currently investigating restructuring Model using perhaps nested union types and type variables…

In the mean time, if someone could show me the way out of my self-created labyrinth, that would be very much appreciated.


#5

What should happen if you get the TextLoaded message while in the NoFiles state? You mention needing the “current values/state” but as written there are no values associated with the NoFiles state.

For example, does the text field actually exist in both the NoFiles and WithFiles case? Then you could have

type Model
    = NoFiles
        { text : String }
    | WithFiles
        { file : File
        , fileList : List File
        , text : String
        }

and for [something in here to get the model record] you could use something like

currentText =
    case model of
        NoFiles { text } ->
            text

        WithFiles { file, fileList, text } ->
            text

which if you wanted to you could factor out into a helper function

getCurrentText : Model -> String
getCurrentText model =
    case model of
        NoFiles { text } ->
            text

        WithFiles { file, fileList, text } ->
            text

#6

Happily, there are ways out of that! Write those functions to work on File. Then use Maybe.map to apply them to your Maybe File.

@joelq has a great article on this


#7

I think you are really close to the exit of your labyrinth. Update your model only when your model matches to the relevant custom type, otherwise just return the original model.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...
        TextLoaded string ->  
            case model of
                WithFiles someFiles ->
                  ( WithFiles { someFiles | text = string ++ someFiles.text }, Cmd.none )
                _ ->
                  ( model, Cmd.none )

Also the record of NoFiles is not necessary since it is empty anyway, so you can write your Model like

type Model
    = NoFiles
    | WithFiles
        { file : File
        , fileList : List File
        , text : String
        }

and the init function becomes something like this.

init : ( Model, Cmd Msg)
ini = 
    ( NoFiles, Cmd.none )

#8

@ianmackenzie

Thank you. The idea of there being text before a file load — maybe from a previous file load — has provided good food for thought.

In theory — and probably in practice given that this is Elm — that shouldn’t happen: the function that generates the TextLoaded message is called by the FilesLoaded branch of update; no files loaded, no text loaded.

But this doesn’t mean that there isn’t an initial condition of NoFiles — and no text.


@Brian_Carroll

Excellent article, thank you for the link.


@kyasu1

Brilliant. That works. Thank you muchly.

In a nutshell, destructure the model with a subordinate case
— which I have now also used successfully elsewhere in the code :slight_smile: