There are many excellent design concepts in functional programming that are worth learning. This article briefly introduces the basic concepts in functional programming, but it is more important to think and summarize how to apply them to our daily development to help us improve the readability and maintainability of the code.
This post is also published on my personal blog
© Original article, reprint please indicate the source!
Overview
I’ve heard about functional programming, but I haven’t studied it in depth, and I rarely think about it in my daily development. Until recently, functional programming had an updated understanding when developing flutter applications due to dart’s better support for it. Pure functional programming is utopian for our normal business development, but there are many design concepts worth learning. There are a number of articles on functional programming, but what makes this article different is that it focuses on how to use functional programming ideas to help you write better code, which is why the title of this article is called Functional thinking rather than functional programming.
The theoretical basis of functional programming is lambda calculus, but this paper does not intend to discuss it too much on the theoretical level.
First, to summarize my personal opinion: what can functional programming do for us? Simple, clear, easy to maintain, reusable code.
Simplicity, clarity, easy maintenance and reusability are the primary goals pursued by various architectural designs and design specifications.
And how does functional programming achieve this benefit:
- State immutable, pure function;
- Avoid introducing states, Pointfree;
- Emphasize combination and improve reuse;
- Higher levels of abstraction, rich collection operations.
This article will focus on the above aspects of functional programming discussion.
Functional vs. Imperative
As one of the Programming paradigms, functional Programming is the most familiar counterpart of Imperative Programming (object-oriented Programming also belongs to this Paradigm).
From the perspective of thinking mode:
- Imperative programming: “process-oriented”, emphasizing how to do it — the focus is on performing the steps, how to accomplish tasks step by step;
- Functional programming: “results-oriented”, emphasizing what to do — the focus is on executing the result, rather than on implementation details, which are at a higher level of abstraction.
From the theoretical basis:
- Imperative programming: computer oriented model, variables, assignments, control statements and so on corresponding to the computer’s physical storage, read and write instructions, jump instructions;
- Functional programming: mathematically oriented models that represent tasks in the form of expression evaluation.
In terms of implementation, functional programming is a further abstraction of imperative programming, shielding concrete details, describing the intent of the program in a more abstract and more natural language way, and leaving the implementation details to the language runtime or tripartite extension to complete. As a result, developers can be freed from implementation details and think about business problems at a higher level of abstraction.
The status quo
The concepts of pure function, higher-order function, first-class citizenship of function, and set operation have greatly enhanced the creativity and expression of language. Although pure function cannot be achieved, more and more high-level languages begin to develop towards the direction of function, and introduce some important concepts in function into their syntax, such as JavaScript, Swift, Java, DART, etc.
Pure functions
A “function” in functional programming is not a function (method) in our everyday development language, but a function in the mathematical sense — a mapping. We know that mathematically a function (mapping) must have the same output for the same input (mapping is one-to-one). Therefore, pure functions in functional programming must satisfy the same characteristics:
- The same input must yield the same output;
- The function call has no side effects.
Same input, same output
Satisfying this means that the function cannot depend on any external state other than the input parameter. The implicit inclusion of this pointer in member functions of object-oriented classes makes it easy to reference member variables in member functions, which is a classic example of how not to use pure functions.
Why is that? Function-level decoupling is achieved, and there are no complex dependencies on input parameters, which makes the function readable and maintainable. I believe that in normal development, you can also have such a feeling: when understanding and maintaining a function, if it depends on a large number of external states, it will certainly cause a lot of cognitive pressure. In addition to understanding the logic of the function itself, you need to be concerned about the external state information it references. Sometimes the reading process is interrupted when you have to look outside the function itself to see the dependent external information.
No side effect
A side effect is any output other than the expected output value of the function.
Common side effects include, but are not limited to:
- Changing external data (such as class member variables, global variables);
- Sending network requests;
- Read and write files;
- Perform DB operations.
- Get user interaction information (user input);
- Read system status information;
- A log;
- .
In short, a pure function cannot have any coupling relationship with the outside world, including dependence and influence on the outside world.
Obviously, the benefits of pure functions mainly include:
- Higher maintainability;
- More testability;
- Better reusability;
- High concurrency is easier, no multithreading problems;
- Cacheable. Because the same input must have the same output, the result can be cached for high-frequency and expensive operations to avoid double calculation.
In practical development, although it is impossible to make all functions pure, the consciousness of pure functions should be rooted in our minds, and we should write as many pure functions as possible.
Higher-order functions
Another important idea of functional programming is that functions are values, or first-class functions, or functions have first-class citizenship. This means that functions can be used wherever values can be used, such as parameters, return values, and so on.
A higher-order function is one in which at least one of its arguments or return values is a function type. Higher-order functions bring the reuse granularity down to the function level, whereas in object-oriented reuse granularity is usually at the class level.
Closures are the underlying support for the implementation of higher-order functions.
On the other hand, higher-order functions also achieve a higher level of abstraction, since implementation details can be passed in as parameters, that is, dependency injection mechanisms are implemented at the function level. Therefore, a variety of GoF design patterns can be realized in the form of higher-order functions, such as Template Method pattern and Strategy pattern.
Currie (Currying)
To put it simply, Currization is the process of converting “multi-parameter functions” into “a series of single-parameter functions”.
// JavaScript
//
function add(x, y) {
return x + y;
}
var addCurrying = function(x) {
return function(y) {
returnx + y; }}Copy the code
As above, add is an addition function that takes two arguments, such as add(1, 2). AddCurrying is curried, and is essentially a single-argument function whose return value is also a single-argument function. Add (1, 2), equivalent to addCurrying(1)(2).
What does corrification do?
- On functional set operations such as:
filter
,map
,reduce
,expand
Etc only accept single-parameter functions, so if the existing function is multi-parameter, it can be converted to single-parameter through Currie transformation; - When a function needs to be called multiple times and some of the arguments are the same, currization can reduce duplicate argument boilerplate code.
For example, if you need to add multiple times and add 10 each time, use the normal add function:
add(10, 1);
add(10, 2);
add(10, 3);
Copy the code
And through the Currified version:
var addTen = addCurrying(10);
addTen(1);
addTen(2);
addTen(3);
Copy the code
The famous JavaScript tripartite library Lodash provides a curry wrapper function that makes currying even easier, like the lodash#curry function used for addCurrying above:
var curry = require('lodash').curry;
var addCurrying = curry(function(a, b) {
return a + b;
});
Copy the code
Currization is an indispensable skill for functional programming. For us, even without writing functional code, currification provides a new way to solve problems such as repeating arguments.
Set operation three axe
Functional programming languages and object-oriented languages treat code reuse differently. Object-oriented languages like to build a lot of data structures with a lot of operations, and functional languages have a lot of operations, but very few data structures. Object-oriented languages encourage us to build methods that are specific to a particular class, and we find recurring patterns in class relationships and reuse them. Functional languages reuse is manifested in the versatility of functions, which encourage the use of a variety of common transformations on data structures and the use of higher-order functions to adjust operations to suit specific requirements.
In object-oriented imperative programming languages, the units of reuse are classes and the messages used for communication between classes, often expressed as a class diagram. For example, the ground-breaking book in this area, Design Patterns: The Foundation of Reusable Object-oriented Software, has at least one class diagram for each pattern. In the OOP world, developers are encouraged to create specialized data structures for specific problems and associate specialized operations on the data structures in the form of methods. Functional programming languages take another approach to reuse. They use a small set of critical data structures (such as lists, sets, maps) to pair operations that have been deeply optimized for these data structures. We “plug in” additional data structures and higher-order functions as needed to adapt the machine to the specific problem on top of a set of mechanisms that are made up of these key data structures and operations. For example, the filter function, which we have practiced in several languages, is passed a block of code that is such an “insert”, the filter criteria are determined by the passed higher-order function, and the operator is responsible for efficiently performing the filter and returning the filtered list.
— Excerpt from: Neal Ford. “Functional Programming Thinking (Turing Programming Books).”
As mentioned in the above excerpt, another important idea of functional programming is to provide rich operations on a limited Collection. Today, many high-level languages offer extensive support for collection operations, such as Swift, Java 8, JavaScript, DART, and more. With these highly abstract operations, you can write very concise, readable code.
Here is a brief description of some common collection operations.
Filter (filter)
Filtering is to filter out the elements in the list that do not meet the specified conditions. The elements that meet the conditions are returned as a new list. The operation is named differently in different languages: filter in JavaScript, Swift, and Java 8, and DART is WHERE.
// JavaScript
//
filter(callback(element[, index[, array]])[, thisArg]);
Copy the code
// dart
//
可迭代<E> where(bool test(E element));
Copy the code
// Swift
//
func filter(_ isIncluded: (Self.Element) throws -> Bool) rethrows- > [Self.Element];
Copy the code
// Java
//
Stream<T> filter(Predicate<? super T> predicate);
Copy the code
As you can see, the language representation is different, but the essence is the same: a callback is injected into the filter to determine whether the element meets the specified criteria.
For example, filter out those under 18 years of age:
// JavaScript
//
const ages = [19.2.8.30.11.18];
const result = ages.filter(age= > age >= 18);
console.log(result); / / 19, 30, 18
Copy the code
The loop implementation is not in this column, and the comparison should be obvious.
Mapping (map)
A map is a transformation of each element in a collection to get a new value, which can be of the same or different type.
// JavaScript
//
map(function callback(currentValue[, index[, array]]);
Copy the code
// dart
//
可迭代<T> map<T>(T f(E e));
Copy the code
// Swift
//
func map<T> (_ transform: (Element) throws -> T) rethrows- > [T];
Copy the code
// Java
//
<R> Stream<R> map(Function<? super T,? extends R> mapper);
Copy the code
Map is one of the most frequently used operations in daily development, such as converting JSON to dart object instances:
jsons.map((json) => BusinessModel.fromJson(json)).toList();
Copy the code
Fold/reduce
Folding simply means that a specified operation is applied to each element of the collection in sequence, the result of which is superimposed according to the operation rules, and the result of which is returned. (The result type is usually a concrete value, not an Iterable, and thus often appears at the end of a chain call.) .
// JavaScript
//
reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue]);
Copy the code
// dart
//
E reduce(E combine(E value, E element))
T fold<T>(T initialValue, T combine(T previousValue, E element));
Copy the code
// Swift
//
func reduce<Result> (_ initialResult: Result._ nextPartialResult: (Result.Element) throws -> Result) rethrows -> Result;
Copy the code
// Java
//
T reduce(T identity, BinaryOperator<T> accumulator);
Copy the code
Dart provides reduce and fold. The main difference is that the latter provides the initial value for the fold.
List<int> nums = [1.3.5.7.9,];
// reduceResult: 25
//
int reduceResult = nums.reduce((value, elemnt) => value + elemnt);
// foldResult: 35
//
int foldResult = nums.fold(10, (value, elemnt) => value + elemnt);
Copy the code
The above example,reduce
Is the sum of the list elements directly (the result is 25), andfold
The initial value of 10 is provided during the summation (the result is 35). The abovereduce
,fold
All fold from left to right, and some languages offer right-to-left versions, such as JavaScript:
reduceRight(callback(accumulator, currentValue[, index[, array]])[, initialValue])
Copy the code
There are many more collection operations, which will not be listed here. When you start using these actions, you’ll be amazed at how impossible it is to stop!
Operations on collections are also immutable, meaning that they do not change the collection they are acting on, but rather generate new collections to provide the results of their operations.
There are many patterns or frameworks with similar ideas, such as Flux, Redux, Bloc, etc., which emphasize (force) that no operation can directly modify existing data, but instead generate new data based on existing data, and eventually replace the old data as a whole.
In the actual development, we have also encountered similar problems. The network request directly modified the data source after the child thread returned data, resulting in the multi-thread problem of data synchronization. The best solution is to assemble the complete data in the child thread after the network request is returned, and then to the main thread for a one-time replacement.
Immutability can avoid the problems of intermediate state, state asynchronism and multithreading. At the same time, invariant semantics make code readability and inference maintenance better.
Because the filter, map, reduce, and other operations, rather than the for, while loop statements that operate on collections, make it clear that the intention is to generate a new collection rather than modify an existing one, the code is cleaner.
In addition, because most of the return value types of these operations on collections are collections, when there are more than one operation on a collection, it can be implemented as a chain call. This also simplifies the code further. Look at an example of flutter:
// functional flutter
//
memberIconURLs
.where(_isValidURL)
.take(4)
.map(_memberWidgetBuilder)
.fold(stack, _addMemberWidget2Stack);
Copy the code
// imperative flutter
//
int count = 0;
for (String url in memberIconURLs) {
if (_isValidURL(url)) {
Widget memberWidget = _memberWidgetBuilder(url);
_addMemberWidget2Stack(stack, memberWidget);
count++;
}
if (count >= 4) {
break; }}Copy the code
The above two code snippets use a functional collection operation and a normal for loop to do the same thing: convert the user’s avatar url retrieved from the background into an avatar widget to display on the interface (up to four urls are displayed, and invalid urls are filtered out).
Here’s another example to further appreciate the difference:
// imperative JavaScript
//
var excellentStudentEmails_I = function(students) {
var emails = [];
students.forEach(function(item, index, array) {
if (item.score >= 90) { emails.push(item.email); }});return emails;
}
Copy the code
// functional JavaScript
//
var excellentStudentEmails_F = students= >
students
.filter(_= > _.score >= 90)
.map(_= > _.email);
Copy the code
The above two codes are the email addresses of students with scores >=90.
Obviously, the functional implementation’s code is clean, readable, logical, and error-proof. The for loop version requires careful maintenance of implementation details and introduces unnecessary intermediate states: count, URL, memberWidget, emails, and so on — all breeding grounds for bugs!
Well, when it comes to reducing intermediate states, we have to mention Pointfree.
Pointfree
After careful analysis of the functional version of the email of students with scores >=90 in the above section, it is found that the whole process can be divided into two independent steps:
- Filter out students with scores >=90;
- Get the student’s email.
Separate these two steps into two small functions:
function excellentStudents(students) {
return students
.filter(_= > _.score >= 90);
}
function emails(students) {
students
.map(_= > _.email);
}
Copy the code
An example would be to write excellentStudentEmails as follows:
var excellentStudentEmails_N =
students= > emails(excellentStudents(students));
Copy the code
There doesn’t seem to be any advantage to writing nested calls like this. But one thing is clear: The output of one function directly becomes the output of another function.
var compose = (f, g) = > x= > f(g(x));
Copy the code
We introduce another function, compose, which takes two single-parameter functions (f, g) as inputs and outputs a single-parameter function (x => f(g(x)). Compose to rewrite excellentStudentEmails:
var excellentStudentEmails_C = compose(emails, excellentStudents);
Copy the code
The combined excellentStudentEmails_C version has two advantages over the nested call version excellentStudentEmails_N:
- Readability is better, and reading from right to left rather than inside-out is more consistent with our habits of mind;
excellentStudentEmails_C
The release never mentions the data to manipulate, reducing the intermediate state information (the more states, the more errors).
There are no intermediate states, no parameters, and data flows directly between the combined functions, which is the most straightforward definition of Pointfree. Pointfree is essentially a series of “combinations of generic functions” to accomplish more complex tasks.
- Encourage the writing of highly cohesive, reusable “small” functions;
- Emphasize “composition” rather than “coupling”, and complex tasks are accomplished through small task combinations rather than coupling all operations into a “large” function.
The combined function acts as if it were piped together, in which data flows freely without external intervention:
Have special pipes in UNIX shell command command ‘|’, such as: ls | grep Podfile, combination of ls and the grep command, used to determine whether the current directory Podfile file.
Note that the excellentStudentEmails_F version is a better way to write the excellentStudentEmails_C version of an excellentStudentEmails_C is used only to illustrate Pointfree concepts.
summary
We don’t expect, nor can we expect, pure functional programming, but there are many good design ideas in functional programming that we can learn from:
- State immutable, avoid too many intermediate states;
- Pure functions;
- Highly cohesive small function;
- Multi-purpose combination;
- Do a good job of abstraction, shielding details;
- .
The resources
JS functional programming guide
Functional programming thinking
What is functional programming thinking
Collection Pipeline
Functional-Light JavaScript