Elm Object Oriented Style

Based on the code presented by @jhbrown at a recent Elm meetup, with accompanying slides, I decided to build a small app in an object oriented style using this technique. What intrigued me about this talk was the “hiding implementation types” idea, which immediately made me think “existential types in Elm?”. If I can hide the implementation type of something, then I can separate interface and implementation, and I can have multiple implementations of the same interface even if the implementations have different types. I did not think this was possible in Elm, but Jeremy showed how to sneak it past the type checker.

My code is here. This is a re-implementation of a little example I wrote to test my 2d camera package.. That explains why I have a black strip at the left (to leave space for a menu), and a transparent one on the right (for an overlay) - I was experimenting with setting up a 2d camera where the frame is offset from 0,0 and where the focus region of the camera does not necessarily fill the entire SVG region with the drawing in it.

The original non-OO version can be found in this repo under OldMain.elm

The interfaces for the OO version are defined in Scene/Spec.elm.

To get started with this I imagined doing something along the lines of an ECS or entity-component-system, so the drawings themselves I called Entity. In the end this wasn’t quite the right idea, but I stuck with the name Entity. A set of Entities are composed into a Scene, with this interface:

type alias SceneIF =
    { subscriptions : Sub Msg
    , update : Msg -> ( Scene, Cmd Msg )
    , view : () -> Html Msg
    , add : EntityId -> Entity Msg -> Scene
    }

And the Entities implement this interface (note move and animate not actually used in this example):

type alias EntityIF msg =
    { gestureHandler : Maybe (GestureIF msg)
    , move : VScene -> Entity msg
    , animate : Posix -> Maybe (Entity msg)
    , position : PScene
    , bbox : BLocal
    , view : ViewContextIF msg -> List (Svg msg)
    }

The gesture handlers are optional on an entity, and have this interface:

type alias GestureIF msg =
    { tap : Maybe (Pointer.PointArgs Screen -> UpdateContext msg -> UpdateContext msg)
    , doubleTap : Maybe (Pointer.PointArgs Screen -> UpdateContext msg -> UpdateContext msg)
    , drag : Maybe (Pointer.DragArgs Screen -> UpdateContext msg -> UpdateContext msg)
    , dragEnd : Maybe (Pointer.DragArgs Screen -> UpdateContext msg -> UpdateContext msg)
    }

The idea is that mouse or touch gestures are sent to the Scene through its subscription or event handlers in its view. Those events will contain the id of the entity that they take place on, and so the scene can trigger the gesture handler on an entity, provided it exists in the scene and has the relevant gesture handler.

Some interesting things about this:

  • The Entity has no idea what Msg type it is dealing with, it is parameterized over msg. This means it cannot create arbitrary side effects, it will be given an interface with functions in it to use to generate from a limited pallete of side effects (none in this example, although I did pass down noop just to show it can be done). An Entity is like a Program that I have specialized to fit a particular purpose - and unlike most Elm Programs, it is not a TEA.

  • The Scene and Entity know nothing about each others implementations. In particular new entities can be added to the codebase without touching any other code. So each entity gives a bounding box - I do not need to update a case statement somewhere that dervies bounding boxes for all Entities, just adding this code to the new one is sufficient.

Having the implementations hidden, does make it a little awkward, since sometimes the Entities do need to know things about the Scene. For view purposes, they need to know about the camera and frame and so on. For update purposes, they need some kind of interface they can apply those updates to, in order to change the state of the Scene, for example to move the camera, add more entities to the scene, etc.

For the view we have a view interface:

type alias ViewContextIF msg =
    { noop : msg -- Example side-effect the view is allowed to create.
    , zoom : Float
    , frame : BScreen
    , mousePos : PScreen
    , camera : Camera2d Unitless Pixels Geometry.Scene
    }

The update interface is a bit more complicated:

type alias UpdateContextIF msg =
    { frame : BScreen
    , camera : Camera2d Unitless Pixels Geometry.Scene
    , setCamera : Camera2d Unitless Pixels Geometry.Scene -> UpdateContext msg
    , animateZoomToTarget : Point3d Unitless (ZoomSpace Pixels Geometry.Scene) -> UpdateContext msg
    , updateEntity : EntityId -> Entity msg -> UpdateContext msg
    , toScene : Scene
    }

The update interface has a toScene method on it. This is because this interface is for making changes to the Scene, and so must ulimately be able to produce a new Scene, even though several updates can be applied to it to produce new UpdateContexts.

I haven’t described everything but this is probably enough of a guide for now, in case anyone wants to take a closer look at the code.

==========

Here is a summary what I think about this way of working with Elm, good and bad:

  • I like the extensibility, new implementations can be added without changing any old code. Comparing that with using a custom type, where a new constructor gets added for each new thing, it feels nicely encapsulated.

  • I like the idea of building my own mini-Program types in Elm that are more restrictive than the general TEA pattern. More restrictive means more specific to some particular application, and this can be a nice way of designing and enforcing a design on an area of code.

  • Elm does not have dynamic code loading, so this cannot be used to dynamically load plug-ins at runtime. The lack of runtime extensibility somewhat negates the benefits of the technique.

  • It was a bit complicated to figure out, but perhaps that was just because its the first time I have done this. The UpdateContext was the hardest part to figure out. I could actually have avoided UpdateContext altogether, if I had not hidden the Scene implementation type, and I only have one Scene implementation.

  • There is one Msg type over the whole scene and entities, and the entities do not have their own internal Msg types. If they did, they would need to expose it so that the parent update could use a constructor on the parent Msg type to group the child one under, as per nested TEA. This would stop the entities from all having the same type. On the other hand, I quite like the idea of passing a set of functions to the entity update or view offering a limited pallete of side-effects, it seems appropriate to using this technique for creating highly restricted Program types.

  • Separation of interface and implementation has a lot of appeal for me. Opaque types and modules can be used to achieve this too, and are usually going to be a more appropriate way of doing so in Elm.

In short, its too fiddly to use for most Elm work, but I think it can have its uses in limited situations where writing software in an open-ended and extensible way is highly important to you.

6 Likes

So I made some improvements to this, which I think show how narrowing interfaces can lead to cleaner code.

I removed the entire GestureIF interface from Entities, and added a new select method on the entity. Instead of entities themselves handling gestures to implement move, I now use the move method to do so. Instead of handling a doubleTap gesture event on the entity, I now user the select method.

type alias EntityIF msg =
    { move : VScene -> UpdateContext msg -> UpdateContext msg
    , select : UpdateContext msg -> UpdateContext msg
    , animate : Posix -> Maybe (Entity msg)
    , position : PScene
    , bbox : BLocal
    , view : ViewContextIF msg -> List (Svg msg)
    }

There was code in each entity to handle pointer drag, and this code needs to implement a small state machine (called GestureCondition in the implementation). This state machine flips into dragging mode when a drag is occurring and back to normal mode when the drag ends.

By removing the logic to drive the state machine from the entities and putting it in the Scene implementation instead, I am able to re-use this logic for all entities, current and future. Once this logic is correctly implemented, it removes the risk that some new entity added to the system can get it wrong. This greatly simplifies the logic each entity needs to implement to handle moves, since each move is now just a 2d vector.

For the Target shape:

onMove trans model raise (UpdateContext uc) =
    { model | pos = Point2d.translateBy trans model.pos }
        |> raise
        |> uc.updateEntity model.id

For the Root which acts as camera controller:

onMove trans _ _ (UpdateContext uc) =
    uc.moveCamera (Vector2d.reverse trans)

(On the UpdateContextIF, setCamera was replaced by moveCamera which is also narrower - the camera can no longer be arbitrarily set, it can only be moved or other specific and limited camera operations that are desirable to expose.)

I like how this now splits low-level gesture handling, from more high-level outcomes. The low-level handling bit happens entirely in the Scene implementation, and each entity provides implementations of more high-level methods that describe better the outcomes that will be applied to them. In this case I implemented doubleTap as triggering select on an entity, and that also leaves open the possibility of triggering the same select outcome in different ways - such as dragging a lasso control around a group of them, or single clicking, or …

Of course this is the well known OO refactoring of ‘pulling up’ a method from a sub-class into a parent one. A specific implementation gets recognised as a re-usable pattern, and pulled up into a place where it can be re-used and applied uniformly over a class of things.

Thanks for the write up!

In my understanding, FP facilitates adding an operation to a fixed set of data types (ADT) while OOP facilitates adding a data type to a fixed set of operations (Interface, type classes).

It’s clear how your work enables the second kind of extensibility as you pointed out, and it’s a very interesting pattern.

In your last point, you mention opaque types. In my experience with them, they do provide encapsulation, but not the extensibility of your solution. Is it yours as well? Or do you have examples of opaque types “done right” with some extensibility that I could look at for comparison/education?

I think that is correct, custom types do not give extensibility, at least not without modifying the type to add a new constructor.

type Shape = Square SquareProps | Triangle TriangleProps

Now you want to add cirlces?

type Shape = Square SquareProps | Triangle TriangleProps | Circle CircleProps

But you probably also have a load of functions over Shapes, so you need to add circle to them too:

shapeBBox : Shape -> BoundingBox2d Unitless Scene
shapeBBox shape = 
    case shape of 
        Circle props -> ... -- New code here
        Square props -> .. -- Existing code
        Triangle props -> .. -- Existing code

And so on, maybe you also have Encoder/Decoder, view, translation, select and many other things in some imaginary drawing program. If Shape is also opaque, the implementation details are hidden behind this drawing API.

So in that sense a custom type can be extended to add new behaviour, but it is necessary to modify existing code. The OO pattern can let you add new behaviour without touching existing code at all.

A good example of opaque types hiding implementation details might be Elms Dict. You don’t need to know how it works to use it, and its implementation could even change without breaking code using it. Inside I think its a red-black tree, but that should never leak through its API.

1 Like

I don’t classify FP and OO as opposites though. FP is about letting you program with pure mathematical functions, making functions first class in the language and allowing higher order functions; it usually employs immutability to achieve purity. OO is about combining data and code together into objects, and using them together as units that have a degree of independance of other code; it is about achieving modularity.

My code combines these two ideas as they can be orthogonal. But as there are no built in OO constructs in Elm, it is necessarily a bit of a round about way of doing it.

I agree that FP and OOP can’t really be described as opposites, especially with the two (IMO valid) definitions you provided.

What I would be described as “opposites” (or in fact, complementary) are the two extension mechanisms:

  • Defining a single function for all the variants of an ADT with pattern matching (i.e. adding an operation to a fixed set of data types)
  • Implementing an interface or a type class for a datatype (i.e. adding a fixed set of operation to a data type)

Different projects will emphasize those differently. For example, a compiler will work great with the first, while a game engine will rely more on the latter (some would say UI programming too!). It depends on how the developper thinks too!

(I got the idea from Dan Grossman in this video)

This would explain while some people hope for type classes in Elm: there is first class language support for the first kind of extensibility only.
This is why your thread piqued my interest. This might become a very useful pattern!

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