Skip to content

Latest commit

 

History

History
159 lines (108 loc) · 12.8 KB

ch-001.md

File metadata and controls

159 lines (108 loc) · 12.8 KB

Functional Jargon and Programming Experience

This first chapter introduces important functional programming jargon and gives you a preview of the functional programming experience.

First class values

Values are expressions that cannot be evaluated any further. Technically speaking they are expressions in normal form (NF). In Javascript as in most other programming languages we can use literals to encode values:

"foo"
123
true
[1, 2, 3]
{foo: "bar"}
a => a
/^[A-Z]$/

You can pass values to and return them from functions. This trait is referred to as first class. Values are first class entities.

First class expressions

Values are the most atomic entity of programming but not particularly useful on their own. Fortunately we can generalize them to expressions. Generally speaking (pun intended) the process of generalization means to make things more useful, i.e. applicable to a wider range of scenarios.

"foo" + "bar"
123  1
true && false
[1, 2, 3] [0]
({foo: "bar"}).foo
(a => a) ("foo")

Just like values expressions are first class entities. In Javascript there actually is no difference between both, because expressions are first evaluated to values before they are passed to or returned from functions. Some functional languages pursue a different evaluation strategy, where expressions remain unevaluated until the result is needed but are still first class. Expressions are a great improvement compared to mere values. But we can only use them in place as-is. Is there a way to make them less ad-hoc? Let us generalize further!

First class functions

Imagine named expressions with holes in them and a mechanism to fill these holes when needed. Such generalized expressions would be way more flexible, because their results vary depending on the provided values. I am obviously talking about functions. Functions abstract from and are themselves expressions and hence are first class entities. It is important to understand that the definition of a function, the reference to it and its application all form expressions.

We can call a function once, twice, several times or not at all. It is only evaluated when needed. In a sense this behavior deviates from evaluating every expression to a value in normal form before it is used any further. By abstracting from an expression by wrapping it in a function we also defer its immediate evaluation. The most important trait of functions, however, is their reusability. Since functions are more general than expressions we are able to use them in more situations by simply referencing their name.

If functions are just first class expressions, what differentiates them from values? Nothing really, except that they are more general and are only evaluated when needed. This is exactly how functions are treated in the functional paradigm: functions are just values! But why do we want to treat functions like values in the first place? Because that is what the paradigm does - treating everything as a value. During this course you will develop a notion what the implications of this approach are and why it is extremely useful.

I used the hole metaphor, because it helps to draw the connection between functions and expressions. In the functional paradigm functions are understood rigorously mathematic, though. They are mere mappings from input to output.

Pure functions

I must admit to having simplified in the previous section. As a matter of fact there is more to it for functions to act like ordinary values. More accurately there are three requirements:

  • they must return a result value no matter what arguments are provided
  • they must return the same result value for the same argument
  • they must not perform another visible effect than returning a result value

The first requirement enforces total functions, which are going to be discussed in the next section. The latter two enforce pure functions. A pure function must be deterministic and must not perform visible side effects so that you can substitute its invocation with the respective result value without changing the behavior of the program. The technical term for the last requirement is referential transparency. Only pure and total functions can be considered as ordinary values.

If a function declaration includes an impure expression but the effect is not visible outside the function scope, calling it can be still considered a pure expression. If an otherwise pure function depends on an impure one it is also impure, because impurity acts systemic.

Idempotent functions

Functions that respect the idempotent property f(x) === f(f(x)) are idempotent functions. Idempotency is very desirable, because it allows an at-least-once strategy as opposed to exactly-once. Idempotence is orthogonal to purity, i.e. an idempotent function must not be pure and vice versa:

const toUC = s => s.toUpperCase(),
  inc = x => x + 1,
  delFoo = o => (delete o["foo"], o),
  log = x => (console.log(x), x),
  o = {foo: 123, bar: "abc"};
  
// idempotent pure function
toUC(toUC("hey")) === toUC("hey"); // true

// non-idempotent pure function
inc(inc(0)) === inc(0); // false

// idempotent impure function
delFoo(delFoo(o)).toString() === delFoo(o).toString(); // true

// non-idempotent impure function
log(log("foo")); // logs twice
log("foo"); // logs once

Please note that I used the toString method with delFoo solely to make the function result comparable. As you can see even an impure function can be idempotent regarding both its return value and its side effect.

Total and partial functions

The functional paradigm considers functions as mappings from input (arguments) to output (result values). In functional parlance the possible valid arguments of a function are also called its domain and the possible result values its codomain. If a function yields a result value for every passed argument, it is a total function. If a function does not yield a result value for even a single valid argument, it is merely a partial one. In doing so it is irrelevant whether the functions raises an explicit error and unwinds the stack or implictly returns undefined. In both cases we deal with a partial function:

const head = xs => xs[0];
head([1, 2, 3]); // 1
head([]); // undefined

Partial functions are per se less predictable and less reliable than total functions and you should avoid them consistently. Fortunately, you can transform any partial function into a total one by using a special Option type, which is one of the most common functional types. It will be covered in a later chapter.

Higher order functions

We are not done generalizing. When functions are just first class values let us pass a function to another one and see what is happening:

const app = f => x => f(x);
const add = x => y => x + y;
const sub = x => y => x  y;

app(add) (2) (3) // 5
app(sub) (2) (3) // -1

What we are doing here is a form of dependency injection. Such functions are called higher order functions, because they expect at least one function argument. Consequently functions without a function argument are called first order functions. Please note that a function without any function arguments that merely returns another function is not a higher order but a curried one. We will deal with currying in a later chapter of this course.

You can most likely imagine how powerful higher order functions are, since we are now able to inject new behavior depending on the function argument. As I have already mentioned the process of generalization means to make things more useful.

Are statements harmful?

No, but they act like dead ends in your code, because they are decoupled from one another. Since they do not evaluate to a value you need to bind their intermediate results to names explicitly in order to reference them from within other statements. As a result you have to declare a lot of name bindings to store all these accruing intermediate values:

const x = 1 + 2;
const y = 2 + 3;
const z = x * y;

I use the term name binding instead of variable, because there is no such thing as a variable in functional programming. All we can do is bind immutable values to names. Name bindings themselves are also immutable, i.e. you cannot reassign them. In Javascript, though, this is just a convention we need to adhere to. Later in this course you will see that statements go against the functional control flow, which consists of various forms of function composition.

Are operators special?

In functional programming operators are just functions in infix position (e.g. x + y), which have a predefined precedence and associativity. Functions on the other hand are usually written in prefix notation (e.g. add(x) (y)) and their application is left-associative and has a higher precedence then every other operator. It applies that every operator is a function but not the other way around.

Some functional programming languages allow partially applying operators, which is referred to as left (e.g. (2+) and right (e.g. (+2)) sectioning. Others even allow declaring custom operators along with their precedence and associativity. So ultimately there is nothing special about operators. Unfortunately, Javascript treats them special and they are not first class entities. If we want functional operators we need to mimic them with functions.

The functional programming experience

Composition

Composition lets us break down large problems into many smaller ones, which are addressed by pure, small functions and then recompose these functions to solve the original problem. This approach has far-reaching consequences on the coding style. Since such functions tend to be focused on a single concern, we gain a high degree of code reuse. Since such functions are pure we can easily test them without mocking the real world. If you recompose small functions with meaningful names you get back pretty descriptive code which abstracts from the underlying algorithm.

Local and equational reasoning

We have already learned that functional programming is based on referential transparency. But this is only a technical term. How do we benefit from this property? Well, it enables local reasoning and this is a pretty amazing quality. Local reasoning means we can think about an expression which sticks deep inside the structure and logic of our program without having to be concerned about this very context. With the absence of side effects we can focus on the piece of code we are interested in no matter how extensive the program becomes. Local reasoning is complemented by equational reasoning, which describes thinking about code using algebraic properties, which results in principled code changes that work as predicted. You probably know some of these properties from school, so there is no reason to be intimidated by them:

f(x) = x // identity function f
f(x) (y) = x // identity element y
f(x) = f(f(x)) // idempotence property
f(f(x) (y)) (z) = f(x) (f(y) (z)) // associative property
f(x) (y) = f(y) (x) // commutative property

Effects as values

Functional programming can be essentially described as programming with values. We have already learned that functions are values. In the same sense effects are values, because computations in an effectful context are put into sealed boxes or container types, where their execution is deferred and from which their results cannot be freely retrieved. This way the effect is ultimately tied to a value of a certain type and we can pass this value around as any other first class entities.

Recurring general abstractions

Functional abstractions tend to be so general that they are applicable to a wide range of scenarios. You will stumble upon a couple of abstractions throughout functional code bases, which facilitates to get acquainted with code and understand its intention. This often holds for other languages as well, because despite the syntactic differences functional abstractions are mainly shaped by math and not by particular language idioms.

The functional dilemma

The functional paradigm makes both the learning process for novices and individual projects for advanced users harder up front but gets easier later on. The reason for this experience is that the paradigm forces you to not make shortcuts in exchange for future benefits like more predictable, more resilient and more reliable code. Sticking to all these rules imposed on you by math might be annoying in the short term, but it will save you a lot of trouble in the long run.

Editor's note

If you enjoyed this chapter please 🌟 scriptum here on Github or share it on your preferred social media platform. If you found a mistake or inaccuracy or want to propose an improvement please file an issue/feature. Thank you.

TOC | next chapter >