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.
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()
)
The same example in Kotlin, which possesses all three features:
fun drive(
Car(
seats = 2,
engine = ElectricEngine(),
)
)
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);
}
}
Since send
is essentially a function
, this hierarchy boils down to a single function:
fun slackNotification(
baseNotification: (String) -> Unit = {},
): (String) -> Unit = { message ->
baseNotification(message)
sendSlackMessage(message)
}
And is usable as:
val notifier = slackNotification(
facebookNotification(
smsNotification()
)
)
notifier("Hello World!")
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)
}
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 Component
s - Leaf
or Composite
). This is how the same hierarchy is written functionally:
data Component a = Leaf a | Composite [Component a]
And this is how to execute our functional Composite:
execute :: Component String -> IO ()
execute (Leaf x) = putStrLn x
execute (Composite xs) = forM_ xs execute
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);
}
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]
The pattern here is the function
, 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 []
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
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
- 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 ++ "!"
Finally, the client-side of the pattern:
chain [IntHandler 0, IntHandler 1] 0
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
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
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"
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!"
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!"
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() ...
}
Becomes:
data Duck = DomesticDuck | CountingDuck
name DomesticDuck = ...
name CountingDuck = ...
quack DomesticDuck = ...
quack CountingDuck = ...
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()
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
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
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
The most notable difference being missing val
qualifier: indeed, you don't need one, when val
s 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
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)