iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Software Development

Functional Programming For Everyone系列 第 5

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 04 - Function Composition
下一篇
Day 06 - Lenses (Basic)
系列文
Functional Programming For Everyone30

1 則留言

0
Ken Chen
iT邦新手 5 級 ‧ 2021-09-20 22:09:02

identity 真的是剛學 FP 時覺得莫名奇妙的東西,但等真的需要用到就知道它的存在價值了XDD

我記得第一次知道 transduce 的意義跟用途也是看大大的部落格文章。大心~

期待後續的分享耶~

我要留言

立即登入留言