Skip to content

Instantly share code, notes, and snippets.

@TheSeamau5
Created December 29, 2014 23:16
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save TheSeamau5/4fc43cb00253f4e5d7b4 to your computer and use it in GitHub Desktop.
Save TheSeamau5/4fc43cb00253f4e5d7b4 to your computer and use it in GitHub Desktop.
An exploration of the Entity Component System in Elm

#Exploring Entity Component Systems in Elm

Entity-Component-System (or ECS) is a pattern for designing programs that is prevalent in the games industry. This pattern consists of three simple parts:

  • Entity : A uniquely identifiable object that may contain any number of components
  • Component : A property usually representing the raw data of one aspect of the object. (Position is a component, Velocity is a component, Strength is a component, etc...)
  • System : A continuous process performing actions on every entity that possesses a component of the same aspect as that system

To understand this, let us try to make a simple example: Boxes that move in space:

#Example

Let us model boxes that move through space. Each box is a square of given size and has a position and velocity. (and let's add a color for good measure)

type alias Box = {
  position : Vector,
  velocity : Vector,
  size : Float,
  color : Color
}

where Vector is the record type:

type alias Vector = {
  x : Float,
  y : Float
}

if we want to render a Box, we would simply do:

renderBox : Box -> Form
renderBox box =
  move (box.position.x, box.position.y) <|
    filled (box.color) (square box.size)

So, then, to display a red box on the screen, we just need to do:

box = {
  position : Vector 0 0,
  velocity : Vector 0 0,
  size     : 10,
  color    : rgb 255 0 0
}

main = collage 400 400 [renderBox box]

If we want to update the position of a box, this is very simple:

-- Assume delta time = 1
updatePosition : Box -> Box
updatePosition box =
  { box | position <- box.position `vAdd` box.velocity }

where vAdd is defined as vector addition as follows:

vAdd : Vector -> Vector -> Vector
vAdd v w = Vector (v.x + w.x) (v.y + w.y)

The final step is that we may want to move this box with the keyboard arrows. For this, we may want to feed in the keyboard input to an updateVelocity function as follows:

updateVelocity : Vector -> Box -> Box
updateVelocity vector box =
  { box | velocity <- vector}

input : Signal Vector
input = foldp vAdd origin (toFloatVector <~ arrows)

update : Vector -> Box -> Box
update input box =
  updateVelocity input <| updatePosition

where origin is the vector at the origin and toFloatVector helps us convert a vector of Int to a vector of Float:

origin = Vector 0 0

toFloatVector : {x : Int, y : Int} -> Vector
toFloatVector {x, y} = Vector (toFloat x) (toFloat y)

So, to put everything together we just do:

-- nice shorthand
render box = collage 400 400 [renderBox box]

main = render <~ foldp update box input

And, there, we have a box that moves whenever you press the arrow keys. Each time you press the arrow keys you change the box's velocity and then the game updates the position accordingly.

For reference, here is the full code:

import Color (..)
import Graphics.Collage (..)
import Graphics.Element (..)
import Signal (..)
import Keyboard (..)

-------------- Model ----------------

-- Two dimensional Vector type
type alias Vector = {
  x : Float,
  y : Float
}

origin : Vector
origin = Vector 0 0

-- Need this function to turn keyboard arrows into a Vector
toFloatVector : {x : Int, y : Int} -> Vector
toFloatVector {x, y} = Vector (toFloat x) (toFloat y)

-- Vector Addition
vAdd : Vector -> Vector -> Vector
vAdd v w = Vector (v.x + w.x) (v.y + w.y)


-- Box type to represent our box
type alias Box = {
  position : Vector,
  velocity : Vector,
  size : Float,
  color : Color
}

-- default box (red box at the origin)
box : Box
box = {
  position = origin,
  velocity = origin,
  size = 10,
  color = rgb 255 0 0 }

------- Update ------------
updatePosition : Box -> Box
updatePosition box =
  { box | position <- box.position `vAdd` box.velocity}

updateVelocity : Vector -> Box -> Box
updateVelocity vector box =
  { box | velocity <- vector }

update : Vector -> Box -> Box
update input box =
  updateVelocity input <| updatePosition box


------- Render -------------
renderBox : Box -> Form
renderBox box =
  move (box.position.x, box.position.y) <|
    filled (box.color) (square box.size)

render : Box -> Element
render box = collage 400 400 [renderBox box]


------- Input --------------
input : Signal Vector
input = foldp vAdd origin (toFloatVector <~ arrows)


------- Main --------------
main : Signal Element
main = render <~ foldp update box input

#Analyzing the example

This code has several nice things going for it. First of all, the code is nicely seperated between the model code (our types and data), the update code (how to update the model given an input), the render code (how to render the model onto the screen), and the main part which glues everything together.

This separation is good in that one can easily reason about the program. It is also quite easy to debug because the only things in the entire code that involve signals are the input and the main. Everything else are just in terms of immutable data and pure functions.

Now, suppose that we want to extend this game. We want to add another box, that is blue, but we only control the red box. This sounds like a reasonable extension and shouldn't cost much but isn't obvious to implement.

A quick and dirty way would be to do the following changes:

blueBox = { box | color <- rgb 0 0 255 }

render : Box -> Element
render redBox = collage 400 400 [renderBox redBox, renderBox blueBox]

This works, but we can already smell something wrong with this approach. In some sense, the blue box does not participate in the system like the red box. The blue box is relegated to just some square that we render on the screen. The above change is completely equivalent to the following change:

render : Box -> Element
render redBox = collage 400 400 [renderBox redBox,
  filled (rgb 0 0 255) (square 10)]

We suddenly lost the potential for the box to interact with the world. This may become clear if we decide to add gravity to the system and all boxes must be subject to gravity.

... uh ... oh ...

well, one way to do this is by adding an addition updateVelocityDueToGravity function

gravity = 0.5 --Cuz why not

updateVelocityDueToGravity : Box -> Box
updateVelocityDueToGravity box =
  {box | velocity <- box.velocity `vAdd` (Vector 0 -gravity) }

This means we'll have to modify update to:

updateBox : Vector -> Box -> Box
updateBox input box =
  updateVelocityDueToGravity <|
    updateVelocity input <|  
      updatePosition box

update : Vector -> List Box -> List Box
update input = map (updateBox input)

We modify render to:

render : List Box -> Element
render boxes =
  collage 400 400 (map renderBox boxes)

We modify our base model to be:

redBox  = { box | color <- rgb 255 0 0 }
blueBox = { box | color <- rgb 0 0 255,
                  position <- Vector 20 0 }

boxes = [redBox, blueBox]

And thus, modify main to be:

main : Signal Element
main = render <~ foldp update boxes input

And...we get gravity. So, if we only move sideways we see that the boxes fall down. It's not a realistic model of gravity because it is as if you reach terminal velocity instantly, but it push the boxes down as intended. But, something weird has happened... now both boxes are controlled by the keyboard... oops...

So, how do we fix that?

Well, we could have two separate update functions, one for blue squares and one for red squares. But that just feels weird. That's specializing too much. Because what if I decide to control green squares or yellow pentagons... That's not a good strategy.

A better strategy would be to extend the box type to have an isControllable flag.

so, that means that we change the model to :

type alias Box = {
  position : Vector,
  velocity : Vector,
  size : Float,
  color : Color,
  isControllable : Bool
}

box = {
  position = origin,
  velocity = origin,
  size = 20,
  color = rgb 255 0 0,
  isControllable = False
}


redBox  =
  { box | color <- rgb 255 0 0,
          isControllable <- True }

blueBox =
  { box | color <- rgb 0 0 255,
          position <- Vector 20 0}

And, all we need to change is the one function in the code that uses the input to modify the model which is updateVelocity (let's change its name to updateVelocityDueToInput to clarify things ):

updateVelocityDueToInput : Vector -> Box -> Box
updateVelocityDueToInput input box =
  if (box.isControllable == False) then box
  else
    { box | velocity <- vector }

I guess that wasn't too bad. But it does mean that every box must have an isControllable attribute, which kinda feels like it should be there if you need it. The problem becomes if you start having many such flags.

But the real problem comes if you want things other than square boxes. What if you want circles...

The obvious step would be to do

type alias Ball = {
  position : Vector,
  velocity : Vector,
  radius : Float,
  color : Color
  isControllable : Bool
}

ball = {
  position = origin,
  velocity = origin,
  radius = 10,
  color = rgb 255 0 0,
  isControllable = False
}

renderBall ball =
  move (ball.position.x, ball.position.y) <|
    filled ball.color (circle ball.radius)

But, now, we'd need to update render to:

render = collage 400 400
  ((map renderBall balls) ++ (map renderBox boxes))

and, we'd need to modify a bunch of type signatures to make sure they also work with balls, like

updateVelocity :
  Vector ->
    {a | position : Vector, velocity : Vector} ->
      {a | position : Vector, velocity : Vector}

and so on...

As, we can see, these changes are starting to become hairy and must be done for every additional type or functionality we add. Is there a simpler way?

#Component Entity System Instead of modeling each object as a separate type, we should model all objects as having the same type but having a varying number of components.

type Entity = Entity (List Component)

type Component =
  Position Float Float |
  Velocity Float Float |
  Color Color  |
  Size Float   |
  Radius Float |
  IsControllable

With these types, we could simply model our boxes and balls as:

box =
  Entity [
    Position 0 0,
    Velocity 0 0,
    Color (rgb 255 0 0),
    Size 10
  ]

ball =
  Entity [
    Position 0 0,
    Velocity 0 0,
    Color (rgb 255 0 0),
    Radius 10
  ]

if we want to make something controllable, all we need to do is make an addComponent function and add the isControllable component to the entity.

addComponent : Component -> Entity -> Entity
addComponent component (Entity components) =
  Entity (component :: components)

controllableBox = addComponent isControllable box

This allows to also do the crazy trick of having a list of entities with dramatically different components

entities : List Entity
entities = [box, ball, controllableBox]

This means that we don't need to separate our entities and have code that can work on all entities. More code reuse, more generality, more awesome!

Now, let's try to implement the example with one controllable red box, one blue box, one green circle, and (to spice things up) one black circle that isn't affected by gravity.

import Color (..)
import Graphics.Collage (..)
import Graphics.Element (..)
import Signal (Signal, foldp, (<~))
import Keyboard (..)
import List (map, (::))

-- A type to capture the shape of an object.
-- Either a square or a circle
type Shape = Square | Circle

-- Easier to alias the input type.
-- This matches the output from `arrows`
type alias Input = { x : Int, y : Int }

-- The Entity type.
-- An entity is an object with a list of components.
type Entity = Entity (List Component)

-- All the different type of components we will used
-- together in one big union type.
-- If you want to make a new kind of component,
-- just add it here
type Component =
  Position Float Float |
  Velocity Float Float |
  Scale Float |
  Color Color |
  Shape Shape |
  Controllable |
  Static


-- Red controllable box
redBox : Entity
redBox =
  Entity [
    Position 0 0,
    Velocity 0 0,
    Scale 10,
    Color (rgb 255 0 0),
    Shape Square,
    Controllable  
  ]

-- Blue box
blueBox : Entity
blueBox =
    Entity [
    Position -30 0,
    Velocity 0 0,
    Scale 10,
    Color (rgb 0 0 255),
    Shape Square
  ]

-- Green circle
greenCircle : Entity
greenCircle =
  Entity [
    Position 30 0,
    Velocity 0 0,
    Scale 10,
    Color (rgb 0 255 0),
    Shape Circle
  ]

-- Black circle (unaffected by gravity)
blackCircle : Entity
blackCircle =
  Entity [
    Position 80 0,
    Velocity 0 0,
    Scale 10,
    Color (rgb 0 0 0),
    Shape Circle,
    Static
  ]

-- The list of all entities in the system
entities : List Entity
entities = [redBox, blueBox, greenCircle, blackCircle]

-- Some value for gravity
gravity : Float
gravity = -0.5


-- Action: Apply gravity on an object.
-- To apply gravity, an entity must have a velocity component and
-- not have a static component
applyGravity : Float -> Entity -> Entity
applyGravity gravity entity =
  case (getVelocity entity, getStatic entity) of
    (Just (Velocity x y), Nothing) ->
      updateVelocity (Velocity x (y + gravity)) entity
    _ -> entity

-- Action: Move an entity
-- To move an entity, there must be a position and velocity component
moveEntity : Entity -> Entity
moveEntity entity =
  case (getPosition entity, getVelocity entity) of
    (Just (Position x y), Just (Velocity vx vy)) ->
      updatePosition (Position (x + vx) (y + vy)) entity
    _ -> entity


-- Action: Apply input
-- To apply input to an entity, there must be a velocity and
-- controllable component
applyInput : Input -> Entity -> Entity
applyInput {x,y} entity =
  case (getVelocity entity, getControllable entity) of
    (Just (Velocity vx vy), Just Controllable) ->
      updateVelocity (Velocity (vx + toFloat x) (vy + toFloat y)) entity
    _ -> entity



-- Action: Render entity
-- To render an entity, there must be a position, scale,
-- shape, and color component
renderEntity : Entity -> Maybe Form
renderEntity entity =
  case (getPosition entity,
        getScale entity,
        getShape entity,
        getColor entity) of

    (Just (Position x y),
     Just (Scale scale),
     Just (Shape shape),
     Just (Color color)) ->

      case shape of
        Square ->
          Just <| move (x,y) <| filled color (square scale)
        Circle ->
          Just <| move (x,y) <| filled color (circle scale)


    _ -> Nothing


-- Function to render a list of entities
render : List Entity -> Element
render entities = collage 400 400
  (filterJusts (map renderEntity entities))


-- The update function for a single entity
-- Move the entity, the apply input, then apply gravity
updateEntity : Input -> Entity -> Entity
updateEntity input entity =
  applyGravity gravity <|
    applyInput input <|
      moveEntity entity

-- Function to update a list of entities given an input
update : Input -> List Entity -> List Entity
update input = map (updateEntity input)


-- Get input from Keyboard
input : Signal Input
input = accumulateInput arrows


-- The Main Function
-- Update and render all entities
main : Signal Element
main = render <~ foldp update entities input

#Analysis

Every object in the game is modeled as an Entity. As we can see, the Entity type is super simple:

type Entity = Entity (List Component)

An entity simply contains a list of components. Now, let's look at the Component type:

type Component =
  Position Float Float |
  Velocity Float Float |
  Scale Float |
  Color Color |
  Shape Shape |
  Controllable |
  Static

Every component in the system is included in this union type. This may seem like an odd choice but this implies that we can have a list of entities where each entity has wildly different components.

As we can see, the entities object contains completely different entities. Some are controllable, some are unaffected by gravity, some are circles, other are squares... This is basically the closest thing there is in Elm to a heterogeneous list. Think of it like an explicit heterogeneous list.

After that we have "actions". Operations on entities. Each of these actions perform a distinct operation that depends on the type of components the entity has. The adequate pattern here is to make explicit by pattern matching only the cases you are interested in and use underscore to fail. This allows for the component type to be extendable ad infinitum and reduce boilerplate. DRY and composability FTW!

We can see that applyGravity, moveEntity, applyInput, and renderEntity are all actions although renderEntity deserves special mention for being the only one not to return an entity. This means that rendering and update are still kept separate in this model.

All "actions" are performed in the updateEntity function. updateEntity is quite simple, it takes an input and an entity and returns an entity.

updateEntity : Input -> Entity -> Entity
updateEntity input entity =
  applyGravity gravity <|
    applyInput input <|
      moveEntity entity

After this, we have the input, which is just accumulating the input from the keyboard arrows.

input : Signal Input
input = accumulateInput arrows

where accumulateInput is defined as:

accumulateInput : Signal Input -> Signal Input
accumulateInput =
  let add p q = { x = p.x + q.x, y = p.y + q.y}
      origin = {x = 0, y = 0}
  in foldp add origin

And then the main function is almost the same as before:

main : Signal Element
main = render <~ foldp update entities input

At first glance, it may not seem obvious how this code is better than the previous example. But, one can see that this code can accept additive changes without major changes to the codebase. In order to add an additional component to this code, all you'd need to do is add the component to the Component type and add whatever action you need to be performed on the entity.

Say you want to add mass, you just need to add mass to the component type

type Component =
  Position Float Float |
  Velocity Float Float |
  Mass Float |
  Scale Float |
  Color Color |
  Shape Shape |
  Controllable |  
  Static

and perhaps you'll want to change the gravity code to take mass into account.

applyGravity : Float -> Entity -> Entity
applyGravity gravity entity =
  case (getVelocity entity, getStatic entity, getMass entity) of
    (Just (Velocity x y), Nothing, Mass mass) ->
      updateVelocity (Velocity x (y + gravity / mass)) entity
    _ -> entity

And, now that you require mass for things to move, you might want to add a mass component to any object you want to move.

redBoxWithMass = addComponent (Mass 10) redBox
blueBoxWithMass = addComponent (Mass 1) blueBox
blackCircleWithMass = addComponent (Mass 100) blackCircle

and change the entities to :

entities = [redBoxWithMass, blueBoxWithMass, greenCircle, blackCircleWithMass]

Now if you run the code, the boxes are affect by gravity at a different rate and suddenly the green circle is unaffected by gravity which may be preferred since now we have decided to tie mass to gravity. Note how while the black circle has mass, it still does not move. This is because it still has a Static component and that overrides having mass.

So, as we can see, this approach seems to be more reasonable to architect code as it lends itself nicely to updates and modification, requiring you to change only what conceptually needs to be changed.

But you might be wondering, "hey, where did all those updatePosition, updateVelocity functions come from?".

Well, this is the drawback to this method. Since you can't create a generic function to ask if a value has a type tag, the following boilerplate is required:

accumulateInput : Signal Input -> Signal Input
accumulateInput =
  let add p q = {x = p.x + q.x, y = p.y + q.y}
      origin  = {x = 0, y = 0}
  in foldp add origin


filterJusts : List (Maybe a) -> List a
filterJusts list =
  case list of
    [] -> []
    x :: xs ->
      case x of
        Nothing -> filterJusts xs
        Just just -> just :: filterJusts xs


addComponent : Component -> Entity -> Entity
addComponent component (Entity components) =
  Entity (component :: components)


--- position component accessors

getPosition : Entity -> Maybe Component
getPosition (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Position _ _ -> Just x
        _ -> getPosition (Entity xs)

filterPosition : Entity -> Entity
filterPosition (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Position _ _ -> filterPosition (Entity xs)
        _ -> addComponent x (filterPosition (Entity xs))


updatePosition : Component -> Entity -> Entity
updatePosition component entity =
  case component of
    Position _ _ ->
      case (getPosition entity) of
        Nothing -> entity
        _ -> addComponent component (filterPosition entity)
    _ -> entity


--- velocity component accessors

getVelocity : Entity -> Maybe Component
getVelocity (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Velocity _ _ -> Just x
        _ -> getVelocity (Entity xs)

filterVelocity : Entity -> Entity
filterVelocity (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Velocity _ _ -> filterVelocity (Entity xs)
        _ -> addComponent x (filterVelocity (Entity xs))

updateVelocity : Component -> Entity -> Entity
updateVelocity component entity =
  case component of
    Velocity _ _ ->
      case (getVelocity entity) of
        Nothing -> entity
        _ -> addComponent component (filterVelocity entity)
    _ -> entity


--- scale component accessors

getScale : Entity -> Maybe Component
getScale (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Scale _ -> Just x
        _ -> getScale (Entity xs)


filterScale : Entity -> Entity
filterScale (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Scale _ -> filterScale (Entity xs)
        _ -> addComponent x (filterScale (Entity xs))


updateScale : Component -> Entity -> Entity
updateScale component entity =
  case component of
    Scale _ ->
      case (getScale entity) of
        Nothing -> entity
        _ -> addComponent component (filterScale entity)
    _ -> entity

--- color component accessors
getColor : Entity -> Maybe Component
getColor (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Color _ -> Just x
        _ -> getColor (Entity xs)

filterColor : Entity -> Entity
filterColor (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Color _ -> filterColor (Entity xs)
        _ -> addComponent x (filterColor (Entity xs))


updateColor : Component -> Entity -> Entity
updateColor component entity =
  case component of
    Color _ ->
      case (getColor entity) of
        Nothing -> entity
        _ -> addComponent component (filterColor entity)
    _ -> entity


--- shape component accessors
getShape : Entity -> Maybe Component
getShape (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Shape _ -> Just x
        _ -> getShape (Entity xs)


filterShape : Entity -> Entity
filterShape (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Shape _ -> filterShape (Entity xs)
        _ -> addComponent x (filterShape (Entity xs))


updateShape : Component -> Entity -> Entity
updateShape component entity =
  case component of
    Shape _ ->
      case (getShape entity) of
        Nothing -> entity
        _ -> addComponent component (filterShape entity)
    _ -> entity


--- controllable component accessors
getControllable : Entity -> Maybe Component
getControllable (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Controllable -> Just x
        _ -> getControllable (Entity xs)


filterControllable : Entity -> Entity
filterControllable (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Controllable -> filterScale (Entity xs)
        _ -> addComponent x (filterControllable (Entity xs))


updateControllable : Component -> Entity -> Entity
updateControllable component entity =
  case component of
    Controllable ->
      case (getControllable entity) of
        Nothing -> entity
        _ -> addComponent component (filterControllable entity)
    _ -> entity


--- static component accessors
getStatic : Entity -> Maybe Component
getStatic (Entity components) =
  case components of
    [] -> Nothing
    x :: xs ->
      case x of
        Static -> Just x
        _ -> getStatic (Entity xs)



filterStatic : Entity -> Entity
filterStatic (Entity components) =
  case components of
    [] -> Entity (components)
    x :: xs ->
      case x of
        Static -> filterStatic (Entity xs)
        _ -> addComponent x (filterStatic (Entity xs))


updateStatic : Component -> Entity -> Entity
updateStatic component entity =
  case component of
    Static ->
      case (getStatic entity) of
      Nothing -> entity
      _ -> addComponent component (filterStatic entity)
    _ -> entity

As you can see, it is a bit gnarly. But there are two redeeming things to note:

  • You only need to write a get, filter, and update function per component. This means that whenever you create a new component (chances are you won't create thousands), you create these three functions alongside. This is a somewhat tractable process.
  • If you notice, all the get functions and all the filter functions and all the update functions look almost alike except from a few details that depend on the name of the component and the number of parameters. With this knowledge, one can imagine that it wouldn't be too hard to generate this code automatically just by looking at the component type. This does mean that there would be a step prior to compilation but it would be an incredible time saver (and sanity saver).

All this could be avoided if Elm had a way to ask if an object has a given tag.

You could rewrite getPosition, filterPosition, and updatePosition as follows

getPosition : Entity -> Maybe Component
getPosition (Entity components) =
  head <| get Position components

filterPosition : Entity -> Entity
filterPosition (Entity components) =
  Entity (filter Position components)

updatePosition : Component -> Entity -> Entity
updatePosition component (Entity components) =
  update Position components

where the get, filter, and update functions would be implemented based on a testing function hasTag

{- Ignore the details of the syntax, just read it as:

      `hasTag` is a function that takes a tag from a
      union type and a union type and returns a Bool.
-}
hasTag : (Tag : unionType) -> unionType -> Bool

which can be used as follows

type UnionType = A Int | B String
type UnionType2 = C Int

a = A 4
b = B "Hello"
c = C 8

test  = hasTag A a -- True
test2 = hasTag A b -- False
test3 = hasTag B b -- True
test4 = hasTag A c -- Compile-time error. Type mismatch

#Conclusion

Entity Component Systems are promising. They have a really clean way of separating code. Basically, it is possible to write code that can be easily extended with very minimal changes. This pattern is not at odds with the best practices in Elm that encourage the signal code and the pure code to be separate and the signal code to be minimized.

The only issue is that of boilerplate. One can use Strings instead of tags but Strings lead to brittle code. Elm's type system should be taken advantage of fully.

I will continue to explore this pattern to see how it can be improved in Elm and perhaps reduce the boilerplate which is conceptually unnecessary but required for the code to work.

Another area worth exploring is how rendering is such a special case in the code. Should rendering be a special case? Could this be all wrapped in some monadic pattern? Can the renderer be modeled as a component?

@takaczapka
Copy link

takaczapka commented Apr 4, 2017

Great article.

How have you implemented hasTag function? Declaration hasTag : (Tag : unionType) -> unionType -> Bool does not even compile. Am I missing something here?

@GunpowderGuy
Copy link

Frp is better , because it doesn't require ecs

@k-bk
Copy link

k-bk commented Sep 12, 2018

Thank you for this article! Your approach seems to work well in my project.

I managed to implement hasTag function, so I avoided all the boilerplate. Yay!
Here is my solution:

module Component exposing (..)

type Component
    = Position Float Float
    | Speed Float

position : Component
position = Position 0 0

speed : Component
speed = Speed 0.0

hasTag : Component -> Component
hasTag tag component =
    case ( tag, component ) of
        ( Position _ _, Position _ _ ) -> True
        ( Speed _, Speed _ ) -> True
        _ -> False

And I use it for example as follows:

import Component exposing (Component(..), hasTag)

hasSpeed : List Component -> Bool
hasSpeed components =
    List.any (hasTag Component.speed) components

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment