layout: post
title: “Thirteen ways of looking at a turtle”
description: “Examples of an API, dependency injection, a state monad, and more!”

categories: [Patterns]

This post is part of the F# Advent Calendar in English 2015 project.
Check out all the other great posts there! And special thanks to Sergey Tihon for organizing this.

I was discussing how to implement a simple turtle graphics system some time ago,
and it struck me that, because the turtle requirements are so simple and so well known, it would make a great basis for demonstrating a range of different techniques.

So, in this two part mega-post, I’ll stretch the turtle model to the limit while demonstrating things like: partial application, validation with Success/Failure results,
the concept of “lifting”, agents with message queues, dependency injection, the State monad, event sourcing, stream processing, and finally a custom interpreter!

Without further ado then, I hereby present thirteen different ways of implementing a turtle:

and 2 bonus ways for the extended edition:

All source code for this post is available on github.


The requirements for a Turtle

A turtle supports four instructions:

  • Move some distance in the current direction.
  • Turn a certain number of degrees clockwise or anticlockwise.
  • Put the pen down or up. When the pen is down, moving the turtle draws a line.
  • Set the pen color (one of black, blue or red).

These requirements lead naturally to some kind of “turtle interface” like this:

  • Move aDistance
  • Turn anAngle
  • PenUp
  • PenDown
  • SetColor aColor

All of the following implementations will be based on this interface or some variant of it.

Note that the turtle must convert these instructions to drawing lines on a canvas or other graphics context.
So the implementation will probably need to keep track of the turtle position and current state somehow.


Common code

Before we start implementing, let’s get some common code out of the way.

First, we’ll need some types to represent distances, angles, the pen state, and the pen colors.

  1. /// An alias for a float
  2. type Distance = float
  3. /// Use a unit of measure to make it clear that the angle is in degrees, not radians
  4. type [<Measure>] Degrees
  5. /// An alias for a float of Degrees
  6. type Angle = float<Degrees>
  7. /// Enumeration of available pen states
  8. type PenState = Up | Down
  9. /// Enumeration of available pen colors
  10. type PenColor = Black | Red | Blue

and we’ll also need a type to represent the position of the turtle:

  1. /// A structure to store the (x,y) coordinates
  2. type Position = {x:float; y:float}

We’ll also need a helper function to calculate a new position based on moving a certain distance at a certain angle:

  1. // round a float to two places to make it easier to read
  2. let round2 (flt:float) = Math.Round(flt,2)
  3. /// calculate a new position from the current position given an angle and a distance
  4. let calcNewPosition (distance:Distance) (angle:Angle) currentPos =
  5. // Convert degrees to radians with 180.0 degrees = 1 pi radian
  6. let angleInRads = angle * (Math.PI/180.0) * 1.0<1/Degrees>
  7. // current pos
  8. let x0 = currentPos.x
  9. let y0 = currentPos.y
  10. // new pos
  11. let x1 = x0 + (distance * cos angleInRads)
  12. let y1 = y0 + (distance * sin angleInRads)
  13. // return a new Position
  14. {x=round2 x1; y=round2 y1}

Let’s also define the initial state of a turtle:

  1. /// Default initial state
  2. let initialPosition,initialColor,initialPenState =
  3. {x=0.0; y=0.0}, Black, Down

And a helper that pretends to draw a line on a canvas:

  1. let dummyDrawLine log oldPos newPos color =
  2. // for now just log it
  3. log (sprintf "...Draw line from (%0.1f,%0.1f) to (%0.1f,%0.1f) using %A" oldPos.x oldPos.y newPos.x newPos.y color)

Now we’re ready for the first implementation!


1. Basic OO — A class with mutable state

In this first design, we will use an object-oriented approach and represent the turtle with a simple class.

  • The state will be stored in local fields (currentPosition, currentAngle, etc) that are mutable.
  • We will inject a logging function log so that we can monitor what happens.

Thirteen ways of looking at a turtle - 图1

And here’s the complete code, which should be self-explanatory:

  1. type Turtle(log) =
  2. let mutable currentPosition = initialPosition
  3. let mutable currentAngle = 0.0<Degrees>
  4. let mutable currentColor = initialColor
  5. let mutable currentPenState = initialPenState
  6. member this.Move(distance) =
  7. log (sprintf "Move %0.1f" distance)
  8. // calculate new position
  9. let newPosition = calcNewPosition distance currentAngle currentPosition
  10. // draw line if needed
  11. if currentPenState = Down then
  12. dummyDrawLine log currentPosition newPosition currentColor
  13. // update the state
  14. currentPosition <- newPosition
  15. member this.Turn(angle) =
  16. log (sprintf "Turn %0.1f" angle)
  17. // calculate new angle
  18. let newAngle = (currentAngle + angle) % 360.0<Degrees>
  19. // update the state
  20. currentAngle <- newAngle
  21. member this.PenUp() =
  22. log "Pen up"
  23. currentPenState <- Up
  24. member this.PenDown() =
  25. log "Pen down"
  26. currentPenState <- Down
  27. member this.SetColor(color) =
  28. log (sprintf "SetColor %A" color)
  29. currentColor <- color

Calling the turtle object

The client code instantiates the turtle and talks to it directly:

  1. /// Function to log a message
  2. let log message =
  3. printfn "%s" message
  4. let drawTriangle() =
  5. let turtle = Turtle(log)
  6. turtle.Move 100.0
  7. turtle.Turn 120.0<Degrees>
  8. turtle.Move 100.0
  9. turtle.Turn 120.0<Degrees>
  10. turtle.Move 100.0
  11. turtle.Turn 120.0<Degrees>
  12. // back home at (0,0) with angle 0

The logged output of drawTriangle() is:

  1. Move 100.0
  2. ...Draw line from (0.0,0.0) to (100.0,0.0) using Black
  3. Turn 120.0
  4. Move 100.0
  5. ...Draw line from (100.0,0.0) to (50.0,86.6) using Black
  6. Turn 120.0
  7. Move 100.0
  8. ...Draw line from (50.0,86.6) to (0.0,0.0) using Black
  9. Turn 120.0

Similarly, here’s the code to draw a polygon:

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let angleDegrees = angle * 1.0<Degrees>
  4. let turtle = Turtle(log)
  5. // define a function that draws one side
  6. let drawOneSide() =
  7. turtle.Move 100.0
  8. turtle.Turn angleDegrees
  9. // repeat for all sides
  10. for i in [1..n] do
  11. drawOneSide()

Note that drawOneSide() does not return anything — all the code is imperative and stateful. Compare this to the code in the next example, which takes a pure functional approach.

Advantages and disadvantages

So what are the advantages and disadvantages of this simple approach?

Advantages

  • It’s very easy to implement and understand.

Disadvantages

  • The stateful code is harder to test. We have to put an object into a known state state before testing,
    which is simple in this case, but can be long-winded and error-prone for more complex objects.
  • The client is coupled to a particular implementation. No interfaces here! We’ll look at using interfaces shortly.

The source code for this version is available here (turtle class)
and here (client).


2: Basic FP - A module of functions with immutable state

The next design will use a pure, functional approach. An immutable TurtleState is defined, and then the
various turtle functions accept a state as input and return a new state as output.

In this approach then, the client is responsible for keeping track of the current state and passing it into the next function call.

Thirteen ways of looking at a turtle - 图2

Here’s the definition of TurtleState and the values for the initial state:

  1. module Turtle =
  2. type TurtleState = {
  3. position : Position
  4. angle : float<Degrees>
  5. color : PenColor
  6. penState : PenState
  7. }
  8. let initialTurtleState = {
  9. position = initialPosition
  10. angle = 0.0<Degrees>
  11. color = initialColor
  12. penState = initialPenState
  13. }

And here are the “api” functions, all of which take a state parameter and return a new state:

  1. module Turtle =
  2. // [state type snipped]
  3. let move log distance state =
  4. log (sprintf "Move %0.1f" distance)
  5. // calculate new position
  6. let newPosition = calcNewPosition distance state.angle state.position
  7. // draw line if needed
  8. if state.penState = Down then
  9. dummyDrawLine log state.position newPosition state.color
  10. // update the state
  11. {state with position = newPosition}
  12. let turn log angle state =
  13. log (sprintf "Turn %0.1f" angle)
  14. // calculate new angle
  15. let newAngle = (state.angle + angle) % 360.0<Degrees>
  16. // update the state
  17. {state with angle = newAngle}
  18. let penUp log state =
  19. log "Pen up"
  20. {state with penState = Up}
  21. let penDown log state =
  22. log "Pen down"
  23. {state with penState = Down}
  24. let setColor log color state =
  25. log (sprintf "SetColor %A" color)
  26. {state with color = color}

Note that the state is always the last parameter — this makes it easier to use the “piping” idiom.

Using the turtle functions

The client now has to pass in both the log function and the state to every function, every time!

We can eliminate the need to pass in the log function by using partial application to create new versions of the functions with the logger baked in:

  1. /// Function to log a message
  2. let log message =
  3. printfn "%s" message
  4. // versions with log baked in (via partial application)
  5. let move = Turtle.move log
  6. let turn = Turtle.turn log
  7. let penDown = Turtle.penDown log
  8. let penUp = Turtle.penUp log
  9. let setColor = Turtle.setColor log

With these simpler versions, the client can just pipe the state through in a natural way:

  1. let drawTriangle() =
  2. Turtle.initialTurtleState
  3. |> move 100.0
  4. |> turn 120.0<Degrees>
  5. |> move 100.0
  6. |> turn 120.0<Degrees>
  7. |> move 100.0
  8. |> turn 120.0<Degrees>
  9. // back home at (0,0) with angle 0

When it comes to drawing a polygon, it’s a little more complicated, as we have to “fold” the state through the repetitions for each side:

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let angleDegrees = angle * 1.0<Degrees>
  4. // define a function that draws one side
  5. let oneSide state sideNumber =
  6. state
  7. |> move 100.0
  8. |> turn angleDegrees
  9. // repeat for all sides
  10. [1..n]
  11. |> List.fold oneSide Turtle.initialTurtleState

Advantages and disadvantages

What are the advantages and disadvantages of this purely functional approach?

Advantages

  • Again, it’s very easy to implement and understand.
  • The stateless functions are easier to test. We always provide the current state as input, so there is no setup needed to get an object into a known state.
  • Because there is no global state, the functions are modular and can be reused in other contexts (as we’ll see later in this post).

Disadvantages

  • As before, the client is coupled to a particular implementation.
  • The client has to keep track of the state (but some solutions to make this easier are shown later in this post).

The source code for this version is available here (turtle functions)
and here (client).


3: An API with a object-oriented core

Let’s hide the client from the implementation using an API!

In this case, the API will be string based, with text commands such as "move 100" or "turn 90". The API must validate
these commands and turn them into method calls on the turtle (we’ll use the OO approach of a stateful Turtle class again).

Thirteen ways of looking at a turtle - 图3

If the command is not valid, the API must indicate that to the client. Since we are using an OO approach, we’ll
do this by throwing a TurtleApiException containing a string, like this.

  1. exception TurtleApiException of string

Next we need some functions that validate the command text:

  1. // convert the distance parameter to a float, or throw an exception
  2. let validateDistance distanceStr =
  3. try
  4. float distanceStr
  5. with
  6. | ex ->
  7. let msg = sprintf "Invalid distance '%s' [%s]" distanceStr ex.Message
  8. raise (TurtleApiException msg)
  9. // convert the angle parameter to a float<Degrees>, or throw an exception
  10. let validateAngle angleStr =
  11. try
  12. (float angleStr) * 1.0<Degrees>
  13. with
  14. | ex ->
  15. let msg = sprintf "Invalid angle '%s' [%s]" angleStr ex.Message
  16. raise (TurtleApiException msg)
  17. // convert the color parameter to a PenColor, or throw an exception
  18. let validateColor colorStr =
  19. match colorStr with
  20. | "Black" -> Black
  21. | "Blue" -> Blue
  22. | "Red" -> Red
  23. | _ ->
  24. let msg = sprintf "Color '%s' is not recognized" colorStr
  25. raise (TurtleApiException msg)

With these in place, we can create the API.

The logic for parsing the command text is to split the command text into tokens and then
match the first token to "move", "turn", etc.

Here’s the code:

  1. type TurtleApi() =
  2. let turtle = Turtle(log)
  3. member this.Exec (commandStr:string) =
  4. let tokens = commandStr.Split(' ') |> List.ofArray |> List.map trimString
  5. match tokens with
  6. | [ "Move"; distanceStr ] ->
  7. let distance = validateDistance distanceStr
  8. turtle.Move distance
  9. | [ "Turn"; angleStr ] ->
  10. let angle = validateAngle angleStr
  11. turtle.Turn angle
  12. | [ "Pen"; "Up" ] ->
  13. turtle.PenUp()
  14. | [ "Pen"; "Down" ] ->
  15. turtle.PenDown()
  16. | [ "SetColor"; colorStr ] ->
  17. let color = validateColor colorStr
  18. turtle.SetColor color
  19. | _ ->
  20. let msg = sprintf "Instruction '%s' is not recognized" commandStr
  21. raise (TurtleApiException msg)

Using the API

Here’s how drawPolygon is implemented using the TurtleApi class:

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let api = TurtleApi()
  4. // define a function that draws one side
  5. let drawOneSide() =
  6. api.Exec "Move 100.0"
  7. api.Exec (sprintf "Turn %f" angle)
  8. // repeat for all sides
  9. for i in [1..n] do
  10. drawOneSide()

You can see that the code is quite similar to the earlier OO version,
with the direct call turtle.Move 100.0 being replaced with the indirect API call api.Exec "Move 100.0".

Now if we trigger an error with a bad command such as api.Exec "Move bad", like this:

  1. let triggerError() =
  2. let api = TurtleApi()
  3. api.Exec "Move bad"

then the expected exception is thrown:

  1. Exception of type 'TurtleApiException' was thrown.

Advantages and disadvantages

What are the advantages and disadvantages of an API layer like this?

  • The turtle implementation is now hidden from the client.
  • An API at a service boundary supports validation and can be extended to support monitoring, internal routing, load balancing, etc.

Disadvantages

  • The API is coupled to a particular implementation, even though the client isn’t.
  • The system is very stateful. Even though the client does not know about the implementation behind the API,
    the client is still indirectly coupled to the inner core via shared state which in turn can make testing harder.

The source code for this version is available here.


4: An API with a functional core

An alternative approach for this scenario is to use a hybrid design, where the core of the application consists of pure functions, while the boundaries are imperative and stateful.

This approach has been named “Functional Core/Imperative Shell” by Gary Bernhardt.

Applied to our API example, the API layer uses only pure turtle functions,
but the API layer manages the state (rather than the client) by storing a mutable turtle state.

Also, to be more functional, the API will not throw exceptions if the command text is not valid,
but instead will return a Result value with Success and Failure cases, where the Failure case is used for any errors.
(See my talk on the functional approach to error handling for a more in depth discussion of this technique).

Thirteen ways of looking at a turtle - 图4

Let’s start by implementing the API class. This time it contains a mutable turtle state:

  1. type TurtleApi() =
  2. let mutable state = initialTurtleState
  3. /// Update the mutable state value
  4. let updateState newState =
  5. state <- newState

The validation functions no longer throw an exception, but return Success or Failure:

  1. let validateDistance distanceStr =
  2. try
  3. Success (float distanceStr)
  4. with
  5. | ex ->
  6. Failure (InvalidDistance distanceStr)

The error cases are documented in their own type:

  1. type ErrorMessage =
  2. | InvalidDistance of string
  3. | InvalidAngle of string
  4. | InvalidColor of string
  5. | InvalidCommand of string

Now because the validation functions now return a Result<Distance> rather than a “raw” distance, the move function needs to be lifted to
the world of Results, as does the current state.

There are three functions that we will use when working with Results: returnR, mapR and lift2R.

  • returnR transforms a “normal” value into a value in the world of Results:

Thirteen ways of looking at a turtle - 图5

  • mapR transforms a “normal” one-parameter function into a one-parameter function in the world of Results:

Thirteen ways of looking at a turtle - 图6

  • lift2R transforms a “normal” two-parameter function into a two-parameter function in the world of Results:

Thirteen ways of looking at a turtle - 图7

As an example, with these helper functions, we can turn the normal move function into a function in the world of Results:

  • The distance parameter is already in Result world
  • The state parameter is lifted into Result world using returnR
  • The move function is lifted into Result world using lift2R
  1. // lift current state to Result
  2. let stateR = returnR state
  3. // get the distance as a Result
  4. let distanceR = validateDistance distanceStr
  5. // call "move" lifted to the world of Results
  6. lift2R move distanceR stateR

(For more details on lifting functions to Result world, see the post on “lifting” in general )

Here’s the complete code for Exec:

  1. /// Execute the command string, and return a Result
  2. /// Exec : commandStr:string -> Result<unit,ErrorMessage>
  3. member this.Exec (commandStr:string) =
  4. let tokens = commandStr.Split(' ') |> List.ofArray |> List.map trimString
  5. // lift current state to Result
  6. let stateR = returnR state
  7. // calculate the new state
  8. let newStateR =
  9. match tokens with
  10. | [ "Move"; distanceStr ] ->
  11. // get the distance as a Result
  12. let distanceR = validateDistance distanceStr
  13. // call "move" lifted to the world of Results
  14. lift2R move distanceR stateR
  15. | [ "Turn"; angleStr ] ->
  16. let angleR = validateAngle angleStr
  17. lift2R turn angleR stateR
  18. | [ "Pen"; "Up" ] ->
  19. returnR (penUp state)
  20. | [ "Pen"; "Down" ] ->
  21. returnR (penDown state)
  22. | [ "SetColor"; colorStr ] ->
  23. let colorR = validateColor colorStr
  24. lift2R setColor colorR stateR
  25. | _ ->
  26. Failure (InvalidCommand commandStr)
  27. // Lift `updateState` into the world of Results and
  28. // call it with the new state.
  29. mapR updateState newStateR
  30. // Return the final result (output of updateState)

Using the API

The API returns a Result, so the client can no longer call each function in sequence, as we need to handle any errors coming
from a call and abandon the rest of the steps.

To make our lives easier, we’ll use a result computation expression (or workflow) to chain the calls and preserve the imperative “feel” of the OO version.

  1. let drawTriangle() =
  2. let api = TurtleApi()
  3. result {
  4. do! api.Exec "Move 100"
  5. do! api.Exec "Turn 120"
  6. do! api.Exec "Move 100"
  7. do! api.Exec "Turn 120"
  8. do! api.Exec "Move 100"
  9. do! api.Exec "Turn 120"
  10. }

The source code for the result computation expression is available here.

Similarly, for the drawPolygon code, we can create a helper to draw one side and then call it n times inside a result expression.

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let api = TurtleApi()
  4. // define a function that draws one side
  5. let drawOneSide() = result {
  6. do! api.Exec "Move 100.0"
  7. do! api.Exec (sprintf "Turn %f" angle)
  8. }
  9. // repeat for all sides
  10. result {
  11. for i in [1..n] do
  12. do! drawOneSide()
  13. }

The code looks imperative, but is actually purely functional, as the returned Result values are being handled transparently by the result workflow.

Advantages and disadvantages

Advantages

  • The same as for the OO version of an API — the turtle implementation is hidden from the client, validation can be done, etc.
  • The only stateful part of the system is at the boundary. The core is stateless which makes testing easier.

Disadvantages

  • The API is still coupled to a particular implementation.

The source code for this version is available here (api helper functions)
and here (API and client).


5: An API in front of an agent

In this design, an API layer communicates with a TurtleAgent via a message queue
and the client talks to the API layer as before.

Thirteen ways of looking at a turtle - 图8

There are no mutables in the API (or anywhere). The TurtleAgent manages state by
storing the current state as a parameter in the recursive message processing loop.

Now because the TurtleAgent has a typed message queue, where all messages are the same type,
we must combine all possible commands into a single discriminated union type (TurtleCommand).

  1. type TurtleCommand =
  2. | Move of Distance
  3. | Turn of Angle
  4. | PenUp
  5. | PenDown
  6. | SetColor of PenColor

The agent implementation is similar to the previous ones, but rather than exposing the turtle functions directly,
we now do pattern matching on the incoming command to decide which function to call:

  1. type TurtleAgent() =
  2. /// Function to log a message
  3. let log message =
  4. printfn "%s" message
  5. // logged versions
  6. let move = Turtle.move log
  7. let turn = Turtle.turn log
  8. let penDown = Turtle.penDown log
  9. let penUp = Turtle.penUp log
  10. let setColor = Turtle.setColor log
  11. let mailboxProc = MailboxProcessor.Start(fun inbox ->
  12. let rec loop turtleState = async {
  13. // read a command message from teh queue
  14. let! command = inbox.Receive()
  15. // create a new state from handling the message
  16. let newState =
  17. match command with
  18. | Move distance ->
  19. move distance turtleState
  20. | Turn angle ->
  21. turn angle turtleState
  22. | PenUp ->
  23. penUp turtleState
  24. | PenDown ->
  25. penDown turtleState
  26. | SetColor color ->
  27. setColor color turtleState
  28. return! loop newState
  29. }
  30. loop Turtle.initialTurtleState )
  31. // expose the queue externally
  32. member this.Post(command) =
  33. mailboxProc.Post command

Sending a command to the Agent

The API calls the agent by constructing a TurtleCommand and posting it to the agent’s queue.

This time, rather than using the previous approach of “lifting” the move command:

  1. let stateR = returnR state
  2. let distanceR = validateDistance distanceStr
  3. lift2R move distanceR stateR

we’ll use the result computation expression instead, so the code above would have looked like this:

  1. result {
  2. let! distance = validateDistance distanceStr
  3. move distance state
  4. }

In the agent implementation, we are not calling a move command, but instead creating the Move case of the Command type, so the code looks like:

  1. result {
  2. let! distance = validateDistance distanceStr
  3. let command = Move distance
  4. turtleAgent.Post command
  5. }

Here’s the complete code:

  1. member this.Exec (commandStr:string) =
  2. let tokens = commandStr.Split(' ') |> List.ofArray |> List.map trimString
  3. // calculate the new state
  4. let result =
  5. match tokens with
  6. | [ "Move"; distanceStr ] -> result {
  7. let! distance = validateDistance distanceStr
  8. let command = Move distance
  9. turtleAgent.Post command
  10. }
  11. | [ "Turn"; angleStr ] -> result {
  12. let! angle = validateAngle angleStr
  13. let command = Turn angle
  14. turtleAgent.Post command
  15. }
  16. | [ "Pen"; "Up" ] -> result {
  17. let command = PenUp
  18. turtleAgent.Post command
  19. }
  20. | [ "Pen"; "Down" ] -> result {
  21. let command = PenDown
  22. turtleAgent.Post command
  23. }
  24. | [ "SetColor"; colorStr ] -> result {
  25. let! color = validateColor colorStr
  26. let command = SetColor color
  27. turtleAgent.Post command
  28. }
  29. | _ ->
  30. Failure (InvalidCommand commandStr)
  31. // return any errors
  32. result

Advantages and disadvantages of the Agent approach

Advantages

  • A great way to protect mutable state without using locks.
  • The API is decoupled from a particular implementation via the message queue. The TurtleCommand acts as a sort of protocol that decouples the two ends of the queue.
  • The turtle agent is naturally asynchronous.
  • Agents can easily be scaled horizontally.

Disadvantages

  • Agents are stateful and have the same problem as stateful objects:
    • It is harder to reason about your code.
    • Testing is harder.
    • It is all too easy to create a web of complex dependencies between actors.
  • A robust implementation for agents can get quite complex, as you may need support for supervisors, heartbeats, back pressure, etc.

The source code for this version is available here .


6: Dependency injection using interfaces

All the implementations so far have been tied to a specific implementation of the turtle functions, with the exception of the Agent version, where the API communicated indirectly via a queue.

So let’s now look at some ways of decoupling the API from the implementation.

Designing an interface, object-oriented style

We’ll start with the classic OO way of decoupling implementations: using interfaces.

Applying that approach to the turtle domain, we can see that our API layer will need to communicate with a ITurtle interface rather than a specific turtle implementation.
The client injects the turtle implementation later, via the API’s constructor.

Here’s the interface definition:

  1. type ITurtle =
  2. abstract Move : Distance -> unit
  3. abstract Turn : Angle -> unit
  4. abstract PenUp : unit -> unit
  5. abstract PenDown : unit -> unit
  6. abstract SetColor : PenColor -> unit

Note that there are a lot of units in these functions. A unit in a function signature implies side effects, and indeed the TurtleState is not used anywhere,
as this is a OO-based approach where the mutable state is encapsulated in the object.

Next, we need to change the API layer to use the interface by injecting it in the constructor for TurtleApi.
Other than that, the rest of the API code is unchanged, as shown by the snippet below:

  1. type TurtleApi(turtle: ITurtle) =
  2. // other code
  3. member this.Exec (commandStr:string) =
  4. let tokens = commandStr.Split(' ') |> List.ofArray |> List.map trimString
  5. match tokens with
  6. | [ "Move"; distanceStr ] ->
  7. let distance = validateDistance distanceStr
  8. turtle.Move distance
  9. | [ "Turn"; angleStr ] ->
  10. let angle = validateAngle angleStr
  11. turtle.Turn angle
  12. // etc

Creating some implementations of an OO interface

Now let’s create and test some implementations.

The first implementation will be called normalSize and will be the original one. The second will be called halfSize and will reduce
all the distances by half.

For normalSize we could go back and retrofit the orginal Turtle class to support the ITurtle interface. But I hate having to change
working code! Instead, we can create a “proxy” wrapper around the orginal Turtle class, where the proxy implements the new interface.

In some languages, creating proxy wrappers can be long-winded, but in F# you can use object expressions to implement an interface quickly:

  1. let normalSize() =
  2. let log = printfn "%s"
  3. let turtle = Turtle(log)
  4. // return an interface wrapped around the Turtle
  5. {new ITurtle with
  6. member this.Move dist = turtle.Move dist
  7. member this.Turn angle = turtle.Turn angle
  8. member this.PenUp() = turtle.PenUp()
  9. member this.PenDown() = turtle.PenDown()
  10. member this.SetColor color = turtle.SetColor color
  11. }

And to create the halfSize version, we do the same thing, but intercept the calls to Move and halve the distance parameter:

  1. let halfSize() =
  2. let normalSize = normalSize()
  3. // return a decorated interface
  4. {new ITurtle with
  5. member this.Move dist = normalSize.Move (dist/2.0) // halved!!
  6. member this.Turn angle = normalSize.Turn angle
  7. member this.PenUp() = normalSize.PenUp()
  8. member this.PenDown() = normalSize.PenDown()
  9. member this.SetColor color = normalSize.SetColor color
  10. }

This is actually the “decorator” pattern at work:
we’re wrapping normalSize in a proxy with an identical interface, then changing the behavior for some of the methods, while passing others though untouched.

Injecting dependencies, OO style

Now let’s look at the client code that injects the dependencies into the API.

First, some code to draw a triangle, where a TurtleApi is passed in:

  1. let drawTriangle(api:TurtleApi) =
  2. api.Exec "Move 100"
  3. api.Exec "Turn 120"
  4. api.Exec "Move 100"
  5. api.Exec "Turn 120"
  6. api.Exec "Move 100"
  7. api.Exec "Turn 120"

And now let’s try drawing the triangle by instantiating the API object with the normal interface:

  1. let iTurtle = normalSize() // an ITurtle type
  2. let api = TurtleApi(iTurtle)
  3. drawTriangle(api)

Obviously, in a real system, the dependency injection would occur away from the call site, using an IoC container or similar.

If we run it, the output of drawTriangle is just as before:

  1. Move 100.0
  2. ...Draw line from (0.0,0.0) to (100.0,0.0) using Black
  3. Turn 120.0
  4. Move 100.0
  5. ...Draw line from (100.0,0.0) to (50.0,86.6) using Black
  6. Turn 120.0
  7. Move 100.0
  8. ...Draw line from (50.0,86.6) to (0.0,0.0) using Black
  9. Turn 120.0

And now with the half-size interface..

  1. let iTurtle = halfSize()
  2. let api = TurtleApi(iTurtle)
  3. drawTriangle(api)

…the output is, as we hoped, half the size!

  1. Move 50.0
  2. ...Draw line from (0.0,0.0) to (50.0,0.0) using Black
  3. Turn 120.0
  4. Move 50.0
  5. ...Draw line from (50.0,0.0) to (25.0,43.3) using Black
  6. Turn 120.0
  7. Move 50.0
  8. ...Draw line from (25.0,43.3) to (0.0,0.0) using Black
  9. Turn 120.0

Designing an interface, functional style

In a pure FP world, OO-style interfaces do not exist. However, you can emulate them by using a record containing functions, with one function for each method in the interface.

So let’s create a alternative version of dependency injection, where this time the API layer will use a record of functions rather than an interface.

A record of functions is a normal record, but the types of the fields are function types. Here’s the definition we’ll use:

  1. type TurtleFunctions = {
  2. move : Distance -> TurtleState -> TurtleState
  3. turn : Angle -> TurtleState -> TurtleState
  4. penUp : TurtleState -> TurtleState
  5. penDown : TurtleState -> TurtleState
  6. setColor : PenColor -> TurtleState -> TurtleState
  7. }

Note that there are no units in these function signatures, unlike the OO version. Instead, the TurtleState is explicitly passed in and returned.

Also note that there is no logging either. The logging method will be baked in to the functions when the record is created.

The TurtleApi constructor now takes a TurtleFunctions record rather than an ITurtle, but as these functions are pure,
the API needs to manage the state again with a mutable field.

  1. type TurtleApi(turtleFunctions: TurtleFunctions) =
  2. let mutable state = initialTurtleState

The implementation of the main Exec method is very similar to what we have seen before, with these differences:

  • The function is fetched from the record (e.g. turtleFunctions.move).
  • All the activity takes place in a result computation expression so that the result of the validations can be used.

Here’s the code:

  1. member this.Exec (commandStr:string) =
  2. let tokens = commandStr.Split(' ') |> List.ofArray |> List.map trimString
  3. // return Success of unit, or Failure
  4. match tokens with
  5. | [ "Move"; distanceStr ] -> result {
  6. let! distance = validateDistance distanceStr
  7. let newState = turtleFunctions.move distance state
  8. updateState newState
  9. }
  10. | [ "Turn"; angleStr ] -> result {
  11. let! angle = validateAngle angleStr
  12. let newState = turtleFunctions.turn angle state
  13. updateState newState
  14. }
  15. // etc

Creating some implementations of a “record of functions”

Noe let’s create some implementations.

Again, we’ll have a normalSize implementation and a halfSize implementation.

For normalSize we just need to use the functions from the original Turtle module, with the logging baked in using partial application:

  1. let normalSize() =
  2. let log = printfn "%s"
  3. // return a record of functions
  4. {
  5. move = Turtle.move log
  6. turn = Turtle.turn log
  7. penUp = Turtle.penUp log
  8. penDown = Turtle.penDown log
  9. setColor = Turtle.setColor log
  10. }

And to create the halfSize version, we clone the record, and change just the move function:

  1. let halfSize() =
  2. let normalSize = normalSize()
  3. // return a reduced turtle
  4. { normalSize with
  5. move = fun dist -> normalSize.move (dist/2.0)
  6. }

What’s nice about cloning records rather than proxying interfaces is that we don’t have to reimplement every function in the record, just the ones we care about.

Injecting dependencies again

The client code that injects the dependencies into the API is implemented just as you expect. The API is a class with a constructor,
and so the record of functions can be passed into the constructor in exactly the same way that the ITurtle interface was:

  1. let turtleFns = normalSize() // a TurtleFunctions type
  2. let api = TurtleApi(turtleFns)
  3. drawTriangle(api)

As you can see, the client code in the ITurtle version and TurtleFunctions version looks identical! If it wasn’t for the different types, you could not tell them apart.

Advantages and disadvantages of using interfaces

The OO-style interface and the FP-style “record of functions” are very similar, although the FP functions are stateless, unlike the OO interface.

Advantages

  • The API is decoupled from a particular implementation via the interface.
  • For the FP “record of functions” approach (compared to OO interfaces):
    • Records of functions can be cloned more easily than interfaces.
    • The functions are stateless

Disadvantages

  • Interfaces are more monolithic than individual functions and can easily grow to include too many unrelated methods,
    breaking the Interface Segregation Principle if care is not taken.
  • Interfaces are not composable (unlike individual functions).
  • For more on the problems with this approach, see this Stack Overflow answer by Mark Seemann.
  • For the OO interface approach in particular:
    • You may have to modify existing classes when refactoring to an interface.
  • For the FP “record of functions” approach:
    • Less tooling support, and poor interop, compared to OO interfaces.

The source code for these versions is available here (interface)
and here (record of functions).


7: Dependency injection using functions

The two main disadvantages of the “interface” approach is that interfaces are not composable,
and they break the “pass in only the dependencies you need” rule, which is a key part of functional design.

In a true functional approach, we would pass in functions. That is, the API layer communicates via one or more functions that are passed in as parameters to the API call.
These functions are typically partially applied so that the call site is decoupled from the “injection”.

No interface is passed to the constructor as generally there is no constructor! (I’m only using a API class here to wrap the mutable turtle state.)

In the approach in this section, I’ll show two alternatives which use function passing to inject dependencies:

  • In the first approach, each dependency (turtle function) is passed separately.
  • In the second approach, only one function is passed in. So to determine which specific turtle function is used, a discriminated union type is defined.

Approach 1 - passing in each dependency as a separate function

The simplest way to manage dependencies is always just to pass in all dependencies as parameters to the function that needs them.

In our case, the Exec method is the only function that needs to control the turtle, so we can pass them in there directly:

  1. member this.Exec move turn penUp penDown setColor (commandStr:string) =
  2. ...

To stress that point again: in this approach dependencies are always passed “just in time”, to the function that needs them. No dependencies are used in the constructor and then used later.

Here’s a bigger snippet of the Exec method using those functions:

  1. member this.Exec move turn penUp penDown setColor (commandStr:string) =
  2. ...
  3. // return Success of unit, or Failure
  4. match tokens with
  5. | [ "Move"; distanceStr ] -> result {
  6. let! distance = validateDistance distanceStr
  7. let newState = move distance state // use `move` function that was passed in
  8. updateState newState
  9. }
  10. | [ "Turn"; angleStr ] -> result {
  11. let! angle = validateAngle angleStr
  12. let newState = turn angle state // use `turn` function that was passed in
  13. updateState newState
  14. }
  15. ...

Using partial application to bake in an implementation

To create a normal or half-size version of Exec, we just pass in different functions:

  1. let log = printfn "%s"
  2. let move = Turtle.move log
  3. let turn = Turtle.turn log
  4. let penUp = Turtle.penUp log
  5. let penDown = Turtle.penDown log
  6. let setColor = Turtle.setColor log
  7. let normalSize() =
  8. let api = TurtleApi()
  9. // partially apply the functions
  10. api.Exec move turn penUp penDown setColor
  11. // the return value is a function:
  12. // string -> Result<unit,ErrorMessage>
  13. let halfSize() =
  14. let moveHalf dist = move (dist/2.0)
  15. let api = TurtleApi()
  16. // partially apply the functions
  17. api.Exec moveHalf turn penUp penDown setColor
  18. // the return value is a function:
  19. // string -> Result<unit,ErrorMessage>

In both cases we are returning a function of type string -> Result<unit,ErrorMessage>.

Using a purely functional API

So now when we want to draw something, we need only pass in any function of type string -> Result<unit,ErrorMessage>. The TurtleApi is no longer needed or mentioned!

  1. // the API type is just a function
  2. type ApiFunction = string -> Result<unit,ErrorMessage>
  3. let drawTriangle(api:ApiFunction) =
  4. result {
  5. do! api "Move 100"
  6. do! api "Turn 120"
  7. do! api "Move 100"
  8. do! api "Turn 120"
  9. do! api "Move 100"
  10. do! api "Turn 120"
  11. }

And here is how the API would be used:

  1. let apiFn = normalSize() // string -> Result<unit,ErrorMessage>
  2. drawTriangle(apiFn)
  3. let apiFn = halfSize()
  4. drawTriangle(apiFn)

So, although we did have mutable state in the TurtleApi, the final “published” api is a function that hides that fact.

This approach of having the api be a single function makes it very easy to mock for testing!

  1. let mockApi s =
  2. printfn "[MockAPI] %s" s
  3. Success ()
  4. drawTriangle(mockApi)

Approach 2 - passing a single function that handles all commands

In the version above, we passed in 5 separate functions!

Generally, when you are passing in more than three or four parameters, that implies that your design needs tweaking. You shouldn’t really need that many, if the functions are truly independent.

But in our case, the five functions are not independent — they come as a set — so how can we pass them in together without using a “record of functions” approach?

The trick is to pass in just one function! But how can one function handle five different actions? Easy - by using a discriminated union to represent the possible commands.

We’ve seen this done before in the agent example, so let’s revisit that type again:

  1. type TurtleCommand =
  2. | Move of Distance
  3. | Turn of Angle
  4. | PenUp
  5. | PenDown
  6. | SetColor of PenColor

All we need now is a function that handles each case of that type.

Befor we do that though, let’s look at the changes to the Exec method implementation:

  1. member this.Exec turtleFn (commandStr:string) =
  2. ...
  3. // return Success of unit, or Failure
  4. match tokens with
  5. | [ "Move"; distanceStr ] -> result {
  6. let! distance = validateDistance distanceStr
  7. let command = Move distance // create a Command object
  8. let newState = turtleFn command state
  9. updateState newState
  10. }
  11. | [ "Turn"; angleStr ] -> result {
  12. let! angle = validateAngle angleStr
  13. let command = Turn angle // create a Command object
  14. let newState = turtleFn command state
  15. updateState newState
  16. }
  17. ...

Note that a command object is being created and then the turtleFn parameter is being called with it.

And by the way, this code is very similar to the agent implementation, which used turtleAgent.Post command rather than newState = turtleFn command state:

Using partial application to bake in an implementation

Let’s create the two implementations using this approach:

  1. let log = printfn "%s"
  2. let move = Turtle.move log
  3. let turn = Turtle.turn log
  4. let penUp = Turtle.penUp log
  5. let penDown = Turtle.penDown log
  6. let setColor = Turtle.setColor log
  7. let normalSize() =
  8. let turtleFn = function
  9. | Move dist -> move dist
  10. | Turn angle -> turn angle
  11. | PenUp -> penUp
  12. | PenDown -> penDown
  13. | SetColor color -> setColor color
  14. // partially apply the function to the API
  15. let api = TurtleApi()
  16. api.Exec turtleFn
  17. // the return value is a function:
  18. // string -> Result<unit,ErrorMessage>
  19. let halfSize() =
  20. let turtleFn = function
  21. | Move dist -> move (dist/2.0)
  22. | Turn angle -> turn angle
  23. | PenUp -> penUp
  24. | PenDown -> penDown
  25. | SetColor color -> setColor color
  26. // partially apply the function to the API
  27. let api = TurtleApi()
  28. api.Exec turtleFn
  29. // the return value is a function:
  30. // string -> Result<unit,ErrorMessage>

As before, in both cases we are returning a function of type string -> Result<unit,ErrorMessage>,. which we can pass into the drawTriangle function we defined earlier:

  1. let api = normalSize()
  2. drawTriangle(api)
  3. let api = halfSize()
  4. drawTriangle(api)

Advantages and disadvantages of using functions

Advantages

  • The API is decoupled from a particular implementation via parameterization.
  • Because dependencies are passed in at the point of use (“in your face”) rather than in a constructor (“out of sight”), the tendency for dependencies to multiply is greatly reduced.
  • Any function parameter is automatically a “one method interface” so no retrofitting is needed.
  • Regular partial application can be used to bake in parameters for “dependency injection”. No special pattern or IoC container is needed.

Disadvantages

  • If the number of dependent functions is too great (say more than four) passing them all in as separate parameters can become awkward (hence, the second approach).
  • The discriminated union type can be trickier to work with than an interface.

The source code for these versions is available here (five function params)
and here (one function param).


8: Batch processing using a state monad

In the next two sections, we’ll switch from “interactive” mode, where instructions are processed one at a time, to “batch” mode,
where a whole series of instructions are grouped together and then run as one unit.

In the first design, we’ll go back to the model where the client uses the Turtle functions directly.

Just as before, the client must keep track of the current state and pass it into the next function call,
but this time we’ll keep the state out of sight by using a so-called “state monad” to thread the state through the various instructions.
As a result, there are no mutables anywhere!

This won’t be a generalized state monad, but a simplified one just for this demonstration. I’ll call it the turtle workflow.

(For more on the state monad see my “monadster” talk and post and post on parser combinators )

Thirteen ways of looking at a turtle - 图9

Defining the turtle workflow

The core turtle functions that we defined at the very beginning follow the same “shape” as many other state-transforming functions, an input plus the turtle state, and the output plus the turtle state.

Thirteen ways of looking at a turtle - 图10

(It’s true that, so far. we have not had any useable output from the turtle functions, but in a later example we will see this output being used to make decisions.)

There is a standard way to deal with these kinds of functions — the “state monad”.

Let’s look at how this is built.

First, note that, thanks to currying, we can recast a function in this shape into two separate one-parameter functions: processing the input generates another function that in turn has the state as the parameter:

Thirteen ways of looking at a turtle - 图11

We can then think of a turtle function as something that takes an input and returns a new function, like this:

Thirteen ways of looking at a turtle - 图12

In our case, using TurtleState as the state, the returned function will look like this:

  1. TurtleState -> 'a * TurtleState

Finally, to make it easier to work with, we can treat the returned function as a thing in its own right, give it a name such as TurtleStateComputation:

Thirteen ways of looking at a turtle - 图13

In the implementation, we would typically wrap the function with a single case discriminated union like this:

  1. type TurtleStateComputation<'a> =
  2. TurtleStateComputation of (Turtle.TurtleState -> 'a * Turtle.TurtleState)

So that is the basic idea behind the “state monad”. However, it’s important to realize that a state monad consists of more than just this type — you also need some functions (“return” and “bind”) that obey some sensible laws.

I won’t define the returnT and bindT functions here, but you can see their definitions in the full source.

We need some additional helper functions too. (I’m going to add a T for Turtle suffix to all the functions).

In particular, we need a way to feed some state into the TurtleStateComputation to “run” it:

  1. let runT turtle state =
  2. // pattern match against the turtle
  3. // to extract the inner function
  4. let (TurtleStateComputation innerFn) = turtle
  5. // run the inner function with the passed in state
  6. innerFn state

Finally, we can create a turtle workflow, which is a computation expression that makes it easier to work with the TurtleStateComputation type:

  1. // define a computation expression builder
  2. type TurtleBuilder() =
  3. member this.Return(x) = returnT x
  4. member this.Bind(x,f) = bindT f x
  5. // create an instance of the computation expression builder
  6. let turtle = TurtleBuilder()

Using the Turtle workflow

To use the turtle workflow, we first need to create “lifted” or “monadic” versions of the turtle functions:

  1. let move dist =
  2. toUnitComputation (Turtle.move log dist)
  3. // val move : Distance -> TurtleStateComputation<unit>
  4. let turn angle =
  5. toUnitComputation (Turtle.turn log angle)
  6. // val turn : Angle -> TurtleStateComputation<unit>
  7. let penDown =
  8. toUnitComputation (Turtle.penDown log)
  9. // val penDown : TurtleStateComputation<unit>
  10. let penUp =
  11. toUnitComputation (Turtle.penUp log)
  12. // val penUp : TurtleStateComputation<unit>
  13. let setColor color =
  14. toUnitComputation (Turtle.setColor log color)
  15. // val setColor : PenColor -> TurtleStateComputation<unit>

The toUnitComputation helper function does the lifting. Don’t worry about how it works, but the effect is that the original version of the move function (Distance -> TurtleState -> TurtleState)
is reborn as a function returning a TurtleStateComputation (Distance -> TurtleStateComputation<unit>)

Once we have these “monadic” versions, we can use them inside the turtle workflow like this:

  1. let drawTriangle() =
  2. // define a set of instructions
  3. let t = turtle {
  4. do! move 100.0
  5. do! turn 120.0<Degrees>
  6. do! move 100.0
  7. do! turn 120.0<Degrees>
  8. do! move 100.0
  9. do! turn 120.0<Degrees>
  10. }
  11. // finally, run them using the initial state as input
  12. runT t initialTurtleState

The first part of drawTriangle chains together six instructions, but importantly, does not run them.
Only when the runT function is used at the end are the instructions actually executed.

The drawPolygon example is a little more complicated. First we define a workflow for drawing one side:

  1. let oneSide = turtle {
  2. do! move 100.0
  3. do! turn angleDegrees
  4. }

But then we need a way of combining all the sides into a single workflow. There are a couple of ways of doing this. I’ll go with creating a pairwise combiner chain
and then using reduce to combine all the sides into one operation.

  1. // chain two turtle operations in sequence
  2. let chain f g = turtle {
  3. do! f
  4. do! g
  5. }
  6. // create a list of operations, one for each side
  7. let sides = List.replicate n oneSide
  8. // chain all the sides into one operation
  9. let all = sides |> List.reduce chain

Here’s the complete code for drawPolygon:

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let angleDegrees = angle * 1.0<Degrees>
  4. // define a function that draws one side
  5. let oneSide = turtle {
  6. do! move 100.0
  7. do! turn angleDegrees
  8. }
  9. // chain two turtle operations in sequence
  10. let chain f g = turtle {
  11. do! f
  12. do! g
  13. }
  14. // create a list of operations, one for each side
  15. let sides = List.replicate n oneSide
  16. // chain all the sides into one operation
  17. let all = sides |> List.reduce chain
  18. // finally, run them using the initial state
  19. runT all initialTurtleState

Advantages and disadvantages of the turtle workflow

Advantages

  • The client code is similar to imperative code, but preserves immutability.
  • The workflows are composable — you can define two workflows and then combine them to create another workflow.

Disadvantages

  • Coupled to a particular implementation of the turtle functions.
  • More complex than tracking state explicitly.
  • Stacks of nested monads/workflows are hard to work with.

As an example of that last point, let’s say we have a seq containing a result workflow containing a turtle workflow and we want to invert them so that the turtle workflow is on the outside.
How would you do that? It’s not obvious!

The source code for this version is available here.


9: Batch processing using command objects

Another batch-oriented approach is to reuse the TurtleCommand type in a new way. Instead of calling functions immediately,
the client creates a list of commands that will be run as a group.

When you “run” the list of commands, you can just execute each one in turn using the standard Turtle library functions,
using fold to thread the state through the sequence.

Thirteen ways of looking at a turtle - 图14

And since all the commands are run at once, this approach means that there is no state that needs to be persisted between calls by the client.

Here’s the TurtleCommand definition again:

  1. type TurtleCommand =
  2. | Move of Distance
  3. | Turn of Angle
  4. | PenUp
  5. | PenDown
  6. | SetColor of PenColor

To process a sequence of commands, we will need to fold over them, threading the state through,
so we need a function that applies a single command to a state and returns a new state:

  1. /// Apply a command to the turtle state and return the new state
  2. let applyCommand state command =
  3. match command with
  4. | Move distance ->
  5. move distance state
  6. | Turn angle ->
  7. turn angle state
  8. | PenUp ->
  9. penUp state
  10. | PenDown ->
  11. penDown state
  12. | SetColor color ->
  13. setColor color state

And then, to run all the commands, we just use fold:

  1. /// Run list of commands in one go
  2. let run aListOfCommands =
  3. aListOfCommands
  4. |> List.fold applyCommand Turtle.initialTurtleState

Running a batch of Commands

To draw a triangle, say, we just create a list of the commands and then run them:

  1. let drawTriangle() =
  2. // create the list of commands
  3. let commands = [
  4. Move 100.0
  5. Turn 120.0<Degrees>
  6. Move 100.0
  7. Turn 120.0<Degrees>
  8. Move 100.0
  9. Turn 120.0<Degrees>
  10. ]
  11. // run them
  12. run commands

Now, since the commands are just a collection, we can easily build bigger collections from smaller ones.

Here’s an example for drawPolygon, where drawOneSide returns a collection of commands, and that collection is duplicated for each side:

  1. let drawPolygon n =
  2. let angle = 180.0 - (360.0/float n)
  3. let angleDegrees = angle * 1.0<Degrees>
  4. // define a function that draws one side
  5. let drawOneSide sideNumber = [
  6. Move 100.0
  7. Turn angleDegrees
  8. ]
  9. // repeat for all sides
  10. let commands =
  11. [1..n] |> List.collect drawOneSide
  12. // run the commands
  13. run commands

Advantages and disadvantages of batch commands

Advantages

  • Simpler to construct and use than workflows or monads.
  • Only one function is coupled to a particular implementation. The rest of the client is decoupled.

Disadvantages

  • Batch oriented only.
  • Only suitable when control flow is not based on the response from a previous command.
    If you do need to respond to the result of each command, consider using the “interpreter” approach discussed later.

The source code for this version is available here.


Interlude: Conscious decoupling with data types

In three of the examples so far (the agent, functional dependency injection
and batch processing) we have used a Command type — a discriminated union containing a case for each API call.
We’ll also see something similar used for the event sourcing and interpreter approaches in the next post.

This is not an accident. One of the differences between object-oriented design and functional design is that OO design focuses on behavior, while functional design focuses on
data transformation.

As a result, their approach to decoupling differs too. OO designs prefer to provide decoupling by sharing bundles of encapsulated behavior (“interfaces”)
while functional designs prefer to provide decoupling by agreeing on a common data type, sometimes called a “protocol” (although I prefer to reserve that word for message exchange patterns).

Once that common data type is agreed upon, any function that emits that type can be connected to any function that consumes that type using regular function composition.

You can also think of the two approaches as analogous to the choice between RPC or message-oriented APIs in web services,
and just as message-based designs have many advantages over RPC,
so the data-based decoupling has similar advantages over the behavior-based decoupling.

Some advantages of decoupling using data include:

  • Using a shared data type means that composition is trivial. It is harder to compose behavior-based interfaces.
  • Every function is already “decoupled”, as it were, and so there is no need to retrofit existing functions when refactoring.
    At worst you might need to convert one data type to another, but that is easily accomplished using… moar functions and moar function composition!
  • Data structures are easy to serialize to remote services if and when you need to split your code into physically separate services.
  • Data structures are easy to evolve safely. For example, if I added a sixth turtle action, or removed an action, or changed the parameters of an action, the discriminated union type would change
    and all clients of the shared type would fail to compile until the sixth turtle action is accounted for, etc. On the other hand, if you didn’t
    want existing code to break, you can use a versioning-friendly data serialization format like protobuf.
    Neither of these options are as easy when interfaces are used.

Summary

The meme is spreading.
The turtle must be paddling.
“Thirteen ways of looking at a turtle”, by Wallace D Coriacea

Hello? Anyone still there? Thanks for making it this far!

So, time for a break! In the next post, we’ll cover the remaining four ways of looking at a turtle.

The source code for this post is available on github.