函数式编程

您所在的位置:网站首页 monad是什么牌子 函数式编程

函数式编程

2024-07-13 19:16| 来源: 网络整理| 查看: 265

前言

初步深入函数式编程是在寒假的时候,搞了一本Haskell的书,啃了没多久就因为我突然的项目任务被搁置了,不过在学习的时候也是各种看不懂,里面的概念略微抽象,再加上当时没有适当地实战敲Demo,导致没过多久脑袋就全空了。庆幸的是,Swift是一门高度兼容函数式编程范式的语言,而我又是一只喜欢敲Swift的程序Dog,在后来我使用Swift编码时,有意识或无意识地套用函数式编程范式的一些概念,也渐渐加深我对函数式编程的理解。这篇文章是我对自己所掌握的函数式编程的一个小总结,主要探讨的是函数式编程中的几个概念: Functor、Applicative、Monad以及它们在Swift中的表现形式。由于本人能力有限,一些概念上的不严谨、编码上的不全面希望大家多包涵,欢迎留下各位宝贵的意见或问题。

本文为纯概念讲述,后期或许会有函数式编程实战的文章推出(我有空写再说吧)

概念 Context

在编码时,我们会遇到各种数据类型,基础的数据类型我们称作值,当然这并不是指编程语言中的基本数据类型,比如说整形1它可以称作一个值,一个结构体struct Person { let name: String; let age: Int }的实例也可以成为一个值,那么何为Context(上下文)呢,我们可以将它理解为对值的一个包装,通过这层包装,我们可以得知值此时所处在的一个状态。在Haskell中,这个包装就是typeclass(类型类),而在Swift中,魔性的enum(枚举)可以充当这个角色,一个例子,就是Swift中的Optional(可选类型),它的定义如下(相关继承或协议关系在这里不标出):

Optional { case none case some(Wrapped)

Optional有两种状态,一种是空状态none,也就是和平时我们传入的nil相等价,一种是存在值的状态,泛型Wrapped指代被包入这层上下文的值的类型。通过这个例子,我们可以很直观地理解Context:描述值在某一阶段的状态。当然,在平时开发中,我们会见到各种Context,比如Either:

enum Either { case left(L) case right(R) }

它代表在某个阶段值可能在left或者right中存在。 在一些函数式响应式编程框架如ReactiveCocoa、RxSwift中,Context无处不在:RACSignal、Observable,甚至是Swift的基本类型Array(数组)它本身也可以看作是一个Context。可见,只要你接触了函数式编程,Context即会接触。

这里,我特别说下这个Context:Result,因为在后面对其他概念以及实战的讲述中我都会以它为基础:

enum Result { case success(T) case failure(MyError) }

Result上下文存在两种状态,一种是成功的状态,当处于这个状态,Result就会持有一个特定类型的值,另外一种状态是失败状态,在这个状态中,你可以获取到一个错误的实例(这个实例可以是你自己拟定的)。这么这个Context有什么用呢?想象一下,你正在进行一项网络操作,获取到的数据是无法确定的,你或许能如你所愿,从服务器中获取到你期望的值,但是也有可能此时服务器发生一些未知的错误,或者网络延时,又或是一些不可抗力的影响,那么,此时你得到的将会是一个错误的表示,如HTTP Code 500...而Result可以在这种情况下引入来表示你在网络操作中获取到的最终结果,是成功还是失败。除了网络请求,诸如数据库操作、数据解析等等,Result都可以引入来进行更明确的标示。

何为Functor、Applicative、Monad?

你可以把Functor、Applicative、Monad想象成Swift中的Protocol(协议),它们可以为某种数据结构的抽象,而这种数据接口正是刚刚我在上面提到的Context,要将某个Context实现成Functor、Applicative、Monad,你必须实现其中特定的函数,所以,要了解什么是Functor、Applicative、Monad,你需要知道它们定义了那些协议函数。接下来我会一一讲解。

Functor

我们对一个值的运算操作使用的是函数,比如我要对一个整形的值进行翻倍操作,我们可以定义一个函数:

func double(_ value: Int) -> Int { return 2 * value }

然后就可以拿这个函数对特定的值进行操作:

let a = 2 let b = double(a)

好,问题来了,如果此时这个值被包在一个Context中呢? 一个函数只能作用于它声明好的特定类型的值,运算整形的函数不能用来运算一个非整形的Context,所以这时,我们引入了Functor。它要做的,就是使一个只能运算值的函数用来运算一个包有这个值类型的Context,最后返回的一个包有运算结果的Context,为此,我们要实现map这个函数(在Haskell中为fmap),它的伪代码是这样的: Context(结果值) = map(Context(初始值), 运算函数)

现在我们拿Result来实现一下:

extension Result { func map(_ mapper: (T) -> O) -> Result { switch self { case .failure(let error): return .failure(error) case .success(let value): return .success(mapper(value)) } } }

我们可以看到,首先我们对Result进行模式匹配,当此时状态是失败的话,我们也直接返回失败,并把错误的实例传递下去,如果状态是成功的,我们就对初始的值进行运算,最后返回包有结果值的成功状态。 为了后面表达式简便,我在这里定义了map的运算符:

precedencegroup ChainingPrecedence { associativity: left higherThan: TernaryPrecedence } // Functor infix operator : ChainingPrecedence // For Result func (lhs: (T) -> O, rhs: Result) -> Result { return rhs.map(lhs) }

我们现在就可以测试一下:

let a: Result = .success(2) let b = double a

在上面我提到,Swift的数组也可以当成是Context,它是作为一个包有多个值的状态存在。想必在日常开发中我们经常也用到了Swift数组中的map函数吧:

let arrA = [1, 2, 3, 4, 5] let arrB = arrA.map(double)

RxSwift中我们也经常使用map:

let ob = Observable.just(1).map(double) Applicative

Applicative其实就是高级的Functor,我们可以调出上面Functor的map伪代码: Context(结果值) = map(Context(初始值), 运算函数) 在函数式编程中,函数也可以作为一个值来看待,若此时这个函数也是被一个Context包裹的,单纯的map是不能接受包裹着函数的Context,所以我们引入了Applicative: Context(结果值) = apply(Context(初始值), Context(运算函数))

我们将Result实现Applicative:

extension Result { func apply(_ mapper: Result O>) -> Result { switch mapper { case .failure(let error): return .failure(error) case .success(let function): return self.map(function) } } } // Applicative infix operator : ChainingPrecedence // For Result func (lhs: Result O>, rhs: Result) -> Result { return rhs.apply(lhs) }

使用:

let function: Result Int> = .success(double) let a: Result = .success(2) let b = function a

Applicative在日常开发中其实用的不多,很多时候我们并不会将一个函数塞进一个Context上,但是如果你用了一些略为高阶的函数时,它强劲的能力就能在此时表现出来,这里举一个略为晦涩的例子,你可以花点时间搞懂它: 这个例子的思路是来自源Swift的函数式JSON解析库Argo的基本用法,若大家有兴趣可以阅读下Argo的源码: thoughtbot/Argo 假设现在我定义了一个函数,它能够接受一个Any的JSON Object,以及一个值在JSON中对应的Key(键)作为参数,返回一个从JSON数据中解析出来的结果,由于这个结果是不确定的,可能JSON中不存在此键对应的值,所以我们用Result来包装它,这个函数的签名为:

func parse(jsonObject: Any, key: String) -> Result

当解析成功时,返回的Result处于成功状态,当解析失败时,返回的Result处于失败状态并携带错误的实体,我们能够通过错误实体得知解析失败的原因。

现在我们有一个结构体,它里面有多个成员,它实现了默认的构造器:

struct Person { let name: String let age: Int let from: String }

我们自己可以编写一套函数柯里化的库,这个库能够对多参数的函数进行柯里化,你也可以从Github中下载: thoughtbot/Curry 比如,我们有一个函数,它的基本签名是: func haha(a: Int, b: Int, c: Int) -> Int,通过函数柯里化我们可以将其转化为(Int) -> (Int) -> (Int) -> Int类型的函数。 我们此时将Person的构造器进行函数柯里化:curry(Person.init),此时我们得到的是类型为(String) -> (Int) -> (String) -> Person的值。 现在奇幻的魔法来了,我定义一个将JSON解析成Person的函数:

func parseJSONToPerson(json: Any) -> Result { return curry(Person.init) parse(jsonObject: json, key: "name") parse(jsonObject: json, key: "age") parse(jsonObject: json, key: "from") }

通过这个函数,我能够将一个JSON数据解析成Person的实例,以一个Result的包装返回,如果解析失败,Result处理失败状态会携带一个错误的实例。

这个函数为什么可以这么写呢,我们来分解一下: 首先通过函数的柯里化我们得到了类型为(String) -> (Int) -> (String) -> Person的值,它也是一个函数,然后经过了map的操作,map的右边是一个解析了name返回的Result,它的类型为Result,map将函数(String) -> (Int) -> (String) -> Person应用于Result,此时我们得到的是返回的结果(Int) -> (String) -> Person的Result包装:Result (String) -> Person>(因为已经消费掉了一个参数),此时,这个函数就被一个Context包裹住了,后面我们不能再用map去将这个函数应用在接下来解析出来的数据了,所以这是我们就借助于Applicative的,接下来看第二个参数,parse函数将JSON解析返回了类型为Result的结果,我们通过将Result (String) -> Person>的函数取出来,应用于Result,就得到了类型为Result Person>的结果。以此类推,最终我们就获取到了经JSON解析后的结果Result。 Applicative强大的能力能够让代码变得如此优雅,这就是函数式编程的魅力之所在。

Monad

Monad中文称为单子,网上看到挺多人被Monad的概念所搞晕,其实它也是基于上面所讲述的概念而来的。对于使用过函数式响应式编程框架(Rx系列[RxSwift、RxJava]、ReactiveCocoa)的人来说,可能不知道Monad是什么,但是在实战中肯定用过,它所要求实现的函数说白了就是flatMap:

let ob = Observable.just(1).flatMap { num in Observable.just("The number is \(num)") }

有很多人喜欢用降维来形容flatMap的能力,但是,它能做的,不止如此。 Monad需要实现的函数我们可以称为bind,在Haskell中它使用符号>>=,在Swift中我们可以定义运算符>>-来表示bind函数,或者直接叫做flatMap。我们先来看看他的伪代码: 首先我们定义一个函数,他的作用是将一个值进行包装,这里标示出这个函数的签名: function :: 值A -> Context(值B)(值A与值B的类型可相同亦可不同) 我们的bind函数就可以这么写了: Context(结果值) = Context(初始值) >>- function 这里我们实现一下Result的Monad:

extension Result { func flatMap(_ mapper: (T) -> Result) -> Result { switch self { case .failure(let error): return .failure(error) case .success(let value): return mapper(value) } } } // Monad infix operator >>- : ChainingPrecedence // For Result func >>-(lhs: Result, rhs: (T) -> Result) -> Result { return lhs.flatMap(rhs) }

Monad的定义很简单,但是Monad究竟能帮我们解决什么问题呢?它要怎么使用呢?别急,通过以下这个例子,你就能对Monad有更深一层的理解: 假设现在我有一系列的操作:

通过特定条件进行本地数据库的查询,找出相关的数据 利用上面从数据库得到的数据作为参数,向服务器发起请求,获取响应数据 将从网络获取到的原始数据转换成JSON数据 将JSON数据进行解析,返回最终解析完成的有特定类型的实体

对以上操作的分析,我们能得知以上每一个操作它的最终结果都具有不确定性,意思就是说我们无法保证操作百分百完成,能成功返回我们想要的数据,所以我们很容易就会想到利用上面已经定义的Context:Reuslt将获取到的结果进行包裹,若获取结果成功,Result将携带结果值处于成功状态,若获取结果失败,Result将携带错误的信息处于失败状态。 现在,我们针对以上每种操作进行函数定义:

// A代表从数据库查找数据的条件的类型 // B代表期望数据库返回结果的类型 func fetchFromDatabase(conditions: A) -> Result { ... } // B类型作为网络请求的参数类型发起网络请求 // 获取到的数据为C类型,可能是原始字符串或者是二进制 func requestNetwork(parameters: B) -> Result { ... } // 将获取到的原始数据类型转换成JSON数据 func dataToJSON(data: C) -> Result { ... } // 将JSON进行解析输出实体 func parse(json: JSON) -> Result { ... }

现在我们假设所有的操作都是在同一条线程中进行的(非UI线程),如果我们只是纯粹地用基本的方法去调用这些函数,我们可能要这么来:

var entityResult: Entity? if case .success(let b) = fetchFromDatabase(conditions: XXX) { if case .success(let c) = requestNetwork(parameters: b) { if case .success(let json) = dataToJSON(data: c) { if case .success(let entity) = parse(json: json) { entityResult = entity } } } }

这代码写起来也好看起来也好真的是一把辛酸泪啊,而且,这里还有一个缺陷,就是我们无法从中获取到错误的信息,如果我们还想要获取到错误的信息,必须再编写多一大串代码了。

此时,Monad出场了:

let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork >>- dataToJSON >>- parse

吓到了吧,只需一行代码,即可将所有要做的事情连串起来了,并且,最终我们获取到的是经Result包装的数据,若在操作的过程中发生错误,错误的信息也记录在里面了。 这就是Monad的威力

当然,我们可以继续对上面的操作进行优化,比如说现在我需要在网络请求的函数中加多一个参数,表示请求的URL,我们可以这样来定义这个网络请求函数:

// B类型作为网络请求的参数类型发起网络请求 // 获取到的数据为C类型,可能是原始字符串或者是二进制 func requestNetwork(urlString: String) -> (B) -> Result { return { parameters in return { ... } } }

调用的时候我们只需要这样调用:

let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork(urlString: "XXX.com/XXX/XXX") >>- dataToJSON >>- parse

这主要是高阶函数的使用技巧。

个人对Monad作用的总结有两部分:

对一系列针对值与Context的操作进行链式结合,代码极其优雅,清晰明了。 将值与Context之间的转换、Context内部进行的操作对外屏蔽,像上面我用原始的方式进行操作,我们需要手动地分析Context的情况,手动地针对不同的Context状态进行相应的操作,而如果我们使用Monad,整一流程下来我们什么都不需要做,坐享其成,取得最终的结果。 总结

Swift是一门高度适配函数式编程范式的语言,你可以在里面到处都能找到函数式编程思想的身影,通过上面对Functor、Appliactive、Monad相关概念的讲述,在巩固我对函数式编程的知识外,希望也能让你对函数式编程的理解有帮助,若文章有概念不严谨的地方或者错误,望见谅,也希望能够向我提出。 谢谢阅读。

参考链接

阮一峰的网络日志 - 图解 Monad



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3