Functions or Objects
It's time for us to have the talk.
Developers polarize into camps over their choices of tools and coding practices. You love your text editor, and despise the others. You love your formatting conventions, and despise the others. You love your language, and despise the others. While some tools and practices do have objective advantages over others, making universal statements about which are “best” is problematic.
Why? For one, you are almost certainly underinformed about the options you despise. You don't use them, after all. For two, the decision about which tool or practice you adopt is heavily influenced by your context and prior knowledge. Others have different contexts and different prior knowledge that lead to different choices. For three, you don't have to pick just one option. Your best bet is to learn about all of your options and synthesize them in a practice that makes you enjoy writing coding and leads to software of high quality.
One of our unnecessary wars is the battle between functional programming and object-oriented programming. Java developers and Haskell developers don't speak positively of each other. But are they really so different?
Consider the problem of writing code to manage the nodes of an abstract syntax tree for a mathematical expression. Operator nodes will be one of four arithmetic types (addition, subtraction, multiplication, and division), and you need to support several behaviors for each (evaluate, serialize, typecheck, and generate machine code). Your list of coding tasks may be represented as this matrix of types and operations, with types on the left and operations on the top:
evaluate | serialize | typecheck | generate | |
add | ||||
subtract | ||||
multiply | ||||
divide |
Whatever language or programming practice you use, you need to write code to meet all 16 tasks. If you are an object-oriented programmer, you will fill out this matrix one row at a time by writing a class for each type:
evaluate | serialize | typecheck | generate | |
add |
ExpressionAdd
|
|||
subtract |
ExpressionSubtract
|
|||
multiply |
ExpressionMultiply
|
|||
divide |
ExpressionDivide
|
Then to each class you will add methods for the four operations. For example, this class checks all the boxes in the top row:
class ExpressionAdd extends Expression {
public double evaluate() { /* ... */ }
public String serialize() { /* ... */ }
public void typecheck() { /* ... */ }
public void generate() { /* ... */ }
}
If you are a functional programmer, you will fill out this matrix one column at a time by writing a function for each operation:
evaluate | serialize | typecheck | generate | |
add |
evaluate
|
toJson
|
checkTypes
|
generate
|
subtract | ||||
multiply | ||||
divide |
Then to each function you will add pattern matching to handle the four different types. For example, this function checks all the boxes in the left column:
evaluate :: Expression -> Double
evaluate expr =
case expr of
(Add a b) -> evaluate a + evaluate b
(Subtract a b) = evaluate a - evaluate b
(Multiply a b) = evaluate a * evaluate b
(Divide a b) = evaluate a `div` evaluate b
In a sense, functional programming and object-oriented programming are just two different arrangements of code. Functional programs arrange code by its operation. Object-oriented programs arrange code by its type. There's room in the technology industry for both approaches to software development. There's even room inside of your brain for both.
Summary
A type system with an expressive type algebra allows new types to be made by combining together (AND) and choosing among existing types (OR). Haskell has such a type system. The data
command's |
operator gives a type a choice of possible variants. A value of the type is guaranteed to be one of the variants. An individual variant is given a combination of fields using tuple or record syntax. Values of custom types are destructured according to patterns whose syntax is identical to the values' construction. A function that serves many types will have type wildcards in its signature rather than named types. To make it serve a subset of types that conform to a certain interface, typeclass constraints are imposed on the wildcards. A typeclass declares a list of functions that a type must support much as a Java interface declares a list of methods that a class must support. Just as functions may have type wildcards, so too may types themselves. With type wildcards, we may make collections of orderable values or data wrappers that add nullability and error information. Though Haskell's type system is powerful, functions remain its organizing principle. To add a behavior to a set of types, we write a function that serves them all. In object-oriented programming, types are the organizing principle. To add a behavior to a set of types, each type gets its own method that serves just its type. Functional programmers and object-oriented programmers ultimately have the same goal but organize their programs differently.