Swift是实现函数式思想的一门好语言,这里简要讲述一下函数式中透镜的Swift实现
举个例子
首先我们先创建一个结构体
1 | struct Point { |
一般情况下这时候会报错Cannot assign to property: 'x' is a 'let' constant
,因为x
是一个常量,所以无法修改
那么有时候我们如果需要改变一个不可变的属性的值,那么一般我们会选择再创建一个对象去覆盖
1 | struct Point { |
但是这样子做未免显得有点麻烦,假如你觉得这样子还能接受,那么我再举个例子
1 | struct Point { |
假如这时候我们希望把square.line.start.x
设置为20,那么按照常规的写法就显得很麻烦了,更何况实际开发中可能还存在着更深的嵌套
方案
我们可以使用函数式的方案去解决上面的问题,首先我们先把问题简化,先考虑如何把Point
的x
属性优雅地去进行更改
基本原理
首先我们定义一个函数签名,取名为Lens
(透镜)
1 | typealias Lens<Subpart,Whole> = (@escaping (Subpart) -> Subpart) -> (Whole) -> Whole |
其中Subpart
表示要修改的属性的值,Whole
表示被修改的这个对象
整个定义的含义是
- 传入闭包表达式,这个闭包表达式会带入参数,参数值为被修改的属性的当前值,比如
x
的当前值1,然后闭包表达式修改这个值之后再次返回出去。 - 返回一个对象的闭包,这个闭包会带入参数,参数值为当前被修改的对象,闭包表达式返回一个新的对象,比如
Point
为了方便构建一个透镜,我们再添加一个方法用于初始化一个透镜
1 | func lens<Subpart,Whole>(view: @escaping (Whole) -> Subpart, set: @escaping (Subpart,Whole) -> Whole) -> Lens<Subpart,Whole> { |
这个方法传入两个参数
- 闭包表达式
view
表示传入一个对象,返回这个对象要被修改的那个属性当前值,比如我们上面提到的x
,相当于一个get
操作 - 闭包表达式
set
表示传入要被修改属性的新值和要被修改的对象,然后返回包含新值的属性的新对象,相当于一个set
操作
最后把闭包表达式返回出去就是我们所要的透镜,也可以理解为一个修改器。这个修改器内定好了哪个属性要被修改成哪个值,然后这个修改器最后返回一个闭包表达式,传入一个旧对象,返回一个带有新值的新对象
如果觉得上面的缩减写法有点晦涩,那我们展开来描述
1 | return { mapper in // mapper是用于修改属性值的一个闭包表达式 |
使用
接下来我们就可以用上面定义好的方法来构件我们的x
属性透镜和y
属性透镜
1 | extension Point { |
改进
但是这样子的使用方法还是不够方便,所以我们定义多两个方法封装一下
1 | func over<Subpart,Whole>(mapper: @escaping (Subpart) -> Subpart, lens: Lens<Subpart,Whole>) -> (Whole) -> Whole { |
over
方法传入一个闭包表达式和一个透镜,闭包表达式参数是用来修改属性值,跟透镜结合使用,返回新对象
set
方法传入一个新值和一个透镜,新值是用于构建修改属性的闭包,跟传入的透镜结合,调用over
方法
有了这两个方法,我们就可以把之前的写法进行修改
1 | let xLens = set(value: 10, lens: Point.xL) // 等价于下面的写法 |
这么一看貌似只是简单地把闭包表达式变成了函数调用,也就是花括号变成了括号而已
但实际上变成了函数调用的方式之后我们就可以做更多的事情了
比如说:
1 | infix operator %~ |
这时候我们的调用方式就变成了
1 | let xLens = Point.xL .~ 10 |
还不够,再加一个定义,我们把对象的修改方式给改了
1 | precedencegroup LensPrecedence { |
这样子当我们想要修改一个点的x值和y值的时候,就可以这么写了
1 | var point = Point(x: 1, y: 1) |
看起来就简洁许多
复杂的情况
那么刚才我们提及的复杂的情况,除了点之外,还出现了线和面,那应该怎么做呢
同理可得,我们给线和面也添加透镜
1 | extension Line { |
新增一个运算符<<<
表示左结合,把右参数(闭包表达式)作为左参数(闭包表达式)的参数,并修改一下运算符之间的优先级
1 | precedencegroup CombinePrecedence { |
最终调用方式如下
1 | /// 线段的起始点设置为20,原点的y值设置为30 |
看起来也是十分简洁
完整代码
仅供参考
1 | import UIKit |
拓展
在这种思想的基础上,我们就可以使用keyPath特性来对我们的编码进行一些改进
首先我们对keyPath进行拓展,也就是\.xxx
这种写法
1 | extension WritableKeyPath { |
修改一下我们先前定义的两个运算符的实现,设置左值为keyPath的键,右值为新值或者生成新值的闭包表达式
1 | infix operator %~ : LensPrecedence |
最终效果如下
1 | let formatter = DateFormatter() |