How to implement this use case in Elm?

92 views
Skip to first unread message

Grzegorz Balcerek

unread,
Jul 4, 2014, 2:58:49 PM7/4/14
to elm-d...@googlegroups.com
Hi,

How can I implement in Elm the following use case:
Imagine a board game (think Chess for examle), where a user plays
against the computer.
Using the usual Elm pattern, the game may be implemented in the
following way:
- make an input signal based on Mouse.clicks
- process the input signal together with the game state using foldp
(with a step function that calculates the new state)
- update the screen based on foldp output.
The step function would have at least those 2 parts:
(1) verify if the user input represents a valid move; if yes, then
calculate a new (intermediate) state
(2) calculate the computer's move, producing the new state (which
becomes the foldp's output).

This might work, however, it is not ideal.
Imagine that (1) takes 0.5 second and (2) takes 1 second to calculate.
The user, after clicking (choosing the move) whould see the application
"hanging" for 1.5 second, and then finally the screen would update with
the game state after both the player's and computer's move.

It would be better to split the processing into 2 phases, and have the
screen updated between (1) and (2).
What is the best way to do it?

I have a few ideas.
One of them is to use Mouse.isDown instead of Mouse.clicks, and process
(1) when isDown = True, and (2) when it is becomes False.
I tried that. I saw the following outcome:
If the user presses the mouse button down and keeps it pressed for more
than 0.5 seconds, the screen will be updated after (1) finishes. Then,
when the user releases the button (which triggers Mouse.isDown = False),
the calculation (2) starts and the screen will be updated again after 1
second. So far, so good.
But if the user presses the mouse button and releases it quickly (after
0.1 second for example), he will not see the screen updated after 0.5
seconds, but after 1.5 seconds -- the screen will be updated after both
(1) and (2) finishes -- that is at least what I observed. It looks like
the screen update is not performed because there is another input event
pending to be processed. Is that how Elm works? Is that by design?

I tried another approach, of using Mouse.clicks and Time.fps. I
processed (1) after the user clicks and (2) after time events. However,
the same happened. The Time events were "glued" together with the Mouse
events, and when that happened, I only saw the screen updated after both
(1) and (2) were calculated.

What I ended up doing (so far) is to have Mouse.clicks and Time.fps, but
also to introduce another phase between (1) and (2).
So the whole cycle is:
- Mouse.clicks happens, and (1) is calculated, producing a new state
- a Time.fps event happens, but I do not calculate a new state;
at that point I see the screen updated
- another Time.fps event happens and now I calculate (2), I see the
screen updated again

Do you have a better idea of how to implement such a use case?

Thanks,
Grzegorz Balcerek

Dobes Vandermeer

unread,
Jul 4, 2014, 5:32:16 PM7/4/14
to elm-d...@googlegroups.com

I think you probably want to break the long computations up into steps and do your calculations in small increments so that the screen can continue to update at 60fps or at least more than 2 fps.  Then you can show an animated "thinking" spinner and the user won't wonder if the game has crashed.

There has been some work on allowing some concurrency in Elm, using web workers, but I'm not sure that is ready for general use right now.  That would allow you to run the slow parts in the background and they would return their results when they are ready and you can update the display at that time and you wouldn't have to manually refactor your algorithms to run in small steps.  Maybe someone else will chime in about the status of that.

Cheers,

Dobes







--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jeff Smits

unread,
Jul 5, 2014, 6:03:57 AM7/5/14
to elm-discuss
Elm as described in Evans thesis can handle your use-case by using async to make something asynchronous. But JavaScript has a wacky event-loop model rather than actual concurrency (and don't even get started on parallelism). I used to think that when one event is fully handled, the browser would repaint the screen before handling the next event. This doesn't seem to be the case... I've found most information combined about these things on this page.

I thought about it and I think forcing the browser to render before handling the next event from the queue might solve the problems you've had. I searched the web a bit and found these solutions.
Possible downside: I wonder what will happen to performance if we were to add this in the Render code of Elm.
Likely upside: ability to define a working async :D

I would love to show you how your code might be written using async, but when I attempted it I bumped into foldp restrictions again..
So it looks like you need both async and generalised state to be able to define this nicely.
Whatever, it's a nice example to show off generalised state, so I'm still putting it in here:

-- given the types `input` and `state`

input : input
input = ... input based on: ... Mouse.clicks

-- the old keyword is from my generalised state API proposal
old state = ... some start state of the program ...

validMove : input -> state -> Bool
doMove : input -> state -> state
computerMove : state -> state
render : state -> Element

correctInput = validMove <~ old state ~ input

state1 = doMove <~ old state ~ keepWhen correctInput input

state2 = computerMove <~ (async state1)
{-
Notice async is before doing the expensive computation.
This is because JavaScript is still single-threaded.
You could do the async after the expensive `computerMove` if you were in a multithreaded setting
-}

state = merge state1 state2

main = render state

Note that the use async will open your code up to race conditions. To solve this you could for example remember who last did a move in the state. Then you can check that in validMove, so that if the user did two moves before the first is processed, the second isn't processed because the computerMove isn't calculated yet.

---------------------------------------------------------------

TL;DR
You can nicely specify your use case in Elm if you had two features that aren't in the current implementation.
One of those allows asynchronous computation and is described in Evans thesis. It's not in the implementation because JavaScript sucks at concurrency. But it might be possible to (ab)use the JavaScript event loop to still implement it.
The second is one is the one I'm still working on (codename Signal Loops).

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Evan Czaplicki

unread,
Jul 5, 2014, 11:08:02 AM7/5/14
to elm-d...@googlegroups.com
For now, I'd set up an outgoing port askForMove that is hooked up to some web worker. When the web worker finishes figuring out the move, it sends a message to some incoming port getMove. The web worker can be running elm code too. I guess you'd send diffs across.

So the user click will put your app in "wait" state and trigger an external event. The "main thread" will be free to update and react to input like changing settings, but it'll stop you from moving pieces. Eventually, the web worker will send an event back and put the app in an "active" state that'll accept new moves again.

This is what I'd do today, but Jeff describes the long term outlook and issues with JS really well!


--
Sent from Gmail Mobile

Grzegorz Balcerek

unread,
Jul 5, 2014, 5:06:02 PM7/5/14
to elm-d...@googlegroups.com

Thank you all for responding.
However, I have kept my solution (for now at least). I think the "thinking" time is not long enough to make it unacceptable.
If you are interested, here are the code: https://github.com/grzegorzbalcerek/chess-elm and the live app: http://www.grzegorzbalcerek.net/elm/Chess.html

By the way, elm-repl was very useful, thanks!
And I noticed, that the changes in my source files are automatically noticed (loaded) by the repl. Awesome!

I tried yet another approach -- I wanted to make a "signal loop" by merging my foldp output with the input signal.
Something like this:



        input ------> merge------>  foldp  ------------+-------------> render the screen
                        ^                              |
                        |                              |
                        |                              |
                        |                              |
                        |                              |
                        +----- lift ---- keepIf -------+


My goal was to generate additional input events from the foldp logic.
However, when running the code I got this:

Cannot read property 'kids' of undefined
    Open the developer console for more details.

And the dev console reports:

Uncaught TypeError: Cannot read property 'kids' of undefined elm-runtime.js:3086
Uncaught TypeError: Cannot read property 'id' of undefined elm-runtime.js:3074
Uncaught Error: The notify function has been called synchronously!
This can lead to frames being dropped.
Definitely report this to <https://github.com/evancz/Elm/issues>


Should I report it? I am not sure, since I am maybe doing something that elm is not prepared to handle.

Grzegorz Balcerek

W dniu 2014-07-05 17:08, Evan Czaplicki pisze:
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jeff Smits

unread,
Jul 6, 2014, 4:56:52 AM7/6/14
to elm-discuss
Elm doesn't support recursively defined signals. It's not exactly trivial to support.
You should report that issue, if it hasn't been reported already. The compiler should give an error if you define a recursive signal, instead of compiling to faulty code.

Basically recursively defined signals form a kind of signal loop. That's certainly the name I used for the feature I'm developing. I recently started using the name "generalised state" because signal loops seemed to scare people off.
Maybe with this in mind, my example of how you might do this in the future makes more sense? The merge is in my code example too.

@Evan: Any interest in the forced rendering idea? It would be nice to support async, even if it's a little quirky.
Reply all
Reply to author
Forward
0 new messages