Monoid
(中文:單位半羣
,又名:幺半羣
),一個來源於數學的概念;得益於它的抽象特性,Monoid
在函數式編程中起着較爲重大的做用。數據庫
本篇文章將會以工程的角度去介紹Monoid
的相關概念,並結合幾個有趣的數據結構(如Middleware
、Writer
)來展示Monoid
自身強大的能力及其實用性。編程
在開始Monoid
的表演以前,咱們首先來感覺一下Semigroup(半羣)
,它在 維基百科上的定義 爲:api
集合S和其上的二元運算·:S×S→S。若·知足結合律,即:∀x,y,z∈S,有(x·y)·z=x·(y·z),則稱有序對(S,·)爲半羣,運算·稱爲該半羣的乘法。實際使用中,在上下文明確的狀況下,能夠簡略敘述爲「半羣S」。數組
上面的數學概念比較抽象,理解起來可能比較麻煩。下面結合一個簡單的例子來通俗說明:bash
對於天然數一、二、三、四、五、...
而言,加法運算+
可將兩個天然數相加,獲得的結果仍然是一個天然數,而且加法是知足結合律的:(2 + 3) + 4 = 2 + (3 + 4) = 9。如此一來咱們就能夠認爲天然數和加法運算組成了一個半羣。相似的還有天然數與乘法運算等。網絡
經過以上的例子,半羣的概念很是容易就能理解,下面我經過Swift
語言的代碼來對Semigroup
進行實現:數據結構
// MARK: - Semigroup
infix operator <> : AdditionPrecedence
protocol Semigroup {
static func <> (lhs: Self, rhs: Self) -> Self
}
複製代碼
協議Semigroup
中聲明瞭一個運算方法,該方法的兩個參數與返回值都是同一個實現了半羣的類型。咱們一般將這個運算稱爲append
。閉包
如下爲String
和Array
類型實現Semigroup
,並進行簡單的使用:app
extension String: Semigroup {
static func <> (lhs: String, rhs: String) -> String {
return lhs + rhs
}
}
extension Array: Semigroup {
static func <> (lhs: [Element], rhs: [Element]) -> [Element] {
return lhs + rhs
}
}
func test() {
let hello = "Hello "
let world = "world"
let helloWorld = hello <> world
let one = [1,2,3]
let two = [4,5,6,7]
let three = one <> two
}
複製代碼
Monoid
自己也是一個Semigroup
,額外的地方是它多了單位元
,因此被稱做爲單位半羣
。單位元
在維基百科上的定義 爲:less
在半羣S的集合S上存在一元素e,使得任意與集合S中的元素a都符合 a·e = e·a = a
舉個例子,在上面介紹Semigroup
的時候提到,天然數跟加法運算組成了一個半羣。顯而易見的是,天然數0
跟其餘任意天然數相加,結果都是等於原來的數:0 + x = x
。因此咱們能夠把0
做爲單位元,加入到由天然數和加法運算組成的半羣中,從而獲得了一個單位半羣。
下面就是Monoid
在Swift中的定義:
protocol Monoid: Semigroup {
static var empty: Self { get }
}
複製代碼
能夠看到,Monoid
協議繼承自Semigroup
,而且用empty
靜態屬性來表明單位元
。
咱們再爲String
和Array
類型實現Monoid
,並簡單演示其使用:
extension String: Monoid {
static var empty: String { return "" }
}
extension Array: Monoid {
static var empty: [Element] { return [] }
}
func test() {
let str = "Hello world" <> String.empty // Always "Hello world"
let arr = [1,2,3] <> [Int].empty // Always [1,2,3]
}
複製代碼
對於有多個Monoid
的連續運算,咱們如今寫出來的代碼是:
let result = a <> b <> c <> d <> e <> ...
複製代碼
若Monoid
的數量居多,又或者它們是被包裹在一個數組或Sequence中,咱們就很難像上面那樣一直在寫鏈式運算,否則代碼會變得複雜難堪。此時能夠基於Sequence
的reduce
方法來定義咱們的Monoid
串聯運算concat
:
extension Sequence where Element: Monoid {
func concat() -> Element {
return reduce(Element.empty, <>)
}
}
複製代碼
如此一來咱們就能夠很方便地爲位於數組或Sequence中的若干個Monoid
進行串聯運算:
let result = [a, b, c, d, e, ...].concat()
複製代碼
在開始討論Monoid
的條件性質前,咱們先引入一個十分簡單的數據結構,其主要是用於處理計劃中即將執行的某些任務,我把它命名爲Todo
:
struct Todo {
private let _doIt: () -> ()
init(_ doIt: @escaping () -> ()) {
_doIt = doIt
}
func doIt() { _doIt() }
}
複製代碼
它的使用很簡單:咱們先經過一個即將要處理的操做來構建一個Todo
實例,而後在適當的時機調用doIt
方法便可:
func test() {
let sayHello = Todo {
print("Hello, I'm Tangent!")
}
// Wait a second...
sayHello.doIt()
}
複製代碼
這裏還未能體現到它的強大,接下來咱們就爲它實現Monoid
:
extension Todo: Monoid {
static func <> (lhs: Todo, rhs: Todo) -> Todo {
return .init {
lhs.doIt()
rhs.doIt()
}
}
static var empty: Todo {
// Do nothing
return .init { }
}
}
複製代碼
在append
運算中咱們返回了一個新的Todo
,它須要作的事情就是前後完成左右兩邊傳入的Todo
參數各自的任務。另外,咱們把一個什麼都不作的Todo
設爲單位元,這樣就能知足Monoid
的定義。
如今,咱們就能夠把多個Todo
串聯起來,下面就來把玩一下:
func test() {
let sayHello = Todo {
print("Hello, I'm Tangent!")
}
let likeSwift = Todo {
print("I like Swift.")
}
let likeRust = Todo {
print("And also Rust.")
}
let todo = sayHello <> likeSwift <> likeRust
todo.doIt()
}
複製代碼
有時候,任務是按照某些特定條件來判斷是否被執行,好比像上面的test
函數中,咱們須要根據特定的條件來判斷是否要執行三個Todo
,從新定義函數簽名:
func test(shouldSayHello: Bool, shouldLikeSwift: Bool, shouldLikeRust: Bool)
爲了可以實現這種要求,一般來講有如下兩種較爲蛋疼的作法:
// One
func test(shouldSayHello: Bool, shouldLikeSwift: Bool, shouldLikeRust: Bool) {
let sayHello = Todo {
print("Hello, I'm Tangent!")
}
let likeSwift = Todo {
print("I like Swift.")
}
let likeRust = Todo {
print("And also Rust.")
}
var todo = Todo.empty
if shouldSayHello {
todo = todo <> sayHello
}
if shouldLikeSwift {
todo = todo <> likeSwift
}
if shouldLikeRust {
todo = todo <> likeRust
}
todo.doIt()
}
// Two
func test(shouldSayHello: Bool, shouldLikeSwift: Bool, shouldLikeRust: Bool) {
let sayHello = Todo {
print("Hello, I'm Tangent!")
}
let likeSwift = Todo {
print("I like Swift.")
}
let likeRust = Todo {
print("And also Rust.")
}
var arr: [Todo] = []
if shouldSayHello {
arr.append(sayHello)
}
if shouldLikeSwift {
arr.append(likeSwift)
}
if shouldLikeRust {
arr.append(likeRust)
}
arr.concat().doIt()
}
複製代碼
這兩種寫法都略爲複雜,而且還引入了變量,代碼一點都不優雅。
這時,咱們就能夠爲Monoid
引入條件判斷:
extension Monoid {
func when(_ flag: Bool) -> Self {
return flag ? self : Self.empty
}
func unless(_ flag: Bool) -> Self {
return when(!flag)
}
}
複製代碼
在when
方法中,若是傳入的布爾值爲true
,那麼此方法將會原封不動地把本身返回,而若是傳入了false
,函數則返回一個單位元,至關於丟棄掉如今的本身
(由於單位元跟任意元素進行append
運算結果都是元素自己)。unless
方法則只是簡單地互換一下when
參數中的布爾值。
如今,咱們就能優化一下剛剛test
函數的代碼:
func test(shouldSayHello: Bool, shouldLikeSwift: Bool, shouldLikeRust: Bool) {
let sayHello = Todo {
print("Hello, I'm Tangent!")
}
let likeSwift = Todo {
print("I like Swift.")
}
let likeRust = Todo {
print("And also Rust.")
}
let todo = sayHello.when(shouldSayHello) <> likeSwift.when(shouldLikeSwift) <> likeRust.when(shouldLikeRust)
todo.doIt()
}
複製代碼
比起以前的兩種寫法,這裏優雅了很多。
接下來我將介紹幾個實用的Monoid
,它們能用在平常的項目開發上,讓你的代碼可讀性更加簡潔清晰,可維護性也變得更強(最重要是優雅)。
Middleware
結構很是相似於剛剛在文章上面提到的Todo
:
struct Middleware<T> {
private let _todo: (T) -> ()
init(_ todo: @escaping (T) -> ()) {
_todo = todo
}
func doIt(_ value: T) {
_todo(value)
}
}
extension Middleware: Monoid {
static func <> (lhs: Middleware, rhs: Middleware) -> Middleware {
return .init {
lhs.doIt($0)
rhs.doIt($0)
}
}
// Do nothing
static var empty: Middleware { return .init { _ in } }
}
複製代碼
比起Todo
,Middleware
在todo
閉包上設置了一個參數,參數的類型爲Middleware
中定義了的泛型。
Middleware
的做用就是讓某個值經過一連串的中間件,這些中間件所作的事情各不相同,它們可能會對值進行加工,或者完成一些反作用(打Log、數據庫操做、網絡操做等等)。Monoid
的append
操做將每一箇中間件組合在一塊兒,造成一個統一的入口,最終咱們只需將值傳入這個入口便可。
接下來就是一個簡單使用到Middleware
的例子,假設咱們如今須要作一個對富文本NSAttributedString
進行裝飾的解析器,在裏面咱們能夠根據須要來爲富文本提供特定的裝飾(修改字體、前景或背景顏色等),咱們能夠這樣定義:
// MARK: - Parser
typealias ParserItem = Middleware<NSMutableAttributedString>
func font(size: CGFloat) -> ParserItem {
return ParserItem { str in
str.addAttributes([.font: UIFont.systemFont(ofSize: size)], range: .init(location: 0, length: str.length))
}
}
func backgroundColor(_ color: UIColor) -> ParserItem {
return ParserItem { str in
str.addAttributes([.backgroundColor: color], range: .init(location: 0, length: str.length))
}
}
func foregroundColor(_ color: UIColor) -> ParserItem {
return ParserItem { str in
str.addAttributes([.foregroundColor: color], range: .init(location: 0, length: str.length))
}
}
func standard(withHighlighted: Bool = false) -> ParserItem {
return font(size: 16) <> foregroundColor(.black) <> backgroundColor(.yellow).when(withHighlighted)
}
func title() -> ParserItem {
return font(size: 20) <> foregroundColor(.red)
}
extension NSAttributedString {
func parse(with item: ParserItem) -> NSAttributedString {
let mutableStr = mutableCopy() as! NSMutableAttributedString
item.doIt(mutableStr)
return mutableStr.copy() as! NSAttributedString
}
}
func parse() {
let titleStr = NSAttributedString(string: "Monoid").parse(with: title())
let text = NSAttributedString(string: "I love Monoid!").parse(with: standard(withHighlighted: true))
}
複製代碼
如上代碼,咱們首先定義了三個最基本的中間件,分別可用來爲NSAttributedString
裝飾字體、背景顏色和前景顏色。standard
和title
則將基本的中間件進行組合,這兩個組合體用於特定的情境下(爲做爲標題和做爲正文的富文本裝飾),最終文字的解析則經過調用指定中間件來完成。
經過以上的例子咱們能夠認識到:Todo
和Middleware
都是一種對行爲的抽象,它們之間的區別在於Todo
在行爲的處理中並不接收外界的數據,而Middleware
可從外界獲取某種對行爲的輸入。
試想一下咱們平時的開發中會常常遇到如下這種問題:
if 知足條件1 {
執行優先級最高的操做...
} else if 知足條件2 {
執行優先級第二的操做
} else if 知足條件3 {
執行優先級第三的操做
} else if 知足條件4 {
執行優先級第四的操做
} else if ...
複製代碼
這裏可能存在一個問題,那就是優先級的狀況。假設某一天程序要求修改將某個分支操做的優先級,如將優先級第三的操做提高到最高,那此時咱們不得不改動大部分的代碼來完成這個要求:比方說將兩個或多個if
分支代碼的位置互換,這樣改起來那就很蛋疼了。
Order
就是用於解決這種與條件判斷相關的優先級問題:
// MARK: - Order
struct Order {
private let _todo: () -> Bool
init(_ todo: @escaping () -> Bool) {
_todo = todo
}
static func when(_ flag: Bool, todo: @escaping () -> ()) -> Order {
return .init {
flag ? todo() : ()
return flag
}
}
@discardableResult
func doIt() -> Bool {
return _todo()
}
}
extension Order: Monoid {
static func <> (lhs: Order, rhs: Order) -> Order {
return .init {
lhs.doIt() || rhs.doIt()
}
}
// Just return false
static var empty: Order { return .init { false } }
}
複製代碼
在構建Order
的時候,咱們須要傳入一個閉包,在閉包中咱們將處理相關的邏輯,並返回一個布爾值,若此布爾值爲true
,則表明此Order的工做已經完成,那麼以後優先級比它低的Order將不作任何事情,若返回false
,表明在這個Order裏面咱們並無作好某個操做(或者說某個操做不符合執行的要求),那麼接下來優先級比它低的Order將會嘗試去執行自身的操做,而後按照這個邏輯一直下去。
Order的優先級是經過它們排列的順序決定的,比方說let result = orderA <> orderB <> orderC
,那麼優先級就是orderA > orderB > orderC
,由於咱們在定義append
的時候使用到了短路運算符||
。
靜態方法when
可以更加簡便地經過一個布爾值和一個無返回值閉包來構建Order,平常開發可自行選擇使用Order自己的構造函數仍是when
方法。
func test(shouldSayHello: Bool, shouldLikeSwift: Bool, shouldLikeRust: Bool) {
let sayHello = Order.when(shouldSayHello) {
print("Hello, I'm Tangent!")
}
let likeSwift = Order.when(shouldLikeSwift) {
print("I like Swift.")
}
let likeRust = Order.when(shouldLikeRust) {
print("And also Rust.")
}
let todo = sayHello <> likeSwift <> likeRust
todo.doIt()
}
複製代碼
如上面例子中,三個Order
的操做要麼所有都不會執行,要麼就只有一個被執行,這取決於when
方法傳入的布爾值,執行的優先級按照append
運算的前後順序。
文章已在以前爲Array
實現了Monoid
,那麼Array
在平常的開發中如何能夠利用Monoid
的特性呢,咱們來看下面的這個代碼:
class ViewController: UIViewController {
func setupNavigationItem(showAddBtn: Bool, showDoneBtn: Bool, showEditBtn: Bool) {
var items: [UIBarButtonItem] = []
if showAddBtn {
items.append(UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add)))
}
if showDoneBtn {
items.append(UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)))
}
if showEditBtn {
items.append(UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(edit)))
}
navigationItem.rightBarButtonItems = items
}
@objc func add() { }
@objc func done() { }
@objc func edit() { }
}
複製代碼
就像在以前講Todo
那樣,這樣的代碼寫法的確不優雅,爲了給ViewController設置rightBarButtonItems,咱們首先得聲明一個數組變量,而後再根據每一個條件去給數組添加元素。這樣的代碼是沒有美感的!
咱們經過使用Array
的Monoid
特性來重構一下上面的代碼:
class ViewController: UIViewController {
func setupNavigationItem(showAddBtn: Bool, showDoneBtn: Bool, showEditBtn: Bool) {
let items = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add))].when(showAddBtn)
<> [UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))].when(showDoneBtn)
<> [UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(edit))].when(showEditBtn)
navigationItem.rightBarButtonItems = items
}
@objc func add() { }
@objc func done() { }
@objc func edit() { }
}
複製代碼
這下子就優雅多了~
Writer Monad
是一個基於Monoid
的Monad(單子)
,旨在執行操做的過程當中去順帶記錄特定的信息,如Log或者歷史記錄。若你不瞭解Monad
沒有關係,這裏不會過多說起與它的相關,在閱讀代碼時你只須要搞清楚其中的實現原理便可。
// MARK: - Writer
struct Writer<T, W: Monoid> {
let value: T
let record: W
}
// Monad
extension Writer {
static func `return`(_ value: T) -> Writer {
return Writer(value: value, record: W.empty)
}
func bind<O>(_ tran: (T) -> Writer<O, W>) -> Writer<O, W> {
let newOne = tran(value)
return Writer<O, W>(value: newOne.value, record: record <> newOne.record)
}
func map<O>(_ tran: (T) -> O) -> Writer<O, W> {
return bind { Writer<O, W>.return(tran($0)) }
}
}
// Use it
typealias LogWriter<T> = Writer<T, String>
typealias Operation<T> = (T) -> LogWriter<T>
func add(_ num: Int) -> Operation<Int> {
return { Writer(value: $0 + num, record: "\($0)加\(num), ") }
}
func subtract(_ num: Int) -> Operation<Int> {
return { Writer(value: $0 - num, record: "\($0)減\(num), ") }
}
func multiply(_ num: Int) -> Operation<Int> {
return { Writer(value: $0 * num, record: "\($0)乘\(num), ") }
}
func divide(_ num: Int) -> Operation<Int> {
return { Writer(value: $0 / num, record: "\($0)除\(num), ") }
}
func test() {
let original = LogWriter.return(2)
let result = original.bind(multiply(3)).bind(add(2)).bind(divide(4)).bind(subtract(1))
// 1
print(result.value)
// 2乘3, 6加2, 8除4, 2減1,
print(result.record)
}
複製代碼
Writer
爲結構體,其中包含着兩個數據,一個是參與運算的值,類型爲泛型T,一個是運算時所記錄的信息,類型爲泛型W,而且須要實現Monoid
。
return
靜態方法可以建立一個新的Writer
,它須要傳入一個值,這個值將直接保存在Writer
中。得益於Monoid
單位元的特性,return
在構建Writer
的過程當中直接將empty
設置爲Writer
所記錄的信息。
bind
方法所要作的就是經過傳入一個能將運算值轉化成Writer
的閉包來對原始Writer
進行轉化,在轉化的過程當中bind
將記錄信息進行append
,這樣就能幫助咱們自動進行信息記錄。
map
方法經過傳入一個運算值的映射閉包,將Writer
內部的運算值進行轉換。
其中,map
運算來源於函數式編程概念Functor
,return
和bind
則來源於Monad
。你們若是對此有興趣的能夠查閱相關的內容,或者閱讀我在以前寫的有關於這些概念的文章。
利用Writer Monad
,咱們就能夠專心於編寫代碼的業務邏輯,而沒必要花時間在一些信息的記錄上,Writer
會自動幫你去記錄。
這篇文章沒有說起到的Monoid
還有不少,如Any
、All
、Ordering
...,你們能夠經過查閱相關文檔來進行。
對於Monoid
來講,重要的不是在於去了解它相關的實現例子,而是要深入地理解它的抽象概念,這樣咱們才能說認識Monoid
,才能觸類旁通,去定義屬於本身的Monoid
實例。
事實上Monoid
的概念並不複雜,然而函數式編程的哲學就是這樣,但願經過一個個細微的抽象,將它們組合在一塊兒,最終成就了一個更爲龐大的抽象,構建出了一個極其優雅的系統。