This is a creation in Article, where the information may have evolved or changed.
Introduction
This is what I learned in the process of learning functional programming, which will use Go,javascript and Haskell three languages to analyze some of the mysteries of functional programming. JavaScript has some advantages that allow us to implement functional programming, and go as a strong type language, although the flexibility is slightly lacking, but also can do some high-level function implementation, Haskell language as a formal functional programming language, in order to explain the problem, As a comparison reference.
Body
Functional programming is often seen, and some of its advantages include:
- Does not include assignment statements (Assignment statement), once a variable is initialized, it cannot be modified (immutable)
- Without side effects, the function will not produce any side effects other than the calculation results
- Because there are no side effects, any expression can be evaluate at all times
Although the above advantage looks like a very powerful look, but, in the end, where is it? We can use the following example to illustrate:
Sum function
Haskell
sum [1,2,3]-- 6-- sum 的实现其实是foldr (+) 0 [1,2,3]
The definition of a function in Haskell flodr
is:
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
The function implementation is:
-- if the list is empty, the result is the initial value z; else-- apply f to the first element and the result of folding the restfoldr f z []
This is a recursive implementation, in functional programming, recursive definition is very common.
foldr
The function actually does something like this: foldr
accept three arguments, the first argument is a function f
, the second argument is the initial value z
, and the third argument is a list. If the list is empty, the initialization value is returned, z
otherwise recursive invocation, it is foldr
necessary to note that the type of function f
is to accept two parameters, return a value, two parameter types should be the z
same as (strongly typed language).
In Haskell we can see that a list can be summed in this way, so how do we implement sum
the function in JavaScript?
Javascript
First we implement the JS version of thefoldr
function foldr(f,z,list){ //为了简洁起见,把类型判断省略了 // Object.prototype,toString.call(list) === '[object Array]' if(list === null || list.length == 0){ return z; } //这里的shift会改变参数的状态,会造成副作用 //return f(list.shift(),foldr(f,z,list)); //改用如下写法 return f(list[0],foldr(f,z,list.slice(1)));}
Then we can implement the JS version (+)
:
function add(a,b){ return a+b;}
Then we sum
will become:
function sum(list){ return foldr(add,0,list);}
Finally, our JS version sum
can be used as well:
let a = [1,2,3];sum(a); // 6
The weak type of language such as JS is more flexible, functions f
can be arbitrarily implemented, for foldr
functions can also be reused between a variety of data types, then for a strong type of language like go, the result is what?
Go
In the same way, we implement the following go versions foldr
:
func foldr(f func(a,b int) int,z int,list []int)int{ if len(list) == 0{ return z } return f(list[0],foldr(f,z,list[1:]))}
Go because there are array slices, it is easier to use, but go is a strong type of language, so when declaring a function, you must declare the type clearly.
Then implement the f
function:
func add(a,b int) int{ return a+b;}
According to the gourd, we can get the function of Go version sum
:
func sum(list []int) int{ return foldr(add,0,list)}
It can be seen as if the routines are similar, really in the call of the time is this:
func main(){ a := []int{1,2,3} sum(a) // 6}
There is no loop in Haskell, because the loop can be implemented recursively, and in the function we implemented above sum
, there is no looping statement, which is different from our original programming thinking, when I first learned to write the summation function, it for
while
started, But the function gives me the door to a new world.
With the foundation above, we find that in functional programming, the reuse of code is very convenient:
quadrature function
Javascript
function muti(a,b){ return a*b;}function product(list){ return foldr(muti,1,list);}
Go
func muti(a,b int) int{ return a*b;}func product(list []int) int{ return foldr(muti,1,list)}
Haskell
foldr (*) 1 [1,2,3,4] -- 24-- or -- product 是Haskell预定义的函数myproduct xs = foldr (*) 1 xs-- myproduct [1,2,3,4]
There are many anyTrue
examples, for example, the following is only the allTrue
JS implementation:
Anyture
Javascript
function or(a,b){ return a || b;}function anyTrue(list){ return foldr(or,false,list);}
Call:
let b = [true,false,true];console.log(anyTrue(b)); // true
Allture
Javascript
function and(a,b){ return a && b;}function allTrue(list){ return foldr(and,true,list);}
Call:
let b = [true,false,true];console.log(allTrue(b)); // false
Of course we can see that this flodr
function thief is useful, but it seems to be a little puzzled, how does it work? Looking around flodr
is a recursive function, but in the programming world, it has a more famous name reduce
. Let's look at how the reduce
sum function is implemented in JS:
Sum function reduce Edition
const _ = require("lodash");_.reduce([1,2,3],function(sum,n){ return sum+n;});
lodash
This is defined in the official documentation:
_.reduce alias _.foldl_.reduceRight alias _.foldr
Well, I lied to you, but it foldr
should correspond reduceRight
.
So foldl
foldr
What's the difference?
In fact, the difference between the two functions is that the combination of different ways, in order to poor for example:
Haskell
foldr (-) 0 [1,2,3]-- 输出: 2foldl (-) 0 [1,2,3]-- 输出: -6
Why are the two outputs different? This is related to the direction of the binding:
foldr (-) 0 [1,2,3]
Equivalent:
1-(2-(3-0)) = 2
and
foldl (-) 0 [1,2,3]
Equivalent:
((0-1)-2)-3) = -6
There is no difference between the combination direction and the summation result, but for the difference, it has the effect:
Javascript
const _ = require("lodash");//reduce 相当于 foldl_.reduce([1,2,3],function(sum,n){ return sum-n;});// 输出 -4
This and agreed to -6
seem to be different, the pit daddy? This is because, in lodash
the implementation, reduce
the initial value is the first element of the array, so the result is 1-2-3 = -4
.
So let's look at reduceRight == foldr
the results:
Javascript
const _ = require("lodash");//reduceRight 相当于 foldr_.reduceRight([1,2,3],function(sum,n){ return sum-n;});// 输出 0
We see the result is 0
also the expected result, because 3-2-1=0
.
Note: For ease of understanding and coherence, I have added some of my own understanding. It should be stated that in Haskell, the foldl1
function should be consistent with the JavaScript reduce
(Lodash) function, which will foldl1
take the first element of the list as the initial value.
Now let's summarize foldr
foldl
Some of the ideas:
If you apply the function's initial value to the list [3,4,5,6]
f
z
foldr
, it should be understood as:
f 3 (f 4 (f 5 ( f 6 z)))-- 当 f 为 +, z = 0 上式就变为:3 + (4 + (5 + (6 + 0)))-- 前缀(+)形式则为:(+)3 ((+)4 ((+)5 ((+)6 0)))
If you apply the function's initial value to the list [3,4,5,6]
g
z
foldl
, it should be understood as:
g(g (g (g z 3) 4) 5) 6-- 当然我们也可以类似地把 g 设为 +, z = 0, 上式就变为:(((0 + 3) + 4) + 5) + 6-- 改成前缀形式(+)((+)((+)((+)0 3) 4) 5) 6
As can be seen from the above example, the left folding ( foldl
) and right folding ( foldr
) have a very important difference, that is, the left folding cannot handle the infinite list, but the right folding can.
All we're talking about is using predefined functions, +
-
*
..., (in functional programming, these operators are actually functions) to make it easier for us to understand, now let's look at our own defined functions. Try reversing a list:
Reverse
Haskell
flip' :: (a -> b -> c) -> b -> a -> cflip' f x y= f y x
The function above is to pass in the flip'
first argument as a function, and then swap the order of the two parameters ( flip
which is a predefined function).
Hasekll
foldr flip' [] [1,2,3]
What about the implementation of JavaScript?
Javascript
function flip(f, a, b){ return f(b,a);}//这个函数需要进行柯里化,否则无法在foldr中作为参数传入var flip_ = _.curry(flip);function cons(a,b){ return a.concat(b); }function reverse(list){ return foldr(flip_(cons),[],list);}
What about the results of the call?
console.log(reverse([1,2,3]))// [ 3, 2, 1 ]
Well, now we seem to see a new thing-- curry
Gerty. To put it simply, Gerty is a function that takes a subset of the arguments and then returns a function that takes the remaining arguments. In the example above, the function that flip
is obtained after the currying flip_
can accept the first parameter cons
and then return a function that accepts two parameters, which is a,b
the connection function we need.
In the go language, the implementation of curry is a very troublesome thing, so go's functional programming support is still relatively limited.
And then we'll try to get a list of lengths to implement a length
function:
Length
Haskell
-- 先定义实现一个count 函数count :: a -> b ->ccount a n = n + 1-- 再实现一个length函数length' = foldr (count) 0-- 再调用length' [1,2,3,4]-- 4
Javascript
//先定义一个count函数function count(a,n){ return n + 1;}//再实现length函数function length(list){ return foldr(count,0,list);}//调用console.log(length([1,2,3,4]));// 4
It's that simple, okay, reduce
we're done, and then we'll look at map
map
how the function comes, and we'll start with a simpler function that multiplies all the elements of the entire list by 2:
Doubleall
Haskell
-- 定义一个乘以2,并连接的函数doubleandcons :: a -> [a] -> [a]doubleandcons x y = 2 * x : ydoubleall x = foldr doubleandcons []-- 调用doubleall [1,2,3]-- 输出-- [2,4,6]
Javascript
function doubleandcons(a,list){ return [a * 2].concat(list)}function doubleall(list){ return foldr(doubleandcons,[],list)}//调用console.log(doubleall([1,2,3]));// [2,4,6]
Let's take a look at how go writes:
Go
The awkward part of GO is that it requires a very explicit function definition, so we're going to re-write a foldr
function to accept the second argument as a list f
.
func foldr2(f func(a int,b []int) []int,z []int,list []int)[]int{ if len(list) == 0{ return z } return f(list[0],foldr2(f,z,list[1:]))}
Then we can implement the same logic as above:
func doubleandcons(n int,list []int) []int{ return append([]int{n * 2},list...)}func doubleall(list []int) []int{ return foldr2(doubleandcons,make([]int,0),list)}// doubleall([]int{1,2,3,4})//[2 4 6 8]
Go this strong type of compiler language although support a certain functional programming, but the use of some limitations, at least the code reuse is not as good as JS.
Now let's focus on the doubleandcons
function, which can actually be converted to a function like this:
fandcons f el [a]= (f el) : [a]double el = el * 2-- 只传入部分参数,柯里化doubleandcons = fandcons double
Now let's look at the here, in fact, here fandcons
can be generalized as Cons·f
, here is ·
called function combination. And the function combination has this action:
$$
(F. G) (h) = f (g (h))
$$
Then our function above can be expressed as:
$$
Fandcons (f (el)) = (CONS.F) (el) = Cons (f (EL))
$$
So:
$$
Fandcons (f (EL), list) = (CONS.F) (el, list) = Cons ((f (EL)), list)
$$
The final version is:
$$
Doubleall = Foldr ((Cons. Double), Nil)
$$
Here is foldr(Cons.double)
actually what we want map double
, then our true nature map
is:
$$
Map = Foldr ((cons.f), Nil)
$$
Here Nil
is the foldr
initial value of the function.
Well map
, it's already showing up, so let's take a closer look at map
how a function should be implemented:
Map
Haskell
fandcons :: (a->b) ->a -> [b] -> [b]fandcons f x y= (f x):ymap' :: (a->b) -> [a] -> [b]map' f x = foldr (fandcons f) [] x-- 调用 map' (\x -> 2 * x) [1,2,3]-- 输出 [2,4,6]
With Haskell's lambda expression, this is actually f
the double
implementation.
We also look at the JS version of the implementation:
Javascript
function fandcons(f, el, list){ return [f(el)].concat(list);}//需要柯里化var fandcons_ = _.curry(fandcons);function map(f, list){ return foldr(fandcons_(f),[],list);}//调用console.log(map(function(x){return 2*x},[1,2,3,4]));// 输出[ 2, 4, 6, 8 ]
These need to curry the go I do not realize, because go implementation of curry is more complex.
Finally, let's look at map
some magical operations:
Matrix summation
Summatrix
Haskell
summatrix :: Num a => [[a]] -> asummatrix x = sum (map sum x)-- 调用summatrix [[1,2,3],[4,5,6]]-- 21
It is important to explicitly declare the type of parameter a, because the SUM function requires that a parameter of type num
Javascript
function sum(list){ return foldr(add,0,list);}function summatrix(matrix){ return sum(map(sum,matrix));}//调用 mat = [[1,2,3],[4,5,6]]; console.log(summatrix(mat));//输出 21
Conclusion
In the process of learning functional programming, I feel the impact of a new mode of thinking, as if to open a new world, no loops, even no branches, simple and elegant syntax. I think that as a computer practitioner, you should have access to functional programming, which allows you to broaden your horizons and think from another perspective.
The original published in my personal blog, the retention of all the rights of the article, without permission shall not be reproduced.