How to do a long running calculation in Elm?

This program behaves a little strange to my thinking:

https://ellie-app.com/96fMBHvP2a1/0

Its behaviour is driven by this simple update loop:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case (Debug.log "update" msg) of
        Count val ->
            if model.count < 10 then
                ( { model | count = val }, message <| Count (val + 1) )
            else
                ( model, Cmd.none )

The function message is:

message : msg -> Cmd msg
message x =
    Task.perform identity (Task.succeed x)

And it prints the correct answer to the DOM – 10.

However, look at the console and see what is logged:

...
update: Count 10
update: Count 11

This means the update function is run 1 time more than I would expect. On Count 10 the else branch is followed, and Cmd.none is returned. But a Count 11 is still run.

Do both of a set of if and else branches get eagerly evaluated in Elm?

Does calling Task.perform immediately evaluate, rather than return a Cmd which uses a continuation to invoke the next update loop more asynchronously?

The behaviour of the above code suggests that the answer is yes to both of these questions, and I find that a little surprising.

Actually, I think I see my mistake. I should be testing val < 10 and not model.count < 10.

What I am experimenting with is having a long running process run during the update loop. I want to implement a search for the Rubiks cube and this will need millions of iterations to find the answer.

I have 2 options:

  • Run some number of search iterations in the update cycle, and loop to search some more by issuing another Cmd, until the answer is found. By breaking the search up into chunks, I hope to keep the UI responsive enough that a progress bar could be rendered or a stop button clicked.

  • Work out how to use web workers to run in a background thread. Use ports and the web workers API for IPC.

val < 10 in the current code would print 9 to the dom, so there is still something else going on there.

Ok it should be val <= 10.

With val <= 10 the count in the DOM goes to 10, but in the console is logs update: Count 11.

I also need logic to not fire the next Count Cmd, once the count reaches 11.

So Task.perform does not evaluate eagerly. Thought I was losing the plot there…

Anybody done web workers with Elm before?

It does not look too bad. To do the IPC over ports I need to serialise the starting cube position and any results found with Json encoders. I cannot just pass around the current search state, as it wraps further states as a continuation:

type SearchResult state
    = Complete
    | Goal state (() -> SearchResult state)
    | Ongoing state (() -> SearchResult state)

Instead I would have the worker thread run until it is either stopped by an explicit command, or it reaches the ‘Complete’ state. It would periodically provide updates on its progress as (state, Int, Bool), giving the most recently examined state, a count of how many states have been explored so far, and a flag indicating whether or not the state supplied is a goal state.

You log before the guard of the if-statement -> you should log 11.
Your guard specifies that the current model-value should be less than 10, otherwise you increment -> you should print 10 to the DOM.

Everything seems fine IMO :slight_smile:

It seems it may not be worth it to update the “count” on the model and then also try to pass it around in a Count message, would something like this work for your use case?

type Msg
    = Increment


init =
    ( { count = 0 }, message <| Increment )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case Debug.log "update" msg of
        Increment ->
            if model.count < 10 then
                let
                    nextValue =
                        model.count + 1
                in
                ( { model | count = nextValue }, message <| Increment )
            else
                ( model, Cmd.none )

Yes, that would be better; if I really was just trying to implement an awesome counting program! But I wanted to put it in the Msg, so that I could log the messages out and understand exactly what messages were being processed.

But yes, for the real search program I will arrange things around this way. Thanks.

This would work for debugging purposes then and you’ll have access to what the “nextValue” is by passing it in with the message.

type Msg
    = Increment Int


init =
    ( { count = 0 }, message <| Increment 0)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case Debug.log "update" msg of
        Increment _ ->
            if model.count < 10 then
                let
                    nextValue =
                        model.count + 1
                in
                ( { model | count = nextValue }, message <| Increment nextValue )
            else
                ( model, Cmd.none )

Obviously the point in the end isn’t a complicated counting program, but what I think this clears up is your code was working properly before, but you weren’t incrementing the value set to the model and the value passed in with the message in the same pass, so your model was 1 behind your Count val so it looked like it was running an extra time.

One thing I am finding with running a long running process in the update loop, is that as I increase the size of the problem that is solved on each iteration, the UI becomes a lot more unresponsive. You would expect that, of course, but for example when iterating a search 1000 times in each update, the DOM will not display every 1000 increment, it will update much less frequently. If I iterate 1 time in each update, the DOM will smoothly update and I will see the counter spin up rapidly from 1.

I think the larger searches are creating a lot of garbage, and garbage collection is making the UI unresponsive. The search does progress faster in bigger increments though.

I wonder if using a web worker thread that generates a lot of garbage will tend not to slow down the main UI thread? I guess I will have to try it, and also the answer will likely be different on different browsers.