Code Monkey home page Code Monkey logo

elm-patterns's Introduction

Elm Patterns

github pages

A collection of common patterns in Elm http://sporto.github.io/elm-patterns/.

Development

mdbook

This book uses mdbook: https://github.com/rust-lang/mdBook

To view locally run

mdbook serve -p 5000

Deployment is done automatically via github actions (No need to build the book).

Contributing

This guide documents patterns that are commonly used in Elm applications.

  • Avoid uncommon or esoteric patterns.
  • The pattern explanation needs to be as simple as possible. Don't write a whole essay for each one. They should be easy to read and understand in a few minutes (whenever possible).
  • Avoid complex terminology e.g. Monads, Applicative, etc. Unless this is really critical for understanding the pattern.
  • The pattern explanation doesn't need to be thorough (i.e. show all the code). As long at it conveys the idea and benefits. Linking to a more detailed source is great.
  • These patterns should try to align with the community recommendations and best practices.
  • Write in first person voice using the pronoun "we". E.g. "We might choose to use X".

elm-patterns's People

Contributors

allanderek avatar anshulvv avatar edgerunner avatar gacallea avatar janiczek avatar jfmengels avatar sporto avatar trs123 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elm-patterns's Issues

Pattern idea: Opaque state machine

Hi!

When modelling an offline-first application, I stumbled twice upon a pattern regarding entities that start with a globally non-unique String ID ("client" entities) that after being stored globally are promoted to "server" entities, which have a globally unique Integer ID provided by the server.

TL;DR: We are modelling an entity that has a state machine with different information in each state, and other information common to all states. We want to enforce the process flow through the states but we also need a way to access the common data regardless of the state, while keeping the internals private to our module. Current patterns in the catalog only solve part of the problem (the closest one being "Process flow using phantom types", so I describe here the problem and the pattern I found:

The problem

This is a process flow I want to be able to restrict:

  • Without API interaction, only Client entities can be created.
  • The API decoders are the only ones that can create Server entities from Client entities.
  • The API accepts only Client entities for creation and only Server entities for modification.

It is a simple state machine:

ClientEntity {id: String} -> ServerEntity {id: Int}

But it has a small peculiarity that makes the pattern "Process flow using phantom types" a bad idea: Some functions are not restricted to a specific state, but act differently on each state.

In my case, client IDs are Strings but server IDs are Integers, but we still need one function to get the ID as string no matter the state.

Anti-patterns

Phantom types

If we try to solve it with phantom types:

type Entity a
    = Entity Id GenericInternals

type alias GenericInternals = { someData: String, ... }

type Id
    = ClientId String
    | ServerId Int

type Client = Client

type Server = Server

we will find out that we need to manage invalid cases in our strict functions, because there is no relationship between the phantom type and our Id type :

insertEntity : Entity Client -> (Entity Server -> msg) -> Cmd msg
insertEntity (Entity id _) msg =
    let
        oldId = case id of
            ServerId id -> -- Invalid case!! An Entity Client should never contain a ServerId!

"Process flow using phantom types" pattern

If we instead try to solve it with "Process flow using phantom types" pattern:

type alias Internals =
    { clientId : String
    , serverId : Int
    ...
    }

type Entity a = Entity Internals

type Client = Client

type Server = Server

Then we can't implement strategy functions (functions that work with all states but differently with each one):

getIdAsString : Entity a -> String
getIdAsString (Entity internals) =
    -- Should we use clientId or serverId??

We should implement a different getId function for each state, so we would be forcing the consumer to store the state of each of the entities itself, thus rendering our phantom type useless or redundant.

Polymorphic type

Finally, if we try to solve it with a polymorphic type:

type Entity a
    = Entity a GenericInternals

type Client = Client String

type Server = Server Int

Then we have no way to access the state-specific internals in strategy functions, like getIdAsString : Entity a -> String, because Elm has no way to infer that a will be Client | Server.

Pattern

We expose the state machine type with all its states. Meanwhile the states, containing both state-specific and state-generic internals, are represented with opaque types:

module Entity exposing (Entity(..), ClientEntity, ServerEntity)

type Entity
    = Client ClientEntity
    | Server ServerEntity


-- Private types

type alias GenericInternals = { someData: String, ... }


-- Opaque types

type ClientEntity = ClientEntity String GenericInternals

type ServerEntity = ServerEntity Int GenericInternals

This allows both generic functions and restricted functions:

getIdAsString : Entity -> String
getIdAsString entity =
    case entity of
        Client (ClientEntity s _) -> s
        Server (ServerEntity i _) -> String.fromInt i

insertEntity : ClientEntity -> (ServerEntity -> msg) -> Cmd msg

Tradeoffs

This pattern forces the consumer to be wrapping and unwrapping the entity in the generic Entity type, depending on which function is being used. This can become tedious, so use it only when the acquired guarantees make this worth it.


This is not meant to be the pattern redaction, I would make a PR for that.
What do you think? Is this problem usual? Do you find the pattern idiomatic? Would you solve it differently?

Thank you for reading this.

In addition to package APIs, opaque types are useful for preserving representation invariants

The section on Opaque Types http://sporto.github.io/elm-patterns/advanced/opaque-types.html presents them as principally useful for packages. But I think the most useful part of opaque types is making it possible to ensure a representation invariant. For instance, a type SortedList would benefit from being opaque -- if its internals were exposed, user code could modify the underlying representation and break the "sorted" invariant. I think this is more important, for most people, than the package API stability argument.

Typo

On phantom types:

<p><code>activeUsers</code> is a function that only takes all users and returns only active users.</p>

activeUsers is a function that only takes all users and returns only active users.

The first "only" should be omitted:

activeUsers is a function that takes all users and returns only active users.

Would "avoid boolean ambiguity" be a good pattern?

Hi :)

I'm still learning with Elm in Action, and I've only made a learning project so far. So please excuse if this is not a pattern or it's inappropriate. Should this fit, it would make a great addition: "Solving the Boolean Identity Crisis" by Jeremy Fairbank.

It boils down to be more specific about True/False values and only use a Bool when strictly necessary. Else use Types to make code clearer and making sense of "an opaque logic". I applied this to the sound control of to the SpeechSynthesis Web API.

Elm code in Main.elm:

type Msg
    = ...
    ...
    | SetSound Sound
type Sound
    = On
    | Off
type alias Model =
    { ...
    , sound : Sound
      ...
    }
initialModel : Model
initialModel =
    { ...
    , sound = On
      ...
    }
        ...
        , button [ onClick (SetSound On) ] [ text "Sound On" ]
        , button [ onClick (SetSound Off) ] [ text "Sound Off" ]
        ...
update msg model =
    case msg of
        SetSound param ->
            ( model, setSound param)
setSound : Sound -> Cmd Msg
setSound switch =
    case switch of
        On ->
            sound True

        Off ->
            sound False
port sound : Bool -> Cmd msg

JavaScript code in index.html

      ...

      // Pause/Resume Speech Synth API (SetSound On | Off)
      app.ports.sound.subscribe(function (message) {
        synth.cancel();
        if (message == true) {
          synth.resume();
        } else {
          synth.pause();
        }
      });

      ...

I hope this makes sense, and I invite everyone reading this to watch Jeremy's video and rationale. It is amazing.

Should this idea be viable, and if necessary, I could come up with a PR.

Cheers

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.