深入探討 FP 的 Point-Free Style

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

内容简介: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


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

查看所有标签

猜你喜欢:

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

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2011-6-26 / CAD 61.50

Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换