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
的概念并不复杂,然而函数式编程的哲学就是这样,但愿经过一个个细微的抽象,将它们组合在一块儿,最终成就了一个更为庞大的抽象,构建出了一个极其优雅的系统。