如何使用常用運算子組合值和函式

在物件導向程式設計中,常見的任務是組合物件(值)。在函數語言程式設計中,組合值和函式是常見的任務。

我們習慣於使用+-*/等運算子來構建其他程式語言的經驗值。

價值構成

let x = 1 + 2 + 3 * 2

由於函數語言程式設計既包含函式也包含值,因此有一些常見的函式組合運算子如 >><<|><|就不足為奇了。

功能構成

// val f : int -> int
let f v   = v + 1
// val g : int -> int
let g v   = v * 2

// Different ways to compose f and g
// val h : int -> int
let h1 v  = g (f v)
let h2 v  = v |> f |> g   // Forward piping of 'v'
let h3 v  = g <| (f <| v) // Reverse piping of 'v' (because <| has left associcativity we need ())
let h4    = f >> g        // Forward functional composition
let h5    = g << f        // Reverse functional composition (closer to math notation of 'g o f')

F# 中,前向管道優於反向管道,因為:

  1. 型別推斷(通常)從左到右流動,因此值和函式從左到右流動也是很自然的
  2. 因為 <|<< 應該具有正確的關聯性但是在 F# 中它們是左聯的,這迫使我們插入()
  3. 混合正向和反向管道通常不起作用,因為它們具有相同的優先順序。

Monad 組成

由於 Monads(如 Option<'T>List<'T>)常用於函數語言程式設計,因此還有一些常見但不太知名的運算子來組合使用 Monad 的函式,如 >>=>=><|><*>

let (>>=) t uf  = Option.bind uf t
let (>=>) tf uf = fun v -> tf v >>= uf
// val oinc   : int -> int option
let oinc   v    = Some (v + 1)    // Increment v
// val ofloat : int -> float option
let ofloat v    = Some (float v)  // Map v to float

// Different ways to compose functions working with Option Monad
// val m : int option -> float option
let m1 v  = Option.bind (fun v -> Some (float (v + 1))) v
let m2 v  = v |> Option.bind oinc |> Option.bind ofloat
let m3 v  = v >>= oinc >>= ofloat
let m4    = oinc >=> ofloat

// Other common operators are <|> (orElse) and <*> (andAlso)

// If 't' has Some value then return t otherwise return u
let (<|>) t u =
  match t with
  | Some _  -> t
  | None    -> u

// If 't' and 'u' has Some values then return Some (tv*uv) otherwise return None
let (<*>) t u =
  match t, u with
  | Some tv, Some tu  -> Some (tv, tu)
  | _                 -> None

// val pickOne : 'a option -> 'a option -> 'a option
let pickOne t u v = t <|> u <|> v

// val combine : 'a option -> 'b option  -> 'c option -> (('a*'b)*'c) option
let combine t u v = t <*> u <*> v

結論

對於新的函式式程式設計師,使用運算子的函式組合可能看起來不透明和模糊,但這是因為這些運算子的含義並不像處理值的運算子那樣通常。然而,通過使用|> 的一些訓練,>>>>=>=> 變得像使用+-*/一樣自然。