Yet another Game of Life implementation

Hi all! I’ve been working on a Game of Life implementation in Elm recently. Not an original idea I know, but it’s been a fun learning experience :slight_smile:

The code:
https://github.com/fizwidget/game-of-life

Try it out here:
https://fizwidget.github.io/game-of-life/index.html

54%20pm

Feedback on the code is welcome, especially on the module structure. In previous projects I’ve been guilty of making too many small modules and trying to componentize things too much. I tried to avoid that here, but there’s probably room for improvement :sweat_smile:

Thanks!

10 Likes

This is truly beautiful!

2 Likes

I’d say you’ve done a pretty good job here. I wouldn’t change much if anything.

A couple of minor personal things:

  1. In general, not just in this repo, I see quite a lot of use of |> where I feel it’s pretty unnecessary. To me |> is useful when you have a bunch of operations to perform and you just want to write them down in the order that they are performed, rather than in function application order. So, I kind of don’t see the point of:
         { model | speed = nextSpeed model.speed }
                |> withoutCmd

Especially when you consider the definition of withoutCmd you could just have written this as:

        ({ model | speed = nextSpeed model.speed}
        , Cmd.none
        )

But I’m 100% sure others would prefer what you’ve written.


In Matix.elm you have a wrap function:

wrap : Int -> Int -> Int -> Int
wrap min max value =
    if value < min then
        max

    else if value > max then
        min

    else
        value

Just to say that min and max are defined in Basics:

wrap : Int -> Int -> Int -> Int
wrap smallest largest value =
       max smallest <| min largest value

Finally something more what you were asking. I personally find your module structure fine. It’s probably a little “over-modularised” given that this program is unlikely to grow significantly. You’re pretty much done. But assuming you were meaning this as a kind of exercise it’s fine.

Your main pain point is the Controls module. It is kind of conceivable that you could re-use this for a different application, but fairly unlikely, and doing so might make it awkward to add a new control specific to this game. In either case the pain point is that you’re insistent that the controls are polymorphic in the type of the message, meaning that you have to pass in a record structure with all that information. But why?

You can get rid of that whole record definition by defining a type within the Controls module:

type ControlsMsg
   = StepBack
   | StepForward
   | etc.

Then in your Main module you can define your Msg type with one constructor for a ControlsMsg:

type Msg
    = ControlsMsg ControlsMsg
    |  ClockTick
    | etc.

Then in your viewControls function you just call Html.map ControlsMsg on the Html returned from Controls.view.
That also means your update function can look a little nicer as well as you can have a separate function to deal with all the ControlsMsgs:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ControlsMsg controlsMsg ->
              updateControls controlsMsg model
        ClockTick ->
            stepGame model
                |> ifGameFinished pauseGame
                |> withoutCmd
        ....

Or, you could just define your Msg type in a module of its own, and just directly refer to it from both Controls.elm and Main.elm.

3 Likes

Thanks for the detailed feedback! :slight_smile:

Very good point about the Controls module - your approach greatly simplifies things. I’d been under the impression that defining a ControlsMsg type within the module also implied defining ControlsModel, updateControls, etc, and making it a full-on “component”. I realise now that’s not the case though!

And yup, I probably did go overboard with |>. I found the withoutCmd util handy in a couple of cases, then used it in all the update branches for the sake of consistency (probably not a good justification, as it’s not needed in most of them).

Replacing the if/else chain with min/max from Basics is also a handy tip. With that change, I’m down to a single if expression in the entire program now. I found your thread on the topic quite interesting (If-then-else versus case expressions) - I might use a custom type to remove the remaining if :smile:

3 Likes

So some small comments:

  • :+1: Kudos on the Matrix wrapper! Very clean code! :smiley:
  • Why do you handle the cell color using a CSS class but the cell size difference between alive/dead using widths in the Elm application? Adding a cell-alive class or something would have worked just as well (and might be optimized by browsers maybe?)
  • Maybe it’s just me, but I’d love a mode with even more cells :sunglasses:. And also a version with even higher speed :angel:.
  • I dislike the hard-coded neighbours list, but I do not know if there is a simple nicer solution (My head wants to use Haskell’s list comprehensions, which Elm does not have :sweat_smile:)
  • Maybe you could use different icons or some extra text to indicate what current speed/zoom level is being used?
  • I loove the history interface!

Needs more comonads though… /joke :stuck_out_tongue_winking_eye:

1 Like

Thanks for the feedback, all good ideas! :slight_smile:

(I might pass on the comonads though, that’ll take me a while to get my head around :stuck_out_tongue_winking_eye:)

Not sure why I defined the sizing styles within the app, it would make more sense to do it with a CSS class :+1:

This is actually the first time in a while I’ve worked directly with CSS - I’m so used to libraries like elm-css and styled-components! I was originally using elm-css here, but found the performance overhead to be quite significant (fair enough, I suppose it’s not really designed for things like this).

Custom game sizes and improved controls are on my todo list :smile:

1 Like

Heh. I watched the talk about these comonads three times in a timespan of two years… It’s well-worth the watch, but it took me a long time to get some intuition for them as well.

Very quickly, concisely and potentially still not very understandable:

  • Forget everything you know about Monads. Comonads happen to look similar because their functions are the duals of the Monad functions (same signature, except the function arrows point the other way), but they have no logic in common.
  • Comonads can be seen as a context with a focus. You can always do two things:
    • Introduce another layer (‘dimension’) of context.
    • Take the thing out of the focus.
  • That’s all you can do with any comonad. But there are some special things we can do with e.g. the Tape that is presented in the presentation and the library.
  • For the Tape that is used in the presentation and the library, we are also able to move left and right one step on the tape, and to construct it from some initial values and iteration-functions.
  • Together, all of this is enough to basically emulate an spreadsheet-program with infinite numbers of cells in n-dimensions, and with cells referencing other cells (as long as you do not create cycles!).
  • For the game-of-life, a three-dimensional tape-of-tape-of-tapes is constructed, with the third dimension being time, where every next value is determined by applying the GoL-rules on the current two-dimensional ‘tape-of-tapes’.

Nice! :smiley:

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.