Day 05 - Ramda

yo, what's up

Ramda 是一个 Functional Programming 的函式库,而 Ramda 的所有函式都有自带 currying.

笔者一开始学 Functional Programming 的时候,觉得 Ramda 跟 lodash 其实很像阿,一度非常不适应!!而且lodash 的 get 这麽好用,为何要改用 path 跟 prop ,超难用的。

到最後才发现,其实 Ramda 有很多非常好用的功能是 lodash/fp 没有的,如 transducelens 等, 且所有函式都是 data last 以及自带 currying!

今天要介绍一下 Ramda 里一些实用的函式

Converge

https://i.imgur.com/P0JKGae.png

可以看到上图,R.converge(Fn, [fn1, fn2]),data 会传入 fn1, fn2 进行运算,最後作为 Fn 的参数,再运算出最终结果。

举例,现在有一个需求,是需要将 API 回传的格式,用 pdfmake 转成 pdf 档,所以我们需要做一层 refine 层

API format

[
    {
        "albumId": 1,
        "id": 1,
        "title": "accusamus beatae ad facilis cum similique qui sunt",
        "url": "https://via.placeholder.com/600/92c952",
        "thumbnailUrl": "https://via.placeholder.com/150/92c952"
    }
]

pdfmake format

{
    content: [
        {
            style: 'tableExample',
            table: {
                headerRows: 1,
                body: [
                    [
                        {text: 'albumId', style: 'tableHeader'}, 
                        {text: 'id', style: 'tableHeader'}, 
                        {text: 'title', style: 'tableHeader'},
                        {text: 'url', style: 'tableHeader'},
                        {text: 'thumbnailUrl', style: 'tableHeader'}
                    ],
                    [
                     1, 
                     1, 
                     "accusamus beatae ad facilis cum similique qui sunt", 
                     "https://via.placeholder.com/600/92c952", 
                     "https://via.placeholder.com/150/92c952"
                    ],
                ]
            },
            layout: 'lightHorizontalLines'
        }
    ]
}

读者们可以练习看看

此时我们可以透过 R.converge 去做 refine

const format = val => ({ text: val, style: 'tableHeader' })

const refine = R.converge(
    R.pair, 
    [
      R.compose(R.map(format), R.keys, R.head),
      R.map(R.values)
    ]
);

const toPDFFormat = data => ({
    content: [
        {
            style: 'tableExample',
            table: {
                headerRows: 1,
                body: refine(data)
            },
            layout: 'lightHorizontalLines'
        }
    ]
})

用图说明上面的流程,首先 data 会分别传入 R.compose(R.map(format), R.keys, R.head) 以及 R.map(R.values) 进行运算,将 key 与 value 改写跟取出成 pdfmake 所要的格式, 最终再进行合并。

https://i.imgur.com/RNUNYkn.png

Identity

identity, 在 Functional Programming 是一个使用频率很高的函式,实作也非常简单

const identity = x => x;

对,没错,就是将传入的参数原封不动的回传,想必读者们可能会开始纳闷,这个函式到底可以用在那些地方。

笔者一开始看到 identity 是在 Functional-Light-JS,其范例是将其作为 predicate 函式,举例

import * as R from 'ramda'

const wordArr = ['', 'hello', 'world'];

wordArr.filter(R.identity); // ['hello', 'world']

而以下是笔者在专案中使用到的情境

在进行链式 (chainable) 写法时,非常适合将函式 default value 设定为 identity

举例来说,现在有查询使用者资料的模组,此一模组负责验证,送出请求...等, 而所有用到此一业务逻辑的页面都会用此一模组

const queryUser = (url, schema, options) => {

    /** ... other logic ... */ 
    
    const submitProcess = (data) => 
        schema
            .validate(data)
            .then(d => 
                fetch(url, { body: JSON.stringify(d), method: 'POST'})
            )
            .then(successHandler)
            .catch(console.error)
            
    /** ... other logic ... */ 

    return {
        submit: submitProcess
    }
}

假设现在想要在验证结束後,送出请求前对送出资料进行 refine, 这时候我们就可以在多新增一条 .then(refineAfterValidated) ,并且为了不要动到其他已用此一模组的服务,我们就可以给refineAfterValidated 预设为 identity ,这样既不会影响现有服务,也可以扩充新需求。

const queryUser = (url, schema, options) => {

    const { refineAfterValidated = identity } = options;
 
    /** ... other logic ... */ 
    const submitProcess = (data) => 
        schema
            .validate(data)
            .then(refineAfterValidated)
            .then(d => 
                fetch(url, { body: JSON.stringify(d), method: 'POST'})
            )
            .then(successHandler)
            .catch(console.error)
            
    /** ... other logic ... */ 

    return {
        submit: submitProcess
    }
}

lift, liftN, ap

这个概念其实源自於 Functional Programming 的 Applying,是一个非常重要的概念,也会在之後的文章讲到。

现在读者们可以思考一件事情,当两组 Array 内的值要进行相加时,我们会怎麽做?

const add = (x, y) => x + y;

add(Array.of(1), Array.of(1)) // "11" 

结果出现了 11 ,为什麽呢? 因为 Array 内值的就像是在一个 container 内,我们必须将它取出才能进行运算,所以我们需要先将值透过 map 取出来,才能进行运算

Array.of(1).map(x => Array.of(1).map(y => add(x, y))) // [[2]]

// same as

[1].map(x => [1].map(y => add(x, y))

但问题似乎还没有解决,现在的值反而被两层 Container(Array) 包覆住了,所以我们需要压平它

Array.of(1).flatMap(x => Array.of(1).map(y => add(x, y))) // [2]

这样看起来非常的复杂,lift 就是用来解决这个问题的勇者,其概念就是 Applying,之後会在 Applying 的时候讲解!

import * as R from 'ramda'

R.lift(R.add)([1], [1]) // [2]

这样是不是变得非常乾净!!!!!

但要注意放入 liftliftN 的函式都必须是 currying 化的函式!

而另外一个实用的小技巧就是有需要进行排列组合的运算时,也可以使用 lift ,

const color = ['Black', 'White']; 
const size = ['S', 'M', 'L'];

const result = lift(pair)(color, size)

// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]

也可以用相同概念 liftN 去改写

const color = ['Black', 'White']; 
const size = ['S', 'M', 'L'];
const combine = [color, size]

const result = liftN(combine.length, pair)(...combine)

// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]

而其实 liftN 底层时做就是用 ramda 的 apmap 进行实作,我们就来简单实作一个简易版的 liftN

import * as R from 'ramda';

const lift2 = R.curry((g, f1, f2) => R.ap(R.map(g, f1), f2))

const liftN_ = R.curry((arity, fn, list) => 
   list.slice(1, arity)
      .reduce((acc, val) => ap(acc, val), R.map(fn, list[0])))


liftN_(2, pair, [color, size])
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]

tranduce

当日常开发中,遇到连续使用 .map .filter 以及 .reduce 的情境时,就非常适合用 transduce 去进行优化,由於有几个 .map.filter 时间复杂度就会多几个 O(n),而 transduce 就是将其优化到无论现在有几个 .map.filter 时间复杂度就是固定的O(n) ,之後会在未来的文章内提到实作方法。

假设我们现在有一组阵列,要乘三後取偶数,原本写法

const tripleIt = x => x * 3;
const isEven = (num) => num % 2 === 0

const arr = [1, 2, 3, 4]

// 原本写法
arr.map(tripleIt).filter(isEven) // [6, 12]

用 transduce後写法

import * as R from 'ramda'

const transducer = R.compose(R.map(tripleIt), R.filter(isEven))

R.transduce(transducer, R.flip(R.append), [], [1, 2, 3, 4]); // [6, 12]

tap

在 Function Composition 的时候有提到如何对 compose 进行 debug

tap 就节省了我们写 log 的时间,所以可以将上次范例改写

import * as R from 'ramda'

const makeItalianHerbalBreast = 
    compose(
        R.tap(console.log),
        wrapIt, 
        R.tap(console.log),
        addFlavor('italianHerbal'),
        R.tap(console.log),
        grab('breast'),
        R.tap(console.log),
    )(WHOLE_CHICKEN)


// [ 'leg', 'wing', 'breast', 'buttock', 'breast strips', 'land', 'bug' ]
// breast
// italianHerbal breast
// Wrapped(italianHerbal breast

小结

感谢大家的阅读!

NEXT: Lense


<<:  [Day 20] Node 注册事件 1

>>:  【Day 06】 实作 - 设定 Google Analytics 工具查看 WordPress 网站

Day6 Project1 - 履历

补充一点HTML的资讯,HTML从1995年至今已经发展了多个版本,目前主流使用为HTML5,每个版...

简单的 HelloWorld ~

今天本来要照着顺序来聊聊开发环境介绍的,但太枯燥了先跳过吧XD 想说点简单的,顺便为下一篇内容提出几...

如果你只用积极情绪做事,那你的效率可能只有50%

Day14 - 谈谈有关情绪 在《真实的幸福》这本书里面,提到了积极情绪跟消极情绪的使用方法。 积极...

[Day 26] review 一下我们的程序,谈谈 DSL 和 DAO 的差异

前面讲了很多 Kotlin Exposed 框架使用的方式。 今天来讲点观念性的东西,谈谈 Expo...

[从0到1] C#小乳牛 练成基础程序逻辑 Day 23 - foreach 遍历 阵列 清单 var

遍历便利 | 细数每个item | 不擅长读空气? var支援你 ...