Functional Programming Design - Data Structures
When we start thinking about how to represent data in Object Oriented programming, we usually think about a way to make it extensible, so that we can use some design principles that will allow us to make changes to the program without changing existing code. With functional programming, that can be a little bit trickier.
We need to know data structures and how to best use them, instead of creating objects whose behavior we can define. There is no other way around: we need to know what is the difference between call-by-name and lazy evaluation, how to choose between a list, a vector and an array, and why calling map
on a vector is more efficient than calling map
on a list. Of course, only having the knowledge of data structures does not make us better programmers — but it helps.
Principles and Patterns
There is a joke that has been around for a while comparing design principles and patterns in OO and FP:
OO | FP |
---|---|
Single Responsibility Principle | Functions |
Open/Closed Principle | Functions |
Interface Segregation Principle | Functions |
Dependency Inversion Principle | Functions |
Factory Pattern | Yes, functions |
Strategy Pattern | Functions again |
Decorator Pattern | Functions |
Visitor Pattern | Functions |
If you still have some patience for polemicists, the above comparison might be fun — and it should be understood only as a joke.
Imperative and functional programming exist to solve different kind of problems, but aiming for an extensible, modular, easy to change program should be the goal of programmers following either of those paradigms. While the design patterns might differ from OO to FP, SOLID principles do apply to FP. Of course each function, namespace and module should have a single responsibility, and, as long as a language allows some kind of polymorphism (generics or subtyping), we should care about all the other principles.
The n
queens problem
As an example of functional design, we can take a look at the 8 queens problem, in which the queens must be placed on a chess board and none of them can be attacking each other. The solution is usually generalized to solve the problem in which n
queens are placed on a board of n x n
dimension.
Before thinking about an algorithm to solve it, it is necessary to think about how to represent the game.
If we had to solve the problem in Ruby, how would we define the board/positions? It could be an Array
of coordinates ([[0,0], [0,1], [0,2], ...]
). We could also use two arrays (rows and columns). We could have a Struct
or an object Board
with :row
and :column
members. What about the queens? Should they be symbols, strings or objects? And how we represent the final result, that must include all the possible combinations for a given number of queens? Would it be a 2D array of coordinates? Should it be a map in which a coordinate is the key and queen/no-queen is a value?
Overthinking the problem is a common misstep, no matter if we are using a functional or an OO language. In the case of OO, it is easy to think that extensible means creating a class so that it can be extended. Not that functional programming cannot present the same risk. It is easy to try to emulate an object by creating a map and associating data and behavior to it — which completely defeats the purpose of FP.
So, what if we had to solve the n
queens problem in a functional language? Which data structure we would use?
This is a slightly adapted version of the solution published in Programming in Scala)
object Problem {
type Column = Int
def solutionFor(numberOfQueens: Int): Set[List[Column]] = {
def placeQueens(n: Int): Set[List[Column]] =
if (n == 0) Set(List())
else
for {
// backtracking: for each partial solution
partialSolution <- placeQueens(n - 1)
// for each column in the board
column <- 0 until numberOfQueens
// is safe to place a queen in a given column?
if isSafe(column, partialSolution)
// if true, add the column to the partial solution
} yield column :: partialSolution
placeQueens(numberOfQueens)
}
def isSafe(col: Int, partialSolution: List[Column]): Boolean = {
val row = partialSolution.length
// associate columns (placed queens) with rows
val queensWithRow = (row - 1 to 0 by -1) zip partialSolution
// check that placing one more queen keeps everything safe
queensWithRow forall {
// the column to be added cannot exist in the partial solution and
// (math.abs(col - c) != row - r) - the column cannot be attacked
case (r, c) => col != c && math.abs(col - c) != row - r
}
}
}
Problem.solutionFor(4)
//-> Set(List(2, 0, 3, 1), List(1, 3, 0, 2))
// 0 1 2 3 0 1 2 3
// |---|---|---|---| |---|---|---|---|
// 0 | | | Q | | 0 | | Q | | |
// |---|---|---|---| |---|---|---|---|
// 1 | Q | | | | 1 | | | | Q |
// |---|---|---|---| |---|---|---|---|
// 2 | | | | Q | 2 | Q | | | |
// |---|---|---|---| |---|---|---|---|
// 3 | | Q | | | 3 | | | Q | |
// |---|---|---|---| |---|---|---|---|
The beauty of the code is in its simplicity. Three functions solved everything and there was no need to create objects to behave in a certain way. It also uses the Backtracking algorithm — that is recursive and, thus, the usage of lists as the data structure. The final solution is a Set[List[Int]]
representing a set of the columns where the queens are placed (the rows are, of course, sequential, and that’s why there was no need to use a List[(Int, Int)]
).