As a result of studying functional programming, some thoughts appeared in my head that I want to share with you.
Design patterns and functional programming? How is this generally related?
In the minds of many developers who are accustomed to the object-oriented paradigm, one gets the impression that software design, as such, is inextricably linked with OOP and everything else is heresy. UML, mostly aimed at OOP, is used as a universal language for design - although it is not, of course. And we see how the world of object-oriented programming is gradually plunging into the abyss of criminal reengineering (1).
Because of this, the question of
choosing a programming paradigm is often not even raised. However, this question is very significant, and often the right answer gives great advantages (3). This, generally speaking, goes beyond what we used to call design - it is a question from the field of architecture.
Lyrical digression: the difference between architecture, design and implementation
Not so long ago, I came across a very interesting study - (2). It addresses the task of formalizing the concepts of "architecture", "design" and "implementation", which are most often used informally. And the authors manage to derive a very interesting criterion: the criterion Intension / Locality. I will not delve into the philosophy and just give a brief description of the criterion (this part is actually a translation) and my conclusions from it.
The property Intension (intension) means the ability of an entity to describe an infinite number of objects: for example, the concept of a prime number. It is opposite to the property of extensionality - the essence describes the final set of objects: for example, the concept of a country - members of NATO.
Locality property - an entity affects only a separate part of the system. Accordingly, globality - essence affects the entire system as a whole.
So, considering these two properties, the authors of this study compile the following table:

Using it is easy to determine what is related to the level of architecture, and that - to the level of design. And here is my conclusion: the
choice of programming paradigm, platform and language is a solution to the architecture level , since this choice is global (affects all parts of the system) and intensional (paradigms determine the ways to solve an infinite number of problems).
Nevertheless, it is not for me to solve such a global problem (to find the criteria for the selection of a suitable paradigm). Therefore, I decided to choose two already existing class of tasks and show that for them it is worthwhile to use not the usual for many OO approach, but the functional one, which has recently (deservedly) become more and more popular.
I chose classes of tasks using an unusual method - I took two OO design patterns and showed that they are, in essence, a limited implementation of the concept from the field of functional programming — a higher order function (higher-order function, further: FVP). The hypothesis was that the patterns
are well-established solutions to certain problems , and since problems arise and their well-established solutions, there are apparently some weaknesses and shortcomings that must be overcome. For the considered patterns this is indeed the case.
Incidentally, a similar approach was used in (5) and (6). In (6), it was generally stated that most patterns could be replaced, but a detailed analysis of each was not carried out. In (5) there was a more detailed discussion of Command and Strategy, but a little on the other side. I decided to do something more practical than in (6), and with other accents than in (5). So let's get started.
')
Higher-order functions
I think almost everyone in one form or another is familiar with this idea.
A higher order function is a function that takes as an argument or returns another function as its result.
This is made possible by the basic concept of functional programming: functions are values. It is worth noting that when we say that a function and a value in functional programming fully correspond to analogous concepts from mathematics, we mean exactly complete correspondence. It is the same. An example of widely used in mathematics FVP are the operators of differentiation, integration, and composition (generally speaking, this is close to the concept of an operator from functional analysis). The composition operator has direct expression in most languages ​​that support the functional paradigm. An example on F #:
let f = (+) 10 let g = (*) 2 let composition = f << g printfn "%i" <| g 15 printfn "%i" <| f 30 printfn "%i" <| composition 15
Conclusion:
30 40 40
Obviously, the entry f << g corresponds to the entry f (g (x)) or F â—‹ G.
To better understand this, I suggest paying attention to the type of the composition operator:
('a -> 'b) -> ('c -> 'a) -> 'c -> 'b
The parts of the description of the type of function in brackets are also the types of functions. That is, it is a function that takes as arguments:
- A function that takes as an argument the value of a generic type 'a and returns the value of a generic type' b
- A function that takes as its argument the value of the generic type 'c and returns the value of the generic type' a
- Value of type 'c
and returning a value of type 'b. In fact, it builds a function that takes as its argument a value of type 'c and returns a value of type' b, i.e. type can be rewritten as:
('a -> 'b) -> ('c -> 'a) -> ('c -> 'b)
FPPs allow for generalized behavior . Due to this, they improve the reusability of the code.
This can be used for different purposes - for example, for exception handling. Suppose that we have many code sections that can cause a certain set of exceptions. We can write the error-prone code itself as functions that we will pass as a parameter to another function that does exception handling. C # example:
private void CalculateAdditionalQuantityToIncreaseGain() {
And here is FVP, which handles exceptions:
private static void ExecuteErrorProneCode(Action procedure) { try { procedure();
Then to handle exceptions caused by any function, it suffices to write:
ExecuteErrorProneCode(CalculateAdditionalQuantityToIncreaseGain);
This significantly reduces the code if exception handlers have many and / or many functions that exceptions can be thrown, which need to be processed in the same type.
Also a classic example of highlighting common behavior is to use higher-order functions for sorting. Obviously, to perform the sorting, you must be able to compare the elements of the sorting collection with each other. A function that is passed as an argument to a sorting function acts as such a “comparator” - accordingly, the sorting function must be a FVP to ensure universality. In general, the ability to create FVP is a critical link in the chain of actions aimed at creating abstract generalized algorithms .
By the way, since FVP, like any functions, are values, they can be used to represent data. See this article about the Church presentation .
Command Pattern
The Command Design Pattern, like the Strategy, refers to the design behavioral patterns. Its main role is encapsulation of a certain function. This pattern has many uses, but most often it is used to do the following:
- Send requests to different recipients
- Build teams in the queue, keep logs, cancel requests
- Create complex operations from simple
- Implement the commands Undo (undo last action) and Redo (repeat last undone action)
In general, it looks like this:

I will consider an example with implementation of undo and redo - you can see a pure OO version of the implementation of this functionality here .
The distribution of roles in this diagram:

Here Filter corresponds to Receiver ', LoggingInvoker - Invoker ' at, IFilterCommand - ICommand . Here's how we will invoke operations (you can create teams both in Client 'e, passing them as a parameter to the Execute()
LoggingInvoker ' method, and in LoggingInvoker 'e itself - the choice depends on the specific situation):

And here is how we will cancel them:

performedOps and undoneOps are stacks that hold executed and canceled commands.
However, after reviewing the PID, it is pretty obvious that all this behavior can be implemented as a PID, if the selected language supports this feature. Indeed, an Invoker object can be replaced with a FVP that takes a function corresponding to a particular operation as an argument — we no longer need Command objects, because the functions themselves are values, and the ICommand interface, because its functions are performed by a type system of a language that supports the functional paradigm.
Let us give a scheme for replacing this pattern with a construction in the functional paradigm, which can perform the same functions:

On pseudocode (inspired F #), the corresponding functional implementation will look like this:
// // - type OpType = (DataType -> DataType) * DataType // let performedOps = Stack<OpType>() let undoneOps = Stack<OpType>() // LoggingInvoker - let execute operation data = let res = operation data // performedOps.Push(operation, data) // res // let undo () = if performedOps.Count > 0 then // undoneOps.Push(performedOps.Pop()) // Some (snd undoneOps.Peek()) // 'a option, . (5) (7) else None // let OperationOne data = ... let OperationTwo data = ... // OperationOne let mutable a = execute OperationOne data // let b <- undo ()
We pass the function we want to execute, the FER execute
. The execute
function keeps a stack of executed operations, performs an operation and returns the result of its execution. The undo function undoes the last operation performed.
This approach has some additional advantages over using the Command pattern:
- The resulting code is more natural, shorter and simpler.
- You can easily create macros and complex operations using composition or pipe-lining (for pipe-lining see (5) or (7)) simple operations
- You can create complex data structures that contain operations, for example for dynamic menu construction.
In addition, if we use a multi-paradigm language, we can combine the Command pattern and the approach shown here in different proportions of the OO pattern.
Many modern languages ​​support PCF to one degree or another. For example, in C # there is a delegate mechanism. An example of solving the problem of creating undo with the help of delegates can be found in (4).
Pattern Strategy
The Strategy pattern is designed to allow the client to choose one of several possible solutions to the problem. In OOP, the following construction is created for this:

The context contains a link to one of the implementations of the IStrategy interface, if necessary, to perform a certain operation, it refers to the method of this stored object. Changing objects - changing methods.
It is also easily converted to a functional style. This time we can use the list of functions to save possible strategies:

On pseudocode:
let strategyA data = ... let strategyB data = ... let useStrategy strategy data = ... strategy data ... useStrategy strategyA data
The functions strategyA
, strategyB
, ... are functions that implement possible strategies. The useStrategy
higher order useStrategy
applies the selected strategy to the data. The strategy is passed simply as an argument to the useStrategy
function.
In addition to a significant simplification and shortening of the code, this approach gives us an additional advantage - now we can easily create functions that are parameterized by several strategies at once, which, with the usual OO approach, leads to a very complex program structure. We can generally not set separate names for strategies using such a feature as anonymous functions, if they are simple enough to implement. For example, to sort data in the FP, you can use the FPP sort and pass as a parameter not the type that implements the IComparer interface, which implements the comparison method, as it is done in OOP, but simply the comparison operation itself:
let a = sort (<) data
findings
1. The correct choice of paradigm in accordance with the class of the problem being solved can often be a critical factor for the success of its solution. If your task belongs to the class of so-called. behavior-centric, it is worth thinking about using the functional approach.
2. Command and Strategy patterns are a limited implementation of higher order functions.
3. It is not necessary to switch to a purely functional language in order to use the advantages of the solution with the help of FVP - most modern mainstream languages ​​support FVP to some extent.
Recently, a large number of languages ​​have appeared, equally combining OO and the functional paradigm, many OO languages ​​have begun to acquire functionality. I hope that this article will help someone to better use the new features of their favorite programming languages. Good luck in job!
Sources
1. Criminal Overengineering
2. Architecture, Design, Implementation . Amnon H. Eden, Rick Kazman. Portland: B.N., 2003. 25th International Conference on Software Engineering - ICSE
3. Development by 50 Percent. Microsoft Case Studies. March 2010
4. Bishop, Judith. C # 3.0 Design Patterns. Sebastopol, California: O'Reilly, 2008.
5. Tomas Petricek, Jon Skeet. Functional Programming for the Real World. BM: Manning Publications, 2010.
6. Gabriel, Richard P. Objects Have Failed Slides DreamSongs.com.
7. Smith, Chris. Programming F #. Sebastopol, California: O'Reilly, 2010.
UDP:
alexeyrom wrote a very useful comment, with his consent I put it in the body of the post so that it can be seen:
“Norwig in 1996 looked at patterns in Lisp and Dylan . Actually, the result is the same (many patterns become trivial or significantly simplified), but on richer material. ”