DEV Community

Vsevolod
Vsevolod

Posted on

GoF Functionalized

As a programmer with an object-oriented Java background, I'm used to thinking in terms like aggregation and inheritance. These and other OO concepts do not have direct counterparts in the functional world. Thus, the conversion, if you require one, might pose a challenge. Today, I'm going to tackle this, by providing you with a simple tool set for performing this mapping.

map:OOPFP map: OOP \rarr FP

Why the hell the article is named GoF then? Heh, just wanted it to sound gravely. Gang of Four patterns are the globally adopted set of the object-oriented best practices. My thought process was the following: if I can map GoF to FP, I'll be able to map any OO code to FP. I might be wrong here, but that's a solid foundation to say the least.

Are you going to explain GoF? No, I'm not. There are tons of other resources out there, which did this explanation better than I ever could. My favorite one being the Refactoring Guru. In fact, I've honestly stolen pattern usage examples from there to showcase below. On top of that, I will only cover the subset of GoF patterns to highlight the core techniques, which in turn should be enough to convert other patterns. So, to keep up with an article, you should be pretty fluent with the OOP version of GoF. At the beginning of each chapter, I'll link the object-oriented pattern that will be discussed. If you feel uncertain about some of them, take a pause there to rehearse.

Are you going to begin already? Yes, yes, right away, my patient reader.

Construction

To whet our appetite, let's start with an extremely simple pattern and an extremely simple conversion. The Builder one. Any object we create in our applications eventually should be used as an argument to something. Otherwise, it shouldn't be created in the first place (which is in fact true for lazy languages, eager ones create such objects and then just discard them). Thus, we could narrow down the core use case of any Builder to a representation of default and, more importantly, named arguments in an arbitrary order. Languages without all these three features require Builder to emulate them. For example, in Java default property is achieved by using method overloading. However, the other two features have no direct representation, therefore Builder is highly popular in Java code.

void drive(
    CarBuilder.createCar()
        .seats(2)
        .engine(new ElectricEngine())
        .build()
)
Enter fullscreen mode Exit fullscreen mode

The same example in Kotlin, which possesses all three features:

fun drive(
    Car(
        seats = 2,
        engine = ElectricEngine(),
    )
)
Enter fullscreen mode Exit fullscreen mode

The latter code does not look considerably more concise comparing to the former one, until you remember all the boilerplate required for the first one to compile and work. Anyway, being arguably cleaner, this is a mapping from OOP to FP, since the Kotlin example directly produces an immutable record from the provided values, while Java one mutates CarBuilder state in order to produce a Car object.

Ornamentation

Guessing from the title, the next pattern we are going to convert is The Decorator. This one contains the OOP bread and butter - inheritance and aggregation. I need to make a side note here: I'm intentionally using the term aggregation instead of the terms composition or association. Since the first one could be easily confused with the functional composition and the second one is quite a broad concept even in the object-oriented world.

The original shortened (and thus non-compilable) Java/OOP version looks like the following:

interface Notifier {
    void send(String message);
}

class BaseDecorator implements Notifier {
    BaseDecorator(Notifier notifier) { ... }

    void send(String message) {
        this.notifier.send(message);
    }
}

class SlackDecorator extends BaseDecorator {
    void send(String message) {
        super.send(message);
        sendSlackMessage(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Since send is essentially a function StringUnitString \rarr Unit , this hierarchy boils down to a single function:

fun slackNotification(
    baseNotification: (String) -> Unit = {},
): (String) -> Unit = { message ->
    baseNotification(message)
    sendSlackMessage(message)
}
Enter fullscreen mode Exit fullscreen mode

And is usable as:

val notifier = slackNotification(
    facebookNotification(
        smsNotification()
    )
)
notifier("Hello World!")
Enter fullscreen mode Exit fullscreen mode

As you might have grasped already, the functional tool we used here is named currying. Although Kotlin's currying looks less elegant than the one from the truly functional languages, the result is still much more concise than the original approach.

Reinforcing my point that these two tools can be reduced to one, in OOP there is already a known morphism from inheritance to aggregation. Additionally, it's not only known but is quite popular in a sense that for the most cases it's recommended to choose aggregation between the two. Functional paradigm removes this choice and leaves you with a single option, which can be considered as a form of inheritance, since more specific functions close over arguments passed to a more abstract ones. Rephrasing slightly, returned closure is a child of the parent function. Even more easily, we can convince ourselves that currying is a form of aggregation, since a curried function aggregates data and behavior and then at some point in time performs computations based on those aggregations.

Enumeration

From the title, you might have assumed that we're going to discuss The Iterator now. However, I'm proposing to review the more specific kind of enumeration, the enumeration of the tree leafs. I'm talking about The Composite. This one is essentially a tree of data and functional languages are famous for dealing with tree-like data structures. The original pattern has an interface with two children and a method to execute some logic for all off them:

interface Component {
    fun execute()
}

class Leaf(val value: String) : Component {
    override fun execute() = println()
}

class Composite(val components: List<Component>) : Component {
    override fun execute() = components.forEach(Component::execute)
}
Enter fullscreen mode Exit fullscreen mode

In OOP code, we see here an inheritance relation. But this inheritance has one distinctive property in comparison to the one we saw above in a Decorator - all children are defined in the same place and thus are known at the compile time. In the object-oriented world, this relation is usually called sealed inheritance. To contrast with an open one in Decorator. The difference is that in Decorator children aren't known beforehand, they might be added later in some library using your library, providing base decorator. In its core, a parent in the sealed inheritance is a sum of all children (i.e. in our example there are only two possible Components - Leaf or Composite). This is how the same hierarchy is written functionally:

data Component a = Leaf a | Composite [Component a]
Enter fullscreen mode Exit fullscreen mode

And this is how to execute our functional Composite:

execute :: Component String -> IO ()
execute (Leaf x) = putStrLn x
execute (Composite xs) = forM_ xs execute
Enter fullscreen mode Exit fullscreen mode

Per my taste, this looks strikingly more powerful than the OOP version (even in a quite concise Kotlin code). But that's debatable. What's not debatable is the fact that we have a one-to-one mapping between sealed inheritance and sum type.

Instead of stopping here, I would like to discuss one more notable conversion technique required for the Composite to become fully functional. In the purely OOP version of the pattern, the components of the Composites are stored in a mutable private field:

private List<Component> components;

void add(Component component) { 
    components.add(component); 
}

void remove(Component component) {
    components.remove(component);
}
Enter fullscreen mode Exit fullscreen mode

In the functional approach, a mutable private state is replaced by an immutable public value. I'm stressing on public here to underline the difference. In reality, these values are just passed from the outside, thus aren't "owned" by the receiver at all. The same functions in Haskell:

add :: (Eq a) => a -> [a] -> [a]
add x xs = if x `elem` xs then xs else xs ++ [x]

remove :: (Eq a) => a -> [a] -> [a]
remove x xs = [y | y <- xs, y /= x]
Enter fullscreen mode Exit fullscreen mode

The pattern here is the function OldStateNewStateOldState \rarr NewState , i.e. we're accepting some "state" and returning a new one without modifying the received argument. The first reaction of the OOP brain is usually "Wow, this might be cumbersome to use", however:

let a = Leaf "Hello"
    b = Leaf "World"
    c = Composite $ add b $ add a []
Enter fullscreen mode Exit fullscreen mode

Looks approximately as readable as the object-oriented analog.

Abstraction

The next conversion is quite obvious, but nevertheless required to be mentioned in order to cover the topic in its entirety. All (or almost all, being too lazy to verify this statement) GoF patterns use interfaces to provide useful abstractions for outside users. In OOP, an interface is a group of method signatures for the single receiver. The same idea in functional languages is represented by type classes. The expected and obvious difference being methods replaced by functions. Let's take The Chain of Responsibility as an example. Its handler can be roughly represented in Haskell as follows:

class Handler h where
  type Request h
  type Response h
  canHandle :: h -> Request h -> Bool
  handle :: h -> Request h -> Response h
Enter fullscreen mode Exit fullscreen mode

Note that receiver is now passed as a first argument to the handle and canHandle functions. In fact, similar convention is as well used in some languages featuring object-oriented paradigm (e.g. Python). So, it should be quite familiar. Next, let's define the chain itself:

chain :: (Handler h) => [h] -> Request h -> Response h
chain hs r = handle h r
  where
    h = fromJust $ find (`canHandle` r) hs
Enter fullscreen mode Exit fullscreen mode
  • You can again spot an immutable sequence of handlers as an argument instead of a mutable private collection in an object-oriented version.

To utilize our chain, we would need to create an instance of some handler. Keeping things simple, I'll use the one that works on plain integers:

newtype IntHandler = IntHandler Int

instance Handler IntHandler where
  type Request IntHandler = Int
  type Response IntHandler = String
  canHandle (IntHandler i) r = i == r
  handle (IntHandler i) _ = "Hello " ++ show i ++ "!"
Enter fullscreen mode Exit fullscreen mode

Finally, the client-side of the pattern:

chain [IntHandler 0, IntHandler 1] 0
Enter fullscreen mode Exit fullscreen mode

We're passing multiple handlers and then the actual value as a request.

While this looks familiar for an object-oriented brain, functional brains tend to study and adopt much more abstract type classes, commonly borrowed from mathematics. Different sources even separate this kind of abstractions into the so-called categorical programming. In our specific example, we could re-write the whole code above into a single line, using Foldable, Alternative and Applicative classes:

chain fs x = asum $ fs <*> pure x
Enter fullscreen mode Exit fullscreen mode

Neat, but quite crazy. In case you're not familiar with those three type classes, I'll leave you a homework to understand this functional categorical Chain of Responsibility totaling to thirty-three(!) characters in length.

The Ultimate Conversion

By this moment, we've covered all the techniques I had in my pocket. However, I want to quickly revise all of them with a single(!) pattern. The ultimate one. The one so popular that its use cases can easily cover the whole topic of our discussion... Tried guessing? Yes (or no, depending on your guess), it's The Singleton.

The most popular use case of it is to create a single instance of some interface. Let's say we have Named interface:

class Named n where
  name :: n -> String
Enter fullscreen mode Exit fullscreen mode

And then we create the unique noble counting duck, which has the unique noble name:

data CountingDuck = CountingDuck

instance Named CountingDuck where
  name _ = "Archibald The Counting Duck"
Enter fullscreen mode Exit fullscreen mode

A similar object-oriented use case of a Singleton is to inherit some open abstract class. Continuing with our example, Archibald can serve as a child to an abstract duck with a template for quaking behavior:

quack :: Named n => n -> String
quack duck = name duck ++ " quacking!"
Enter fullscreen mode Exit fullscreen mode

A bit different usage of a Singleton is to serve as a marker in the sealed hierarchy:

data Duck = Duck String | CountingDuck

quack :: Duck -> String
quack (Duck name) = name ++ "quacking!"
quack CountingDuck = "Noble quacking!"
Enter fullscreen mode Exit fullscreen mode

Same as in open hierarchy, Singleton CountingDuck always quacks the same way.

An interesting observation here, is an isomorphism between methods and pattern matching branches. In OOP, related functionalities are grouped with a single type, while in FP related types are grouped with a single function. In other words, FP is behavior-centric (i.e. to add a new behavior you need to change a single place and adding behaviors preserves backward-compatibility), while OOP is type-centric (i.e. to add a new type to a hierarchy you need to change a single place and adding new types to a hierarchy preserves backward-compatibility).

object DomesticDuck : Duck {
    fun name() ...
    fun quack() ...
}

object CountingDuck : Duck {
    fun name() ...
    fun quack() ...
}
Enter fullscreen mode Exit fullscreen mode

Becomes:

data Duck = DomesticDuck | CountingDuck

name DomesticDuck = ...
name CountingDuck = ...

quack DomesticDuck = ...
quack CountingDuck = ...
Enter fullscreen mode Exit fullscreen mode

This is the same controversy between a method and a function we saw several times already, but from a slightly different angle.

Lastly, I need to cover the use case, due to which in some circles Singleton is considered an anti-pattern. The global mutable state:

object CountingDuck {
    private var counter = 1

    fun count() {
        counter++
    }
}
// ... later on ...
CountingDuck.count()
Enter fullscreen mode Exit fullscreen mode

The direct conversion in Haskell, as expected, completely removes the notion of state:

module CountingDuck (count) where

count :: Int -> Int
count = succ

-- ... later on in another module ...
import CountingDuck (count)

CountingDuck.count 1
Enter fullscreen mode Exit fullscreen mode

After this, the anti part of the pattern is automatically annihilated. Although, in this example, using the associated function looks pointless, namespacing might improve readability in a scenario of using a Singleton for storing some global values:

object ServerSettings {
    val port = 8080
}
// ... later on ...
ServerSettings.port
Enter fullscreen mode Exit fullscreen mode

Since this is not really an object-oriented use case, in Haskell it looks almost the same way:

module ServerSettings (port) where

port = 8080

-- ... later on in another module ...
import ServerSettings (port)

ServerSettings.port
Enter fullscreen mode Exit fullscreen mode

The most notable difference being missing val qualifier: indeed, you don't need one, when vals are all you have.

Summary

As a conclusion, here is the proposed mapping from OOP to FP:

Sealed Inheritance  ==>  Sum Type
-------------------------------------------------
Open Inheritance    ==>  Currying
-------------------------------------------------
Aggregation         ==>  Currying
-------------------------------------------------
Interface           ==>  Type Class
-------------------------------------------------
a.method()          ==>  function(a)
-------------------------------------------------
state.change()      ==>  change :: State -> State
Enter fullscreen mode Exit fullscreen mode

If you think that there are some uncovered object-oriented features, I would be glad to see them in the comments for the discussion.

Hoping that the article was somewhat entertaining and maybe even useful. Wish to meet you again in the future ones!

Top comments (0)