深入探討 FP 的 Point-Free Style

栏目: ASP.NET · 发布时间: 5年前

内容简介:Curry Function 最主要的目的在於 Function Composition,所以儘管是那 argument 該怎樣的設計才適合 composition 呢 ? 這就是本文的主題:Point-Free Style。F# 4.1

Curry Function 最主要的目的在於 Function Composition,所以儘管是 個 argument,最後也可變成多個 單一 argument 的 function 方便 composition。

那 argument 該怎樣的設計才適合 composition 呢 ? 這就是本文的主題:Point-Free Style。

Version

F# 4.1

ECMAScript 6

Ramda 0.25

FSharp

在學習 F# 時,由於 F# 是純 FP 語言,function 可自動成為 Curry Function,常發現 F# 的 function 會如此設計。

List.map()

mapping : (‘T -> ‘U) -> list : ‘T list -> ‘U list

List.filter()

predicate : (‘T -> bool) -> list : ‘T list -> ‘T list

List.reduce()

reduction : (‘T -> ‘T -> ‘T) -> list : ‘T list -> ‘T

僅管 List.map()List.filter()List.reduce() 三個 function 功能都不同,input 與 return 值也不同,但最後一個 argument 一定都是 list : 'T list

Q : 這樣設計 argument 到底有什麼優點呢 ?

Pipeline

let mapSquare = List.map (fun elm -> elm * elm)
let filterOdd = List.filter (fun elm -> elm % 2 = 1)
let sum = List.reduce (fun acc elm -> acc + elm)

let calculate data = 
    data
    |> mapSquare
    |> filterOdd
    |> sum
    
calculate [1 .. 3] 
|> printf "%A"

第 1 行

let mapSquare = List.map (fun elm -> elm * elm)
let filterOdd = List.filter (fun elm -> elm % 2 = 1)
let sum = List.reduce (fun acc elm -> acc + elm)

List.map()List.filter()List.reduce() 都僅提供 1 個 argument,所以 mapSquare()filterOdd()sum() 都是 function。

第 5 行

let calculate data = 
    data
    |> mapSquare
    |> filterOdd
    |> sum

mapSquare()filterOdd()sum() 透過 Pipeline 處理 data。

因為 mapSquare()filterOdd()sum() 的最後一個 argument 都是 list : 'T list ,在 Pipeline 時,F# 允許省略之。

|>為 F# 的 Pipeline, 由左至右

Function Composition

let mapSquare = List.map (fun elm -> elm * elm)
let filterOdd = List.filter (fun elm -> elm % 2 = 1)
let sum = List.reduce (fun acc elm -> acc + elm)

let calculate = mapSquare >> filterOdd >> sum
    
calculate [1 .. 3] 
|> printf "%A"

// 10

第 1 行

let mapSquare = List.map (fun elm -> elm * elm)
let filterOdd = List.filter (fun elm -> elm % 2 = 1)
let sum = List.reduce (fun acc elm -> acc + elm)

List.map()List.filter()List.reduce() 都僅提供 1 個 argument,所以 mapSquare()filterOdd()sum() 都是 function。

第 5 行

let calculate = mapSquare >> filterOdd >> sum

mapSquare()filterOdd()filterOdd() 組合成 calculate()

因為 mapSquare()filterOdd()sum() 的最後一個 argument 都是 list : 'T list ,在 Composition 時,F# 允許省略之。

>>為 F# 的 Function Composition, 由左至右

我們可以發現 F# 在設計 function 時,會 故意 將要處理的 資料 放在最後一個 argument,將 條件 放在前面的 argument,如此所有 function 無論要做 Pipeline 或 Composition 時,都可省略最後一個 argument,讓程式碼更加簡潔。

Pipeline 與 Function Composition 講的其實是同一件事情,只是 F# 文化較喜歡使用 Pipeline,而 Haskell 較喜歡使用 Function Composition,稍後將統一使用 Function Composition

當初以為這只是 F# 的 syntax sugar,後來在歐陽繼超的 前端函數式攻城指南Haskell 趣學指南 這兩本書,才發現這是 FP 特有風格,稱為 Point-Free Style

Definition

Point-Free Style

Function 不特別將要處理的 data 放進參 argument,因此也不回傳處理過的 data,而是回傳 function,這有助於 Function Composition,也稱為 Tacit Programming

Q:為甚麼 Point-Free Style 能成立呢 ?

let fn data = List.map (fun elm -> elm * elm) data

若原本 fn() 帶一個 argument,傳入要處理的 data ,相當於將 data 傳入 List.map (fun elm -> elm * elm) ,並回傳處理過的 data

let fn = List.map (fun elm -> elm * elm)

由於 F# 的 function 天生是 Curry Function, fn() 沒有 argument, = 左右將 data 同時消去,就相當於回傳 List.map (fun elm -> elm * elm) function。

所以一個 function 將 data 放在最後一個 argument 時,提不提供 data 都成立:

  • 有提供 data 則回傳處理過的 data
  • 不提供 data 則回傳 function

由於回傳是 function,特別適合做 Function Composition。

Q:為什麼稱為 Point-Free ?

Point 所指的就是傳入 data 的 argument, Point-Free 就是指 function 將 data 放在最後一個 argument,要使用時故意將最後一個 data argumet 丟棄 (free),則變成回傳 function,但若 data 不是最後一個 argument,則無法丟棄,因此也無法變成 function,所以也無法繼續再做 Function Composition。

Q:為什麼 Point-Free Style 適合做 Function Composition ?

Function Composition 事實上來自於數學的 合成函數 ,也就是 fog(x) = f(g(x)) ,其中:

fog(x) = f(y)
y      = g(x)

也就是 g(x) 的 output 剛好為 f(y) 的 input,因此才能將 f(g(x)) 合併,變成 fog(x) ,其中的 y 剛好被消滅。

f()g() 每個 function 的 格式都一樣 ,都是最後一個 argument 為 data ,則可將所有 function 加以組合成一個新 function,這就是 Function Composition。

JavaScript

前面談的都是 F#,你可能看得似懂非懂,我們就來將相同程式碼以大家熟悉的 JavaScript 改寫:

const _map = fn => data => data.map(fn);
const _filter = fn => data => data.filter(fn);
const _reduce = fn => data => data.reduce(fn);

const mapSquare = _map(elm => elm * elm);
const filterOdd = _filter(elm => elm % 2);
const sum = _reduce((acc, elm) => acc + elm);

const compose = (...fns) =>
    fns.reduce((f, g) => (...args) => f(g(...args)));

const calculate = compose(sum, filterOdd, mapSquare);

const result = calculate([1, 2, 3]);
console.log(result);

// 10

第 1 行

const _map = fn => data => data.map(fn);
const _filter = fn => data => data.filter(fn);
const _reduce = fn => data => data.reduce(fn);

JavaScript 雖然都有提供 map()filter()reduce() ,但這些都是尚未 Curry 化的 function,無法使用 Function Composition,所以我們第一步就是將這些 function 改寫成 Curry Function。

第 5 行

const mapSquare = _map(elm => elm * elm);
const filterOdd = _filter(elm => elm % 2);
const sum = _reduce((acc, elm) => acc + elm);

再改寫成 Point-Free Style function。

第 9 行

const compose = (...fns) =>
    fns.reduce((f, g) => (...args) => f(g(...args)));

由於 JavaScript 沒有提供 compose() 組合 function,我們自己土炮用 reduce() 寫一個 compose() ,負責將多個 Point-Free Style function 組合成單一 function。

12 行

const calculate = compose(sum, filterOdd, mapSquare);

mapSquare()filterOdd()sum() 組合成 calculate()

這裡與 F# 不一樣,而是 由右至左

JavaScript 雖然寫的出來,但由於沒有直接支援 Curry Function 與 compose() ,因此寫起來有點冗長

Ramda

import map from 'ramda/src/map';
import filter from 'ramda/src/filter';
import reduce from 'ramda/src/reduce';
import compose from 'ramda/src/compose';

const mapSquare = map(elm => elm * elm);
const filterOdd = filter(elm => elm % 2);
const sum = reduce((acc, elm) => acc + elm, 0);

const calculate = compose(sum, filterOdd, mapSquare);

const result = calculate([1, 2, 3]);
console.log(result);

第 1 行

import map from 'ramda/src/map';
import filter from 'ramda/src/filter';
import reduce from 'ramda/src/reduce';

從 Ramda 引入 map()filter()reduce() ,這些都是已經是 Curry Function。

第 4 行

import compose from 'ramda/src/compose';

從 Ramda 引入 compose() ,這樣我們就不必自己實作 compose() 了。

第 6 行

const mapSquare = map(elm => elm * elm);
const filterOdd = filter(elm => elm % 2);
const sum = reduce((acc, elm) => acc + elm, 0);

將 Ramda 的 function 改寫成 Point-Free Style function。

10 行

const calculate = compose(sum, filterOdd, mapSquare);

使用 Ramda 的 compose()mapSquare()filterOdd()sum() 組合成 calculate()

這裡與 F# 不一樣,而是 由右至左

Ramda 的版本就非常精簡,我們只需實作 Point-Free Style function 再加以組合即可,整體風格已經與純 FP 的 F# 非常接近

Summary

Q : Function Composition 該 由左至右 ,還是該 由右至左 呢 ?

  • F# 的 >>由左至右 ,優點是程式碼可讀性較佳
  • Haskell 的 . 與 Ramda 的 compose()由右至左 ,優點是與數學的 fog(x) = f(g(x)) 一樣 由右至左

個人是偏好 F# 的 由左至右 ,不過由於 Haskell 與 Ramda 的文化就是 由右至左 ,也只能習慣了。

Conclusion

  • Point-Free Style 是 FP 設計 argument 的基本精神,這也是為什麼歐陽繼超在 前端函數式攻城指南 一書中指出 Underscore 設計錯誤,因為 Underscore 是 _.map([1, 2, 3], x => x + 1) ,將 data 放在第 1 個 argument,這並不符合 Point-Free Style
  • FP 首重觀念,只要心裡有 Function Composition,無論是在 F# 或在 JavaScript 都可實作
  • 純 JavaScript 實作稍微冗長,若使用 Ramda,則整體精簡程度已經與 F# 非常接近

Reference

歐陽繼超, 前端函數式攻城指南

Miran Lipovaca, Haskell 趣學指南

Wikipedia , Tacit Programming


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

U一点·料

U一点·料

阿里巴巴集团 1688用户体验设计部 / 机械工业出版社 / 2015-7-13 / 79.00元

《U一点·料——阿里巴巴1688UED体验设计践行之路》是1688UED团队历经多年实践之后的心血之作,书中以“道─术─器”的思路为编排脉络,从设计观、思考体系、方法论上层层剖析,将零散的行业knowhow串成体系,对“UED如何发展突破”提出了自己的真知灼见。该书重实战、讲方法、求专业、论文化,是一部走心的诚意之作。 本书作者从美工到用户体验设计师,从感性随意到理性思考,从简单的PS做图到......一起来看看 《U一点·料》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具