Look at functional programming in Java
-
Functional programming is gaining more and more attention, with strong support for this programming paradigm in popular programming languages today
-
Functional programming is based on the theory of mathematics, so the code written by functional programming is like a mathematical formula, simple and natural
-
An important characteristic of functional programming support is whether functions are considered first-class citizens of the language.
- Functions can be arguments to other functions, return values to other functions, or assigned to variables
-
In Java
- Java 8 began to support functional programming, and functions could be first-class citizens
- The specific function is defined in
java.util.function
In the package - Common functions
Function<T, R>
Represents a function that takes one argument, of input typeT
, the output type isR
Function
The interface contains only one abstract methodR apply(T t)
- That is, when type is
T
The input to thet
Apply the function to, the result is of typeR
The output of the
Consumer<T>
Take one input, no output- The abstract method is
void accept(T t)
- The abstract method is
Supplier<T>
No input, one output- The abstract method is
T get()
- The abstract method is
Predicate<T>
Take an input, and the output isboolean
type- The abstract method is
boolean test(T t)
- The abstract method is
- A number of standard apis based on functional programming have also been added to Java 8
- Read the REPO for details: A deep dive into Java functional programming and the Streams API
Relevant concepts
Pure Function – Pure Function
-
A function may be considered pure if it meets the following requirements
- Identical inputs always produce identical outputs (idempotent)
- The output cannot be related to any element other than the input value (reference transparency)
- Does not change any state outside the function (no side effects)
-
The advantages of pure functions are as follows
- No side effects, good for reconstruction
- Not affected by external states, facilitating parallel processing
- Same input, always fixed output, good for testing, good for caching
-
In Java
-
Simple examples of pure and impure functions are provided
-
/ / pure functions int f1(int x) { return x + 1; } // an impure function int y; int f2(int x) { // External variables are referenced return x + y; } // an impure function int f3(Counter c) { // Change the status of the input parameter c.inc(); return 0; } // an impure function int f4(int x) { // Operation IO writeFile(); return 1; } Copy the code
-
Partial Function – Partial Function
-
A partial function is one in which only some of the input parameters are bound to actual values
-
In Java
-
In fact, we sometimes inadvertently use this concept when Overloading
int sum(int a, int b, int c) { return a + b + c; } int sum(int a, int b) { return sum(a, b, 0); } // Non-overload is also acceptable int sumWith3(int a, int b) { return sum(a, b, 3); } Copy the code
-
Lambda Calculus – λ Calculus
-
The main theoretical basis of functional programming is the λ calculus
-
λ calculus is a very simple mathematical symbol system, but it is Turing complete, with the equivalent of Turing machine computing power
-
You can read these two articles: Notation: Abstract, semantic, and cognitive scientists write Lambda calculus for Little White
-
The calculus of λ contains the following concepts
-
Anonymous functions
-
High-order Function
- A function that takes another function’s argument or return value
-
Currie (Currying)
- Any multi-parameter function can be nested into a single-parameter higher-order function
-
Closure (Closure)
-
A closure is the sum of the function and the variables (also known as the environment) that can be accessed within the function
-
var local = 'abc'; function foo() { console.log(local); } // The above three lines of code form a closure in an immediate function Copy the code
-
-
-
In Java
-
Lambda expressions were introduced in Java 8
- Simply put, Lambda expressions are syntactic sugar for creating anonymous inner classes
- With the help of a compiler, developers can get their work done in less code
- Simply put, Lambda expressions are syntactic sugar for creating anonymous inner classes
-
Partial functions can be expressed as follows
-
class Partial <T.U.R> { BiFunction<T, U, R> needPartial; Function<T, R> partial = t -> needPartial.apply(t, null); } Copy the code
-
-
The Corrification can be expressed as follows
-
class Currying <T.U.R> { // Convert a function with two inputs to a higher-order function with one input BiFunction<T, U, R> needCurrying; Function<T, Function<U, R>> currying = t -> u -> needCurrying.apply(t, u); } Copy the code
-
Currying returns a function, so it is a higher-order function
-
For functions u -> needCurrying. Apply (t, u);
- References to variables outside the function
t
- So it forms a closure
- and
t
Will be converted to one by the compilerfinal
Enter, because Java specifies that it is notfinal
Cannot be shared
- References to variables outside the function
-
-
Memoization – Memoization
- The core idea is to exchange space for time to improve the performance of the function, which is actually cache
- For example, when calculating the Fibonacci sequence, you put in the result of each step
Map
Cache to avoid double counting
ADT (Algebraic Data Type) – Algebraic Data Type
-
Definition on Wikipedia: In computer programming, especially functional programming and type theory, an algebraic data type is a kind of composite type, i.e., a type formed by combining other types.
- That is, a combinatorial type, a type that is a combination of several other types
- Where does Algebraic fit in? The Haskell Wiki describes it this way:
- sum is alternation (A | B, meaning A or B but not both)
- product is combination (A B, meaning A and B together)
- Algebra refers to the substitution of symbols for elements and operations. In ADT, algebra is product and sum.
-
In Java
-
Classes in Java can have member variables of other types, which also combine other types, but only Product type, not Sum type
-
Class data is mutable, and ADT data is immutable
-
The implementation of the class contains the operations it supports, but ADT has only data and no operations
-
Enumerations are a Sum type
-
enum Shape { Circle, Rectangle, Square; } Copy the code
-
For each Shape, it can only be one of Circle, Rectangle, or Square
-
-
-
The ADT matches the pattern
- One of the obvious advantages of ADT is its use in conjunction with pattern matching
- Similar to Java
switch case
Statement, can be given for ADT different case value of the case for judgment processing
-
Different from Abstract Data Types – Abstract Data Types
- An abstract data type is a mathematical model and a set of operations defined on that model, that is, a collection of data elements and operations on that data
- Abstract data types like trees, graphs, queues, lists
- Corresponding to the List, Set, Map in Java, in fact, is the implementation of the “List”, “Set”, “associative array” provided in Java
Type Class – Type Class
- If a type is an instance of a type class, then the type must implement all of the behavior defined by that type class
- This can be interpreted as the relationship between an interface and a concrete instance in Java
- Functors, Applicatives, And Monads In Pictures are common types In functional programming
Functor – Functor
-
Functor can be expressed as follows
-
interface Functor<T> { <R> Functor<R> map(Function<T, R> f); } Copy the code
-
The only operation Functor provides is map() (called fmap in Haskell)
- Its input is a function f
- This function takes whatever is in the box (that is, the Functor that encapsulates the value), converts it, and wraps the result as a second Functor
-
Functor is often compared to a box containing instance T, and the only way to use this value is to convert it
-
-
In Java
-
Stream and Optional are functors and both have a map method that converts
-
Optional<String> strNum = Optional.of("1"); Optional<Integer> num = strNum.map(Integer::valueOf); Stream<String> strNum = Stream.of("1"."2"."3"); Stream<Integer> num = strNum.map(Integer::valueOf); Copy the code
-
Applicative – Can be applied functor
-
Applicative can be expressed as follows
-
interface Applicative<T> extends Functor<T> { Applicative<T> of(T t); <R> Applicative<R> liftA(Applicative<Function<T, R>> applicativeFunc); } Copy the code
-
Applicative is Functor, and it provides two additional operations:
of()
(Called unit, in Haskellpure
)- Accepts a value of any type as an argument and returns the Applicative value wrapped around the value
liftA()
(Called in Haskell< * >
)- and
map
Kind of. The only difference ismap
The input of is an ordinary function, whileliftA
The input is to wrap ordinary functions in Applicative
-
-
In Java
-
There is no similar liftA function found in Stream or Optional, so we will implement one ourselves
-
static class ApplicativeOptional<T> { private final Optional<T> value; private ApplicativeOptional(Optional<T> value) { this.value = value; } public <R> ApplicativeOptional<R> map(Function<T, R> f) { return new ApplicativeOptional<>(value.map(f)); } static public <T> ApplicativeOptional<T> of(T t) { return new ApplicativeOptional<>(Optional.ofNullable(t)); } public <R> ApplicativeOptional<R> liftA(ApplicativeOptional<Function<T, R>> applicativeFunc) { if (applicativeFunc.value.isPresent()) { return new ApplicativeOptional<>(value.map(applicativeFunc.value.get())); } return new ApplicativeOptional<>(Optional.empty()); } public static void main(String[] args) { ApplicativeOptional<String> strNum = ApplicativeOptional.of("1"); ApplicativeOptional<Function<String, Integer>> strNumFunc = ApplicativeOptional.of(Integer::valueOf); ApplicativeOptional<Integer> num = strNum.liftA(strNumFunc); }}Copy the code
-
List the Monad –
-
Monad can be represented as follows
-
interface Monad<T> extends Applicative<T> { <R> Monad<R> flatMap(Function<T, Monad<R>> f); } Copy the code
-
First Monad is Applicative, and then it provides an additional operation flatMap() (also called bind, called >>= in Haskell)
- with
map()
Similarly, except that the input function f returns the value wrapped in Monad
- with
-
-
In Java
-
Stream and Optional both have flatMap methods
Optional<String> strNum = Optional.of("1"); Function<String, Optional<Integer>> func = s -> Optional.of(Integer.parseInt(s)); Optional<Integer> num = strNum.flatMap(func); Stream<String> strNum = Stream.of("1"."2"."3"); Function<String, Stream<Integer>> func = s -> Stream.of(Integer.parseInt(s), 1.2.3.4.5); Stream<Integer> num = strNum.flatMap(func); Copy the code
-
conclusion
-
The Functor type solves the problem of how to put a value of in context into a normal function and get a value of in context
-
Applicative is how do you take an in context value and put it into an in context function that only returns a normal value and get an in context value
-
The Monad type is how do you take a value of an in context and put it into a function that only returns a value of an in context and get a value of an in context
Category theory
-
In category theory, a category (category) consists of three parts:
- A series of objects
- Mathematical groups, rings, even simple rational numbers, irrational numbers and so on can be classified as an object
- In a programming language, it can be understood as a type, such as an integer or a Boolean
- A type can actually be thought of as a collection of values, such as integers, which consist of 0, 1, 2… Therefore, the object in category theory can be simply understood as a set of values
- A series of morphisms
- Morphism refers to a mapping
- The function of morphism is to map the value VA in one object A to the value VB in another object B, which is very similar to the concept of mapping in algebra, so there are injective, surjective and other distinctions
- The existence of morphism reflects the internal structure of the object, which is the main technique used by category theory to study the object: the internal structural characteristics of the object are reflected through the relationship with other objects, static and static are relative, and category theory achieves the purpose of discovering the internal structure of the object by studying the relationship
- A composition operator with the dot (.) Is used to combine morphisms
- The combinative operator is used to combine two morphisms, for example, if there is A morphism F: A -> B, g: B -> C, then g.F: A -> C
- A series of objects
-
For a structure to be a category, in addition to the three things mentioned above, it must satisfy the following three limitations:
- Morphism satisfies the associative law, that is, F.(g.h) = (F.G).h
- Morphism must be closed in this structure, i.e., if there is a morphism f, g, there must be h = f.g
- For every object A in the structure, there must be A unit morphism Ia: A -> A. For the unit morphism, obviously, for any other morphism f, f.I = f
-
“A Monad is just A monoid in the category of endofunctors”
-
Here are a few things you need to know to understand the above statement
-
Semigroup and Monoid
-
Group defined in mathematics: G is a non-empty set. If the binary operation * defined on G satisfies the following conditions, G is called a group
- Closure: For any A, B ∈ G, there is a* B ∈ G
- Associativity: (a*b) * C = a* (b* C)
- Identity: there exists Identity E such that for any A ∈ G, e*a = A *e = a
- Inverse element: For any A ∈ G, there exists an inverse element A ^-1 such that a^-1 * a = A * a^-1 = e
-
If only closure and associativity are satisfied, G is said to be a Semigroup
-
G is said to be a Monoid if it is closed, associative and has identity only
-
Implement semigroups and monids in Java code
/ / semigroup interface Semigroup<T> { // Binary operation T append(T a, T b); } class IntSemigroup implements Semigroup<Integer> { @Override public Integer append(Integer a, Integer b) { returna + b; }}/ / MAO semigroup interface Monoid<T> extends Semigroup<T> { / / identity element T zero(a); } class IntMonoid implements Monoid<Integer> { @Override public Integer append(Integer a, Integer b) { return a + b; } @Override public Integer zero(a) { return 0; }}class StringMonoid implements Monoid<String> { @Override public String append(String a, String b) { return a + b; } @Override public String zero(a) { return ""; }}class ListMonoid<T> implements Monoid<List<T>> { @Override public List<T> append(List<T> a, List<T> b) { List<T> ret = new ArrayList<>(a); ret.addAll(b); return ret; } @Override public List<T> zero(a) { return newArrayList<>(); }}Copy the code
-
-
Since the functor (Endofunctor)
-
If (x:Int) => x * 2 or (x:Int) => x * 3, they are all self-functions
-
Identity function is a special case of the Identity function, which does nothing but returns what argument it passes in
-
The result of a self-functor map is itself, and here is a simple case:
-
Assuming that the functor is F, the result of the operation on F[Int] is still Int, and the result of the mapping of F: Int=>String is still F
- So this self-functor is actually an Identity functor (a special case of a self-functor) that does nothing to change the elements and relations in the category
-
Put all the types and functions in Haskell into a category called Hask. For the category of Hask, it looks something like this:
-
A and B are common types such as String, Int, Boolean, etc. These (finite) common types are A set of types. Another set of types is derived (that is, made up of type constructors and type parameters). This is an infinite set (which can be derived indefinitely).
- such
Category Hask
It covers all the types in Haskell
- such
-
For the category Hask, if there is a functor F, after mapping the elements in it, the result still belongs to Hask. For example, we use the functor List:
-
List[A], List[List[A]], List[List[List[A]]]... Copy the code
-
It is found that the results of these mappings also belong to the Hask category (subset), so this is a functor, and in fact all functors on the Hask category are functors
-
-
-
Now consider the statement “A Monad is simply a monoid ona category of functors”
- “In short, All monads on category X are nothing more than monids in categories composed of self-functors on category X. The binary operation is replaced by combinations of self-functors, and identity functors (All told, a monad in X is just a monoid in the category of endofunctors of X, With product × replaced by composition of endofunctors and unit set by the identity endofunctor.)”
- It can be understood in combination with this answer
About the Monad design pattern
- Refer to repO:java-design-patterns/monad/
- A MONAD is designed to verify data
- I don’t think this is strictly monad because it lacks the bind function
- Of course, you can also consider using Monad that comes with Java or is provided in a third-party library such as Vavr
reference
Scala and Algebraic Data Type
Functor and Monad in Java
How do you explain monads in Haskell?
My Understanding of Monad (1) : Semigroup and Monoid
Monad (5) : What is Endofunctor