Swift 函数式编程探索(1)—— Functor 和 Monad

最近在看了 swift 大会上由包涵卿大神带来的“Swift 函数式编程实践” 演讲之后,感触颇深,这几天搜索了一些 swift 函数式编程相关的文章了解了一下。这里做一点记录。
这里用到的代码我都写在了 playground 里,放到了 Github 上。

更新:发现解释的更好的大有人在,非常推荐看看:
唐巧大神写的这篇:Swift 烧脑体操(四) - map 和 flatMap
以及这篇:Swift 烧脑体操(五)- Monad
swift gg 翻译的这篇:Swift 2.0 :揭秘 Map 和 FlatMap

在这里记录一下自己的理解(需要阅读者对闭包、泛型等有一定理解)

什么是函数式编程?知乎上的这个回答非常详细:

http://zhihu.com/question/28292740/answer/40336090

而且像包大神说的,转换编程范式实际上是转换思维方式,对于函数式编程,还需要慢慢思考消化来理解。如果有时间,尝试学学 Haskell 来写个应用吧。

这里记录一下个人对 swift 函数式编程相关的一些 feature 的理解。

1. Functor —— 仿函数或函子

任何定义了 map (Haskell 中的 fmap)如何作用于自己的类型都是 Functor
使这个类有了类似于函数的行为,让这个类使用上像函数,称为仿函数
什么是 map 函数呢? map 函数请求一个将自身封装值转换为另一个未封装值的函数,并应用到自身。
Swift 中的 Optional、 SequenceTypes (如数组、字典) 都有 map 函数,所以可以说 swift 中的 optional,array 等都是 functor。

Optional 中的 map 方法

optional 中的的 map 方法是这样定义的:

/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`.
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
// @noescape 标记一个闭包参数表示这个闭包参数不会被传递到其他没有 @noescape 标记闭包参数的方法中,比如其他线程

有了这样的 map 方法,我们可以这样使用:
将一个函数应用到封装内的内容中

let a = Optional.Some(2)
let b = Optional<Int>.None

let addOne: Int -> Int = { $0 + 1 }

a.map(addOne)  //返回一个 Optional(3)
b.map(addOne)  //返回一个 Optional.None
SequenceTypes 中的 map 方法

SequenceTypes 中的 map 方法是这样定义的:

/// Returns an `Array` containing the results of mapping `transform`
/// over `self`.
///
/// - Complexity: O(N).
@warn_unused_result
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

我们能够这样使用:

let ints = [2]
let noInt : [Int] = []

ints.map(addOne)  //返回一个 [2]
noInt.map(addOne)  //返回一个 []
实践

使用 map 函数,能够不需要解开封装就将方法应用在 Functor 中
而且也可以很容易地自定义一个 functor,比如:

public enum Result<Value> {
    case success(Value)
    case failure(ErrorType)

    func map<T>(@noescape transform: Value throws -> T) rethrows -> Result<T> {
        switch self {
        case let .failure(error):
            return Result<T>.failure(error)
        case let .success(value):
            return try Result<T>.success(transform(value))
        }
    }
}

我们甚至还可以定义一个操作符号 <^> 来进行 map 操作(参考了 github 上的 Rune):

infix operator <^> { }

public func <^><U, T>(@noescape f: U throws -> T, a: U?) rethrows -> T? {
    return try a.map(f)
}

public func <^><S: SequenceType ,T>(
    @noescape f: S.Generator.Element throws -> T, a: S) rethrows -> [T] {
    return try a.map(f)
}

public func <^><U ,T>(@noescape f: U throws -> T, a: Result<U>) rethrows -> Result<T> {
    return a.map(f)
}

刚才的例子,我们就可以这么写,结果是和刚才一样的:

addOne <^> a
addOne <^> b

addOne <^> ints
addOne <^> noInt

2. Monad

任何实现了 flatmap (haskell 中的 liftM)的类型都是 monad
Monad 为封装的值,应用一个返回封装值的函数,也就是说, 传递进去的函数,是一个将封装内的值转化成同样封装值的函数,而所谓的 flatMap 中的 flat(flatten) 就是这里将这个函数返回的封装值解除封装,再放入自身的封装
有时间可以去看看 Monads for functional programming 这个 Philip Wadler 的 paper

optional 的 flatmap

optional 同样实现了 flatMap 所以 optional 是一个 monad

/// Returns `nil` if `self` is `nil`, `f(self!)` otherwise.
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?

我们可以这样对一个 optional flatMap :

let half: Int -> Int? = { $0 % 2 == 1 ? nil : $0 / 2 }
let c = Optional.Some(3)
a.flatMap(half)  //结果为1
b.flatMap(half)  //结果为nil
c.flatMap(half)  //结果为nil
SequenceTypes 中的 flatMap

SequenceTypes 也是一个 Monad ,它的 flagMap 实现有两个:

/// Returns an `Array` containing the non-nil results of mapping
/// `transform` over `self`.
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
///   and *N* is the length of the result.
@warn_unused_result
public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]

这个 flatMap 会自动将 transform 返回的值为 nil 的数给剔除,可以像这样使用:

let singulars = [1, 3, 5, 7]
ints.flatMap(half)  //结果为 1
singulars.flatMap(half)  //结果为 []

SequenceTypes 的另一个 flatMap 是这样的:

/// Returns an `Array` containing the concatenated results of mapping
/// `transform` over `self`.
///
///     s.flatMap(transform)
///
/// is equivalent to
///
///     Array(s.map(transform).flatten())
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
///   and *N* is the length of the result.
@warn_unused_result
public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]
//注意这里的 transform 是没有 @noescape 标记的

这个 flatMap 会将一个生成一个数组的函数的返回数组解开封装,再返回,可以这样使用:

let plusTwoAndThree: Int -> [Int] = { [$0 * 2, $0 * 3] }
singulars.flatMap(plusTwoAndThree)  //返回 [2, 3, 6, 9, 10, 15, 14, 21]
实践

同样在自定义的类中也可以实现 flatMap:

extension Result {
    func flatMap<T>(@noescape transform: Value throws -> Result<T>) rethrows -> Result<T> {
        switch self {
        case let .failure(error):
            return .failure(error)
        case let .success(value):
            return try transform(value)
        }
    }
}

当然,我们也可以定义一个 >>--<< 操作符来使用 flatMap

infix operator  >>- { associativity left }

public func >>- <U,T> (a : U? , @noescape f: U throws -> T?) rethrows -> T? {
    return try a.flatMap(f)
}
public func >>- <S: SequenceType, T>(
    a: S, @noescape f: S.Generator.Element throws -> T?) rethrows -> [T] {
    return try a.flatMap(f)
}
public func >>- <S: SequenceType>(
    a: S, f: S.Generator.Element throws -> S) rethrows -> [S.Generator.Element] {
    return try a.flatMap(f)
}
public func >>- <U ,T>(
    a: Result<U>, @noescape f: U throws -> Result<T>) rethrows -> Result<T> {
    return try a.flatMap(f)
}

infix operator  -<< { associativity right }

public func -<< <U,T> (@noescape f: U throws -> T?, a : U?) rethrows -> T? {
    return try a.flatMap(f)
}
public func -<< <S: SequenceType, T>(
     @noescape f: S.Generator.Element throws -> T?, a: S) rethrows -> [T] {
    return try a.flatMap(f)
}
public func -<< <S: SequenceType>(
    f: S.Generator.Element throws -> S, a: S) rethrows -> [S.Generator.Element] {
    return try a.flatMap(f)
}
public func -<< <U ,T>(
    @noescape f: U throws -> Result<T>, a: Result<U>) rethrows -> Result<T> {
    return try a.flatMap(f)
}

是不是有点眼花缭乱?我们怎么来使用这几个函数呢:

a >>- half
b >>- half
c >>- half

ints >>- half
singulars >>- half

singulars >>- plusTwoAndThree

或者

half -<< a
half -<< b
half -<< c

half -<< ints
half -<< singulars

plusTwoAndThree -<< singulars

两种和上面的都是一样的

其他实践

这里使用包涵卿大神的例子:
比如这里有4个方法

///处理一个网络请求回来的 Data ,返回一个封装了 UIImage 的一个 Result Type
func toImage(data: NSData) -> Result<UIImage>
///给这个图片加上一个 Alpha 值
func addAlpha(image: UIImage) -> Result<UIImage>
///给这个图片切一个圆角
func roundCorner(image: UIImage) -> Result<UIImage>
///加一个模糊效果
func applyBlur(image: UIImage) -> Result<UIImage>

这样的一个 monad 允许链式的编程,能够很容易使用,如下:

toImage(data)
    .flatMap(addAlpha)
    .flatMap(roundCorner)
    .flagMap(applyBlur)

或者运用一下运算符 >>-

toImage(data) >>- addAlpha >>- roundCorner >>- applyBlur

其实还有别的例子,下一篇,我就记录一下 functor 和 monad 的实际使用场景。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容