I have been ruminating for a while on the basic meaning of functional programming, and what simple functional source code should look like. This is mostly because of my own pragmatic viewpoint on writing software and my experience in working with other programmers. Frankly, I have found that trying to follow some functional implementations is very difficult. Although this doesn’t sound like a good start to learning about functional programming, I think that there are a few reasons for my less than cheery experience, and being guided by these will ultimately lead to a much simpler path for you.
This is the first in a series of blog posts that is intended to share my more recent experiences with functional programming and how those overlap and clash with my hard-earned programming sensibilities. This is all meant to help and support those using object-oriented and imperative programming languages to walk a gentler curve up to functional programming, rather than the one great vertical leap that is most often required, and thus rarely taken. At the same time, I in no way claim that this will be even remotely interesting to those who consider themselves functional aficionados. Of course I and others would be happy if the functionally rich were influenced to likewise feed and care for the functionally poor and needy. Naturally that will require the rich to assume a position of humility that may not align with egos that are stroked by intellectual superiority. If that description doesn’t fit you, good. Yet, even the likeminded may need to take a step back and grasp that they have not been reaching enough people to make functional programming viable for the long term. I am on the side of the functionally poor and needy, and I am looking after them alone. |
Sometimes it’s the programming language itself that I find difficult; that is, reading source code in a given functional language that I am not familiar with. I don’t blame the programming language for that. It’s just that I have spent much of the past 35 years developing software using languages that fall within C-family syntaxes. This includes C itself, as well as C++, Java, C#, Scala, and JavaScript. Although Scala doesn’t try to identify very closely with C, it still does because of its obvious Java origins. I know that learning a completely unfamiliar syntax isn’t a show stopper for me, because within my first 5-6 years of programming I picked up Smalltalk, which is vastly different from C. To this day, probably 30 years later, Smalltalk is still my favorite language. The Smalltalk enabler for me was that I had to implement Smalltalk applications and tools, so I spent a lot of time with the language.
Presently I find it very difficult to spend much time at all programming in, say, Haskell. Haskell and Lisps, such as Clojure, don’t have prominence within nearly any clients that I work with, although there are usually small pockets within some companies trying to justify their use. I find that Scala is much more likely to be used, but Scala tends to meet with some scorn among functional “purists,” or they introduce Haskell-like constructs to Scala in the form of complex libraries.
Yet, even when using languages that I can more readily understand, such as Scala, I find that the code written by functional aficionados clashes with my pragmatism.
Sure, I used to have fun with C. I liked to use three and more levels of pointer indirection, and learned that I could use commas to combine multiple statements into one so that I didn’t have to use curly braces around conditionals and loops. That’s just for starters. At least it was fun for a while, and then I learned. Others wanted to get their jobs done without waiting for me to refactor my code toward readability. I was also surprised that the leading programmers of that time didn’t write code like that. From them I learned that writing mysterious code was not impressive, intelligent, or cool. Instead, it was just plain immature.
I learned that writing mysterious code was not impressive, intelligent, or cool. Instead, it was just plain immature.
So let’s say that for the past 30+ years I have been ever increasingly cautious about writing really simple code. Simple, clean code is what others want to read and what they can work with. It’s also what I have grown to desire. By this I don’t claim to write perfect code. I still make mistakes like everyone else, but my mindset is geared toward simplicity.
The more experience that I gain with functional programming the more I find the following to be true:
- Most functional programmers are attracted to it because of their mathematics background, or have chosen to learn more about mathematics in order to understand functional programming
- Advanced mathematical theory, such as categories, is non-trivial, and for most people it is either uninteresting or unattainable, even if it’s due to a lack of time or other circumstances
- Most functional programmers are more drawn to brevity or terseness than to readability
- Scala programmers who consider themselves to be functional programmers desire to write Haskell-ish code in Scala
- My simple functional code is critiqued as “not functional,” and by this functional aficionados actually mean “not functional enough for me”
- A fair number of functional aficionados are exhibiting an attitude that I believe is not of real benefit, other than to their own egos
These situations need to change. I am not taking the position of the enemy of functional programming. I think it has a lot of benefits. Like I said before, these benefits are not being broadly realized because the paradigm has not seen broad adoption. For anyone truly desiring to use functional programming over the long term, the current pace can’t sustain those hopes.
Despite being a great language, Smalltalk is today not relevant. For various reasons it wasn’t adopted broadly during its time in the limelight. Having lived through the demise of Smalltalk, I am making a case for lowering the bar to begin using functional programming in order to benefit the functionally poor and needy, with the ultimate outcome of benefiting the broader community as a whole.
When you think about it, thoroughly accomplishing this requires strong feelings with beneficial intent toward both sides: Empathy
What Is Functional Programming?
According to Wikipedia, functional programming is:
a style of building the structure and elements of computer programs that treats computation as the evaluation of mathematical functions and avoids changing-state
If functional programming means using mathematical evaluation, how can I possibly explain functional programming while avoiding mathematics? The answer is, I am not going to avoid mathematics. I am just going to make the mathematics easy to understand. If you decide to dig deeper into the mathematics, that would be great! Yet, you won’t need to do that to successfully use functional programming.
To a pure function, its whole world is the parameters that it receives and the answer that it produces
So, where’s the math? Well, let’s drop down to a very basic example that uses “the evaluation of mathematical functions” and that “avoids changing-state”:
int add(int m, int n) { m + n }
That’s pretty much it. It’s most of what you need to understand in order to evaluate mathematical functions. Here’s the break down of what we have here:
- This source code is a single function that is named
add
- The function
add
takes two parameters of typeint
and answers a value of typeint
- The answer to function
add
is the result of addingm
andn
- Although your programming language may support a return keyword, it is typical for a functional language to automatically return the result of the last expression evaluated by the function
- No state is changed
- To the pure function
add
, its whole world is the parameters that it receives and the answer that it produces
Calling this function is straight forward:
int total = add(5, 2)
A mathematical function isn’t required to perform a mathematical calculation, such as adding m
and n
together. A mathematical function may work on strings of text, or other data types, instead:
string concat(string a, string b) { a + b }
The concat
function concatenates string
b
to the end of string
a
, and answers the result.
I am pretty sure that this successfully expresses the case for simple functional programming, which is both mathematically based and that doesn’t change any preexisting state. Yet, you may be wondering why the concat
function exists at all. Why not simply concatenate a and b inline?
a + b
Well, you could do so, but you may not want to. What if there were concat
functions for every basic type, such as int
, float
, and string
? That would make concatenation very versatile. Also by creating concat
functions we make the outcome more reliable and reusable, because we have only one place where the definition of string concatenations is expressed.
This actually leads to important point about mathematical functions. Functions should be referentially transparent. Referential transparency means that a pure function that takes any number of parameters can be replaced by the value that it answers, or that the value that it answers can be replaced by the function and the same parameters used to produce the answer. In actuality we wouldn’t go around just hard-coding values all other our source code rather than calling a function. So, what this definition is really attempting to state is, if you were to call the function add(5, 2)
any number of times, it will always answer 7
. The same goes for passing any two int
parameters to function add
.
Another important characteristic of mathematical functions is that they may be composed. In simple terms this means that the result of one function can be used as the parameter to another function, but without temporarily holding the answer provided by the first function:
int total1 = add(5, 2)
int total2 = add(total1, 5)
Rather than this long-hand approach, simply compose the first function call into the second as a parameter:
int total = add(add(5, 2), 5)
You may have wondered whether function add
or function concat
should be implemented with more than two parameters. As you can see from function composition, you can compose any number of functions to achieve what is essentially an unlimited number of parameters:
string answer = concat(concat("the", "dog"), concat("barks", "loudly"))
Producing: "thedogbarksloadly"
This is great and all, but it would be much nicer if the result were better formatted, at least with spaces between each word. Of course we could just put a space at the end of each string, but that may not be the way that our strings are being produced. In fact we may purposely trim each string beforehand:
string answer = concat(concat(trim(s1), trim(s2)), concat(trim(s3), trim(s4))
There are a few ways to implement this. For now the simplest approach is to use knowledge that we already have. We will implement a new function that trims and also concatenates exactly one space at the end of each string:
string trimPad(string s) { concat(trim(a), " ") }
Here we’ve reused our known functions to create a new function, trimPad
. If every function does a very small amount of work, it provides a much better opportunity for function reuse through composition of preexisting functions of various kinds. So we end up with the following solution to our earlier problem:
string answer = concat(concat(trimPad(s1), trimPad(s2)), concat(trimPad(s3), trim(s4))
Note that we’ve avoided the problem of a trailing space on our ultimate answer by only trimming s4 and not trim-padding it. If we don’t know beforehand the total number of strings that will be concatenated, we may need to trim-pad everything and then just trim the full string:
string answer = trim(concat(concat(trimPad(s1), trimPad(s2)), concat(trimPad(s3), trimPad(s4)))
I think it is very clear by now the power of functions and function composition.
Using a Familiar Programming Language
You probably don’t recognize the programming language introduced in the above examples. Actually, neither do I. It’s just a made up language that doesn’t exist, at least as far as I know, and that fits into the C-family of languages. Yet, we can use a familiar programming language to accomplish intrinsically functional programming. Consider an example in Java:
package com.weareus.math;
public class Math {
public static int add(int a, int b) { return a + b; }
public static int subtract(int a, int b) { return a - b; }
public static int multiply(int a, int b) { return a * b; }
public static int divide(int a, int b) { return a / b; }
}
Those are the Math functions. And here are the Text functions:
package com.weareus.text;
public class Text {
public static String concat(String a, String b) { return a + b; }
public static String padr(String text) { return concat(text, " "); }
public static String padl(String text) { return concat(" ", text); }
public static String trim(String text) { return text.trim(); }
public static String trimPad(String text) { return padr(trim(text)); }
}
That’s simple enough. But, there are a few things to explain. Why are all of the Java methods declared as static? First of all, let’s clarify something about these “methods.” They are not methods. They are pure functions. Once again, the whole world to each of these functions is the parameters that they are passed and the answer that is produced. There is no other kind of “global” or otherwise “locally static” data for them to operate on. Secondly, the functions are declare as static so that we can statically import them into our client code:
package com.weareus.calculator;
import static com.weareus.math.Math.*;
import static com.weareus.text.Text.*;
public class Calculator {
public static void main(final String[] args) {
String n1 = args[0];
String op = args[1];
String n2 = args[2];
int answer = calc(n1, op, n2);
String show = padr("Answer for:") + padr(n1) + padr(op) + padr(n2) + answer;
System.out.println(show);
}
private static int calc(String sn1, String op, String sn2) {
final int n1 = Integer.parse(sn1);
final int n2 = Integer.parse(sn2);
switch (op) {
case "+":
return add(n1, n2);
case "-":
return subtract(n1, n2);
case "*":
return multiple(n1, n2);
case "/":
return divide(n1, n2);
}
return -1;
}
}
Because we have statically imported the Math and Text functions, we can call them directly without using the class name to dereference them. This makes the use of the functions much more, well, functional. It’s more fluent as well, because there is no need to visually filter out the Math and Text class names. In other words, we prefer the above uses of Math and Text rather than the following:
String show = Text.padr("Answer for:") + Text.padr(n1) + Text.padr(op) + Text.padr(n2) + answer;
...
switch (op) {
case "+":
return Math.add(n1, n2);
case "-":
return Math.subtract(n1, n2);
case "*":
return Math.multiple(n1, n2);
case "/":
return Math.divide(n1, n2);
}
...
Further, I decided not to use deeply composed concat
functions, but chose to use the +
expression instead. In this case composing the several concat
functions makes the code far less readable, or actually unreadable. Thus, the simplest code is as follows:
String show = padr("Answer for:") + padr(n1) + padr(op) + padr(n2) + answer;
Be Careful with Class Instances
Why don’t we declare a class that can have instances with immutable state? For example:
package com.weareus.text;
public class Text {
public final String a;
public final String b;
public Text(String text) { this.a = text; this.b = ""; }
public Text(String a, String b) { this.a = a; this.b = b; }
public String concat() { return a + b; }
public String padr() { return concat(a, " "); }
public String padl() { return concat(" ", a); }
public String trim() { return a.trim(); }
public String trimPad() { return padr(trim(a)); }
}
Now we have two constructors, one that takes one parameter and one that takes two. Further, none of the functions take parameters, because for every operation we are responsible to instantiate the Text with exactly the correct number of strings. So now it could be used, such as:
Text dog = new Text("dog");
String paddedDog = dog.padr();
Doesn’t that just seem strange? You never know exactly which immutable state objects might be used in any given function. Using instance state attribute/fields on types that also contain functions in effect designs each function with N implicit parameters, where N is the total number of instance attributes/fields.
One thing that you can do to improve this design is to hold the state of only the attribute(s)/field(s) that are considered the source operand, and then pass parameters for any additional required operands:
package com.weareus.text;
public class Text {
public final String a;
public Text(String a) { this.a = a; }
public Text concat(String b) { return new Text(a + b); }
public Text padr() { return concat(a, " "); }
public Text padl() { return concat(" ", a); }
public Text trim() { return new Text(a.trim()); }
public Text trimPad() { return trim().padr(); }
}
This improves the fluency of our client code:
Text dog = new Text("dog");
Text dogAndCat = dog.padr().concat("and").padr().concat("cat");
Even so, the best design may be to create function-only classes for your functions (and we will take up data types in our next lesson). In other words, by designing classes only with static methods that can be statically imported into client code, with all operands (parameters) being passed, your programs will be more so in a functional style.
Summary
So far I have covered:
- Very simple functional programming, which is intrinsic functional
- Pure mathematical functions and what constitutes such a function
- Immutability, or non-changing state
- Referential transparency
- Function composition
- Using a familiar programming language to implement functional solutions
I hope that you are ready to learn more! We will move on from here to basic data types and I will also introduce you to a tool named Functors. Functors? Well, what did you expect? There is a bit of mathematics to all this, but I promise it’s going to be really easy to understand.