函数式编程 - 有趣的Monoid(单位半群)

前言

Monoid(中文:单位半群,又名:幺半群),一个来源于数学的概念;得益于它的抽象特性,Monoid在函数式编程中起着较为重大的做用。数据库

本篇文章将会以工程的角度去介绍Monoid的相关概念,并结合几个有趣的数据结构(如MiddlewareWriter)来展示Monoid自身强大的能力及其实用性。编程

Semigroup(半群)

在开始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闭包

如下为StringArray类型实现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(单位半群)

定义

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静态属性来表明单位元

咱们再为StringArray类型实现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中,咱们就很难像上面那样一直在写链式运算,否则代码会变得复杂难堪。此时能够基于Sequencereduce方法来定义咱们的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

接下来我将介绍几个实用的Monoid,它们能用在平常的项目开发上,让你的代码可读性更加简洁清晰,可维护性也变得更强(最重要是优雅)。

Middleware(中间件)

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 } }
}
复制代码

比起TodoMiddlewaretodo闭包上设置了一个参数,参数的类型为Middleware中定义了的泛型。

Middleware的做用就是让某个值经过一连串的中间件,这些中间件所作的事情各不相同,它们可能会对值进行加工,或者完成一些反作用(打Log、数据库操做、网络操做等等)。Monoidappend操做将每一个中间件组合在一块儿,造成一个统一的入口,最终咱们只需将值传入这个入口便可。

接下来就是一个简单使用到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装饰字体、背景颜色和前景颜色。standardtitle则将基本的中间件进行组合,这两个组合体用于特定的情境下(为做为标题和做为正文的富文本装饰),最终文字的解析则经过调用指定中间件来完成。

经过以上的例子咱们能够认识到:TodoMiddleware都是一种对行为的抽象,它们之间的区别在于Todo在行为的处理中并不接收外界的数据,而Middleware可从外界获取某种对行为的输入。

Order

试想一下咱们平时的开发中会常常遇到如下这种问题:

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

文章已在以前为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,咱们首先得声明一个数组变量,而后再根据每一个条件去给数组添加元素。这样的代码是没有美感的!

咱们经过使用ArrayMonoid特性来重构一下上面的代码:

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

Writer Monad是一个基于MonoidMonad(单子),旨在执行操做的过程当中去顺带记录特定的信息,如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运算来源于函数式编程概念Functorreturnbind则来源于Monad。你们若是对此有兴趣的能够查阅相关的内容,或者阅读我在以前写的有关于这些概念的文章。

利用Writer Monad,咱们就能够专心于编写代码的业务逻辑,而没必要花时间在一些信息的记录上,Writer会自动帮你去记录。

这篇文章没有说起到的Monoid还有不少,如AnyAllOrdering ...,你们能够经过查阅相关文档来进行。

对于Monoid来讲,重要的不是在于去了解它相关的实现例子,而是要深入地理解它的抽象概念,这样咱们才能说认识Monoid,才能触类旁通,去定义属于本身的Monoid实例。

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

相关文章
相关标签/搜索