Site Map - skip to main content - dyslexic font - mobile - text - print

Hacker Public Radio

Your ideas, projects, opinions - podcasted.

New episodes Monday through Friday.


hpr2723 :: Using Elm in context of 4X game client

tuturto talks their decisions on structuring Elm application

<< First, < Previous, Latest >>

Hosted by tuturto on 2019-01-09 is flagged as Clean and is released under a CC-BY-SA license.
Tags: elm.
Listen in ogg, spx, or mp3 format. | Comments (0)

Original idea I had with my toy game project was to have Yesod render most of the user interface as static HTML and have as little client side scripting as possible. Later I realized that there would be parts with significant amount of client side code and it might be better if whole site was written in Elm.

Couple goals I had in my mind when I started this:

  • easy to work with
  • type safe
  • extensible
  • user authorization
    • regular player
    • administrator

Backend is written in Haskell and front end in Elm. Communication between them is via REST interface and most of the data is in JSON. All JSON encoding / decoding is centralized (more or less), same with initiating requests to server.

API Endpoints

End points used for REST calls are defined in single data type that captures their name and parameters. These are used when initiating requests, meaning there’s smaller chance of typo slipping through.

type Endpoint
    = ApiStarDate
    | ApiResources
    | ApiStarSystem
    | ApiStar
    | ApiPlanet
    | ApiPopulation PlanetId
    | ApiBuilding PlanetId
    | ApiConstructionQueue PlanetId
    | ApiConstruction Construction
    | ApiBuildingConstruction
    | ApiAvailableBuildings

For example, sending a GET request to retrieve all construction projects on a planet is done as:

Http.send (ApiMsgCompleted << ConstructionsReceived) (get (ApiConstructionQueue planetId) (list constructionDecoder))

GET Request is sent to ApiConstructionQueue endpoint and it has planetId as parameter. When server sends response, our program will parse content of it will be a list that is parsed with constructionDecoder and create “ApiMsgCompleted ConstructionsReceived” message with result of the parsing. Update function will process this and store list of constructions somewhere safe for further use.

Update function

Update function is in charge of reacting to messages (mouse clicks, page changes, responses from server). In a large program update function will quickly get big and unwieldy. Breaking it into smaller pieces (per page for example), will make maintenance easier. This way each page has their own message type and own update function to handle it. In addition there’s few extra ones (cleaning error display, processing API messages and reacting to page changes).

Same way as API end points are encoded in a type, pages are too:

type Route
    = HomeR
    | ProfileR
    | StarSystemsR
    | StarSystemR StarSystemId
    | PlanetR StarSystemId PlanetId
    | BasesR
    | FleetR
    | DesignerR
    | ConstructionR
    | MessagesR
    | AdminR
    | LogoutR
    | ResearchR

routeToString function is used to map Route into String, that can be placed in hyperlink. Below is an excerp:

routeToString : Route -> String
routeToString route =
    case route of
        HomeR ->
            "/home"

        StarSystemR (StarSystemId sId) ->
            "/starsystem/" ++ String.fromInt sId

        PlanetR (StarSystemId sId) (PlanetId pId) ->
            "/starsystem/" ++ String.fromInt sId ++ "/" ++ String.fromInt pId

Because mapping needs to be bi-directional (Route used to define content of a href and string from a href used to define Route), there’s mapping to other direction too:

routes : Parser (Route -> a) a
routes =
    oneOf
        [ map HomeR top
        , map ProfileR (s "profile")
        , map ResearchR (s "research")
        , map StarSystemsR (s "starsystem")
        , map StarSystemR (s "starsystem" </> starSystemId)
        , map PlanetR (s "starsystem" </> starSystemId </> planetId)
        , map BasesR (s "base")
        , map FleetR (s "fleet")
        , map DesignerR (s "designer")
        , map ConstructionR (s "construction")
        , map MessagesR (s "message")
        , map AdminR (s "admin")
        , map LogoutR (s "logout")
]

Result of parsing is Maybe Route, meaning that failure will return Nothing. Detecting and handling this is responsibility of the calling code, usually I just default to HomeR.

Borrowing from Yesod, client uses recursive function to define breadcrumb path. This is hierarchical view of current location in the application, allowing user to quickly navigate backwards where they came.

Breadcrumb path consists of segments that are tuple of (String, Maybe Route). String tells text to display and Route is possible parent route of the segment. This allows hierarchical definition: “Home / Star systems / Sol / Earth”. Because route has only (for example) PlanetId, we need to pass Model too, so that the data retrieved from server can be used to figure out what name such a planet has.

{-| Build complete breadcrumb path and wrap it in enclosing HTML
-}
breadcrumbPath : Model -> Html Msg

{-| Recursively build list of breadcrumbs from segments
Last one is plain text, while parents of it are links
-}
breadcrumb : Model -> Bool -> Route -> List (Html Msg)

{-| Get segment of given route in form of ( String, Maybe Route )
String denotes text describing the segment, Maybe Route is possible parent
-}
segment : Model -> Route -> ( String, Maybe Route )

Comments

Subscribe to the comments RSS feed.

<< First, < Previous, Latest >>

Leave Comment

Note to Verbose Commenters
If you can't fit everything you want to say in the comment below then you really should record a response show instead.

Note to Spammers
All comments are moderated. All links are checked by humans. We strip out all html. Feel free to record a show about yourself, or your industry, or any other topic we may find interesting. We also check shows for spam :).

Provide feedback
Your Name/Handle:
Title:
Comment:
Anti Spam Question: What does the P in HPR stand for ?