关于函数式编程,有读过不少介绍文章,但是很多文章要么太理论化(范畴轮、幺半群等等),有的又讲的太泛泛,比如说函数式“代码简洁、易读、科里化和合成很有用”,但是读完是知道了函数式编程有这些特性,却并没有看出了和其他编程范式相比有什么优点。

自己也学习过 Haskell,但是平时函数式代码写的也比较少,所以很多概念可能是过几天就忘了。

所以这篇文章不深入理论,而是用实际的代码来展示,为什么一些代码逻辑用函数式能写出更“简介、易读、易维护”的代码。

柯里化(Curry)

柯里化可以将一个多参数函数转变为可以一个一个接收参数的函数。

比如一个接收两个数字返回其和的函数 add,未柯里化的时候只能 add(1,2) 的方式来调用,柯里化之后,既可以按上述方式调用,也可以按照 add(1)(2) 的方式来调用。但是这样有什么好处呢?

// add :: Num a => a -> a -> a
function add(x) {
    return function(y) {
        return x + y
    }
}

add(3)(4) // 7

上述 add 函数可以视为一个简单的柯里化的函数。其中 add :: Num a => a -> a -> a 是函数签名,意思是 add 函数接收 两个 Num 类型的参数,返回一个 Num 类型。注意 a -> a -> a 也可以读作接收一个 Num 类型的参数,返回一个接收 Num 类型的参数并返回 Num 类型的函数

柯里化后一个好处是可以从现有函数方便地生成偏函数(partial function)

// add3 :: Num a => a -> a
let add3 = add(3)
add3(4) // 7
add3(5) // 8

我们通过给 add 函数传递参数 3, 得到了一个新的函数 add3。这个新的函数绑定了部分参数,我们就不必每次调用都传入这个参数了。

我们还可以调用 add(2)/add(7) 生成 add2/add7 函数。

为了方便展示,我们在这里给 Function 类型对象的原型添加一个 autoCurry 方法,函数调用后会返回一个当前函数的柯里化版本。

Function.prototype.autoCurry = function() {
    let func = this
    return function curried(...args) {
        if (args.length >= func.length) {
            return func(...args)
        } else {
            return function (...args2) {
                return curried(...args, ...args2)
            }
        }
    }
}

使用 autoCurry,我么可以柯里化一个任意长度参数的函数。我们看下边的接收三个参数的例子:

// fullname :: [Char] -> [Char] -> [Char] -> [Char]
let fullName = function(first, middle, last) {
    return first + ' ' + middle + ' ' + last
}.autoCurry()

我们可以一次传入所有参数

fullName('Thomas', 'Jeffrey', 'Hanks') // Thomas Jeffrey Hanks

也可以传入一个 FirstName 生成一个偏函数

let someThomas = fullName('Thomas')
someThomas('Alva', 'Edison') // Thomas Alva Edison

还可以在偏函数的基础上再次传入参数生成一个偏函数

let someThomasCruise = someThomas('Cruise')
someThomasCruise('Mapother') // Thomas Cruise Mapother
someThomasCruise('SomeOne') // Thomas Cruise SomeOne

对单个函数来说,柯里化可以通过生成 partial function 来减少传参。而如果我们的函数都是柯里化的,那就可以很容易地用当前已有的函数生成新的函数。

// modulo :: Integral a => a -> a -> a
let modulo = function (divisor, dividend) {
    return dividend % divisor
}.autoCurry()

上述函数是一个求模函数

module(3)(9) // 0
modulo(3, 9) // 0

我们可以利用其是否能整除 2 来判断一个数是否是奇数

// isOdd :: Integral a => a -> a
let isOdd = modulo(2)
idOdd(6) // 0
isOdd(5) // 1

对于如下的 filter 函数,我们就可以将 isOdd 作为函数的第一个参数。

// filter :: (a -> Bool) -> [a] -> [a]
let filter = function(f, xs) {
    return xs.filter(f)
}.autoCurry()

filter(isOdd, [1,2,3,4,5]) // [1, 3, 5]

注意我们的 filter 函数也是柯里化的,所以我们就能得到一个新的 partial function getTheOdds

let getTheOdds = filter(isOdd)
getTheOdds([1,2,3,4,5]) // [1, 3, 5]

所以我们发现,函数柯里化可以方便地通过传递函数参数获得新的函数,下边我们来对比一下同样的逻辑使用函数式和非函数式的代码有什么区别。

lodash 是一个前端很广泛使用的函数库,但是它并不是真正的函数式,Ramda 则更函数式一点。我们来用这两个库来实现一个同样的函数:对于一个二维数组,返回一个新的二维数组,数组中每一项是原数据每一项的前两项。

lodash 的实现:

// [[a]] -> [[a]]
function firstTwoLetters(words) {
    return _.map(words, function(word) {
        return _.take(word, 2)
    })
}
firstTwoLetters([[1,2,3], [4,5,6]]) // [[1, 2], [4, 5]]

下面我们尝试用柯里化来简化这个函数。

这里如果我们可以把 _.take 柯里化,并且 word 作为第二个参数, 那么 _map 函数的第二个函数参数可以简化为 _.first(2)

function firstTwo(words) {
    return _.map(words, _.take(2))
}

如果 map 同样被科里化,且 words 为第二个参数,那么整个函数可以进一步简化:

let firstTwo = _.map(_.take(2))

对比可以看到新的实现代码量少了很多,下边就是上述函数的 Ramda 实现版本:

let firstTwo = R.map(R.take(2))
firstTwo([[1,2,3], [4,5,6]]) // [[1, 2], [4, 5]]

合成(Composition)

合成可以将两个或多个函数合成一个新的函数。接下来我们使用 Ramda 提供的一些函数来进行演示。

如果我们要实现一个取数组最后一个元素的函数,Ramda 有 reverse 函数来返回一个翻转数组,head函数来取数组的第一个元素。所以很简单的我们会有如下实现:

// last :: [a] -> a
function last(xs) {
    let sx = R.reverse(xs)
    return R.head(sx)
}

last([1,2,3]) // 3

compose 可以组合多个函数,将上一个函数的返回值作为下一个函数的参数。

compose(f, g)(x) === f(g(x))
compose(f, g, h)(x) === f(g(h(x)))

下边是 compose 的一个简单实现:

function compose(...funcs) {
    return function(...args) {
        let current = funcs.length - 1
        let firstFunc = funcs[current]
        let result = firstFunc.apply(this, args)

        while(current--) {
            result = funcs[current].call(this, result)
        }

        return result
    }
}

let composedLast = compose(R.head, R.reverse)

再例如下边的代码,展示了同时利用柯里化和合成来生成一个统计字符串句子内的单词个数的函数。

// wordCount :: [Char] -> Integral
function wordCount(str) {
    let words = R.split(' ', str)
    return R.length(words)
}

wordCount("I have four letters") // 4

let wordCount2 = compose(R.length, R.split(' '))
wordCount2("I have four letters") // 4

所以合成和柯里化,可以让我们更简洁的生成新的函数。如果函数 fg 表示一种状态到另一种状态的映射,那么合成可以省略中间步骤,让关系更加明确。

uncomposed

composed

morecompose

更多

函子



发表评论




0条评论