Whythefunc cover

为什么要用函数式编程?

面向对象

这是一段我之前写的代码。[receiver doThis]; 这种代码我们曾经写过很多。这是我们用来观察世界的工具。我们以面向对象的眼光来观察世界,然后我们再向接受者发送消息。有些时候我们发送的消息会带有一些参数。因此这里我们还会加上冒号,有些时候还会加很多冒号。但是这种编程方式我们已经用了很长时间了,因为我们的活动范围很小,只需要将消息发送给接收者即可,这就是面向对象的意义所在。

Objective-C 和函数调用

我们使用方括号和冒号这种有意思的语法,您所要知道的就是这是为我们提供的。你们知道,我们得到了一个大惊喜,我们之所以这样做,是因为这是编程世界的运行方式,但是这并不是真实世界的运行方式。这种语法其实没有什么神奇的地方,我们知道它的底层原理,Objective-C 就是这么做的。

[receiver do: this
        with: that];
        
objc_msgSend()

Objective-C 为我们提供了函数调用功能,也就是这些我们正在谈论的方括号,你必须要向接收者发送一个消息,这仅仅只是为了帮助我们观察世界,但是这与计算机本身完全无关。

这种使用面向对象来组织事物的方式,就是我们查看事物的方式,所以这些对象的内部都有相关的属性、状态,你需要避免与直接去操纵对象的状态,因为这是 C 程序员才做的事。

函数式并不像我们所想的那样简单

所以这里为我们提供了这些入口,这些则是你所要调用的方法,也就是间接改变状态的地方。你将向接收者发送消息。然后你就会发现这些扩展点,然后发送消息给接收者,这就是我们所想象的编程方式。也就是面向对象。

我们认为,嗯,在使用 Squeak 机器人套件中,我们可以组装出机器车,如果我们想要让这辆车移动的话,那么我们就必须给车发送消息,所以或许我们会配备一个方向盘,方向盘会以某种方式向车发送消息。现在,有了 Swift 之后,我们能做的就更多了,现在我们可以用 iPad 来控制机器人。

我们仍然需要将这些消息发送给对象,也就是面向对象的方式,或者以这些机器人为例德华,由于我们不知道它是如何实现的,或许跟程序有关。

在底层部分,我们就在使用了函数式部分,因为别人告诉我们这个功能非常酷炫,如果我们不阅读 Haskell 的相关书籍,或者不阅读 Monad 的相关书籍,我们仍然可以使用方括号和冒号。所以,我们对面向对象非常了解,所以今天我们不需要谈论它,我们对其程序部分相当了解。

函数式并不像我们所想的那样简单,我们之前曾经谈论了下面向对象,因为这是我们看待世界的方式,但是或许函数式才是我们看待世界的真正方式。所以想象一下,当我们学习 Swift、Objective-C 或者 Java 之类的东西时,我们总是从 “Hello, world” 开始的。这似乎是一个不成文的规定。

所以在 Swift 中,我们的 “Hello world” 对于人们来说,其实是非常不自然的。这个函数并未获取任何东西,也没有返回任何东西。我们认为这个概念非常简单,代码也很简单,但是在概念上,它与我们所认为的函数大相径庭。

“Hello, Daniel”

func hello(_ name: String) {
    print("Hello, \(name)!")
}

我们认为函数需要接收某些内容,并且还需要给你反馈一些内容,所以这里就有这样一个函数,这就是所谓的副作用 (side-effect)。所以我们会说,嗯,一旦你明白了它的作用,那么我们这里会添加一个下划线,因为 Swift 让我们这样做,并且也会让调用看起来很舒服,我就可以这样调用:hello("Daniel"),然后它就可以输出 “Hello, Daniel”,但是这仍然有问题,即便我们传递了一个字符串进去,但是它并没有返回任何东西。

这是一个很奇怪的函数,大家都知道,回想一下高中数学。所以当我们想到数学当中的函数时,我们认为函数是一种对元素集进行某种处理,并将其映射为另一个范围的元素集的东西。并且,如果你喜欢的花,那么你可能会想要跟门外那个家伙一样,因为它会更像这样。

去年的时候,我向大家介绍了我在新人培训班的黑板上所写的一段话。大致内容是说:”对于元素集当中的每个元素,在某个范围内一定存在一个独一无二的元素”。这个功能需要获取到元素集当中的某个内容,然后提供某个范围内的东西。

映射

如果你给函数相同的输入,那么它每次都能够给出相同的输出。不能你给函数某个值,然后它给你反馈一个值,然后你再给它这个值,然后它反馈给你另一个值。这就是函数的概念。它将某个东西映射为另一个东西。但是它无法完成这个操作,因为函数的概念很紊乱。

Receive news and updates from Realm straight to your inbox

那么第一件事情是,它是从顶端开始,还是从中间开始?都没关系。两种不同的元素都可以映射,也就是 y = x^2,两个不同的 x 都映射到同一个平方,这没问题。但是这些 x 的值是不能混淆的。而且,你的数学老师可能会在黑板上画了一些类似的玩意儿,并将其称之为函数机 (function machine)。

所以这里我创建了自己的函数机,也就是我自己的函数。它需要一个输入,然后以之产生相应的输出。我不会给控制台输出一些东西,我需要返回一些东西,所以我将会返回一条字符串。这就是一个包含输入输出的完整函数,所以当我将字符串 “Daniel” 放到里面的时候,它就能给我相应的输出,这让我自己感觉好很多,它能够返回给我 “Hello, Daniel”,这让我一整天都很开心。

因此,这就是函数,这才符合我们对其的定义。它不可能什么也不获取,然后什么也不返回。此外函数也不存在任何副作用,这就是我们总结的函数。在数学意义上,函数是我们在高中代数当中学习到的一种数学工具。此外也是没有副作用的函数。

可测试性,可重复性

有个让我们非常害怕的东西是,当我们在编程的时候,很可能会在某个我们没有注意到的地方,修改 (mutate) 或者改变了状态,因此我们不想修改状态,如果我们不修改状态的话,那么这就是可测试的,同时也是可重复的。

让我们回到车的例子上来。我们可能会存在这样一种非函数式的方法,我需要告诉车子向前行进,但是在这个方法中会存在很多问题。其中一个问题是,有些时候我可能会让这辆车成为全局变量,那么我对车子进行控制的时候,我告知车子:”嘿,你可以改变你的位置吗?“

然后我进到这个车辆对象里面,我知道它里面有一个位置属性,但是更为重要的是,我之所以知道这个位置,不是因为我只是简单地将其递增,而是直接对其进行操纵,我觉得这种做法很不好。

这同样也存在问题,如果我重复执行这个函数,那么我此时已经将车辆移动了一段距离,然后又再一次移动一段距离,我此时已经两次给了相同的输入,但是我得到的是不同的输出,因为我的车辆位于不同的地方。

因此这就很难进行测试,所以我们认为,面向对象的重点在于对象。所以我需要创建对象,但是这里是 Swift,所以结构体比类的实例要更好一些,所以我会创建一个结构体,因为我在某个地方读过一本书,建议我这样做。所以现在我有一个车辆对象,然后车辆必须有一个位置属性,然后位置会发生改变,所以它必须是一个 var

希望没有人在代码审查时看到我的代码,然后看到这个 var,但是我们仍然继续,然后修改这个变量,这样我的函数就必须具备修改的功能,这让人感觉很不爽。

可变函数

struct Car {
    let position: Int
    
    func forward(_ amount: Int) -> Car {
        return Car(position:  position + amount)
    }
}

这里有一个变量,然后有这样一个可变函数 (mutating function),但是我仍然给给这个方法发送了一个车辆实例信号,并说:”嘿,我的车子向前移动了这个数目!“这就是这么多年以来,我们在 Objective-C 当中所做的,我们感觉良好,因为不这样的话我们就仍旧使用 CG 的残羹冷炙。

所以我将车辆向前移动了一段距离,但是我仍然具备可测试性。我让车辆向前移动。然后再次向前移动,这两次我都得到了不同的结果。所以我就觉得,嗯,如果我让这个变量变成常亮如何?因此我决定不使用 var,而是使用 let,因此这里也不再使用变化函数,那么如果我返回一个新的车辆实例呢?如果我没有让车子向前移动呢?但是当我告诉车辆:”嗨,向前移动!”,它回应:“好的,但是现在你就只能用这辆车了。“

所以我在这个新的位置创建了一个新的车辆实例。我觉得这个代码很不错,然后我尝试将其教给别人,告诉他们这才是编程世界的真正工作方式,每次你让车辆向前移动,我只会给你一辆新的车。

不知何故,这并不符合我的世界观。所以,这里不应该给我一辆新的汽车,我在想,如果是 CarView 的话会是怎样的呢?

class CarView: UIView {
    private var car: Car
    // some init
    
    override func draw(_ rect: CGRect) {
        // code to draw the car
    }
}

这是对车的一种表示方式,但并不代表着车辆本身,所以我现在创建了一个名为 CarView 的类,这个 CarView 的里面有一个车辆对象。这个车辆知道自己的位置,我所要做的就是将这辆车绘制出来,所以我会将这辆车的位置以及样子画出来,随后我再将这些东西给分开,我感觉这种做法非常好,因为我已经将这个函数式的东西,变为一个非函数式的东西了。 如果我摒弃这个概念,也就是只使用函数式或者面向对象,或者怎么说呢,我现在找到感觉了。我们可以这样做,也就是说:”你真的是一个函数式编程的程序员么?“

所以我们最终以这种组合结束,我知道这看起来很像是对象和属性,但是我想让诸位看到一种全新的不同方式。我想让大家将其看作是在外部的可变 CarView,它使我们不会直接与内部的非可变函数式的内容进行交互。

这是一个很好的想法,因为这里面的东西是可测试的。我拿到一个车辆对象,然后我将其移动,然后我在新的位置那里得到一辆新车。然后再次对这辆车移动不同的距离,然后我将得到不同的车辆对象。如果我移动相同的距离,那么我总是能够得到这些可测试、可重复的结果,也就是我得到的是可变的外部部分,能互相相互的部分,所以我就可以将这些东西组合起来,这看起来和 MVC 很是相似,对吧?

我们的视图控制器之间彼此通信,但是视图控制器会隐藏与视图之间的通信。此外视图控制器还会隐藏与模型之间的通信,因此它只是一种组织我们代码的方式而已。函数式、面向对象也是如此,有些时候问题需要不同的解决方案。

我们可以将这个好看的外壳包裹在中间的这个软软的东西外面,从而生产出一个令人愉悦的糖果。不可变函数,然后在这个函数式核心周围进行变化,这是一个好消息,对吗?为什么 Haskell 需要 Monads 呢?之所以 Haskell 需要 Monads,是因为不存在可变性,也不会有什么会发生变化,所以他们需要引入这个东西,以便我们可以捕获外部状态和内部状态,然后修改事物以及进行交互。我们不需要这个东西,因为我们不是一个纯粹的函数式语言。

我们不需要 Monads

在 Swift 中,我们通过这些回转 (gyration) 来了解这些来自 Haskell 和 Skala 以及其他函数式编程语言的概念,但是我们并不一定需要它们。我们有类,我们还有结构体,我们还有其他与函数式编程所不同的东西,我们可以利用这些不同的东西,只要我们能够清楚地保持两者的分离,知道什么时候该与外界进行交互,什么时候不能。于是我们也就知道何时可以进行分离。

但是也有人说,我们现在也可以用 Monads。可选值就是来自于 Monads 的。但是关键在于我们并没有 Monads,我们不需要它们。它们在 Swift 系统中并不必需。所以 Swift 支持函数式编程范式,它支持这种思考世界的方式,但并不是必需的。Swift 并不是一个纯粹的函数式编程语言。

我们已经拥有了结构体和类,我们也同时拥有了 letvar,我们也有了可变方法和不可变方法,所以关键在于,Swift 只需要选取最优者而行即可。它支持这些函数式的想法,但是我们并不会受到相关的限制。

我们不能因语言的限制而捉襟见肘,当然,我们知道,在 Objective-C 中,我们可以通过在某个地方使用类别 (category) 或者类扩展之类的东西,来将这些本该是私有的东西给暴露出来,所以我们可以不受这些限制,但是想法是,我们应该尽可能将这些东西给分离开来。Monads 并不是必需的,也意味着它可有可无。

在 Swift 中学习函数式编程

我想多谈一谈函数式,以及如何学习函数式编程,特别是在 Swift 的环境中。当我们听到映射一词时,我们听到的是之所以需要映射,是因为循环很丑陋。但是实际上循环其实并不丑。

人们误解的另一件事是,映射并不是循环的替代品。我们认为这只是一部分而已,因为我们所看到过的每个例子都是在数组环境当中进行映射。而在数组的环境中,我们已经在映射中隐藏了对数组的循环操作。

所以这里有一个函数。我在 iTunes 商店中,然后给定一个字符串,表明我需要搜索,然后这里将返回一个字符串,也就是实际的搜索词,因此当我传递一段:食物与厨艺的字符串时,如果您曾经在 iTunes 商店中搜索过这段字符串,那么它就会出现在空格之后的位置。

因此,这里有一个从字符串变换到另一个字符串的函数,映射的思路是,如果我给定一个字符串数组,然后我想要提升 (lift) 这个函数,所以这个字符串之前是让一个字符串变换到另一个字符串,现在的话我就可以给其输入一组字符串,然后它能够返回另一组字符串。

extension Array where Element == String {
    func searchStrings(using f: (Element) -> String) 
                                                  -> [String] {
        var result = [String]()
        for entry in self {
            result.append(f(entry))
        }
        return result
    }
}

因此我们在回顾一下回转的概念,那么在 Swift 3.1 中,你喜欢 where Element == String 么?哦,我的天,我很喜欢这个语法,我们为此等了好几年了!所以这里只用于字符串数组,如果我们有一个字符串数组,那么我就可以提取这两个搜索字符串,也就是这个从一个字符串变换到另一个字符串的函数,将其提升为另一个函数,从而接收一个字符串数组并返回一个字符串数组。我们就完成了所需要做的操作,也就是遍历这个数组的字符串,然后对每个条目进行映射,这也就是映射的功能。

这就是我的字符串搜索方法,所以我可以将任何我想要搜索的东西传递进去。我可以像这样进行传递。我可以使用字符串来搜索另一个字符串,或许由于它是一个尾随闭包 (trailing closure),然后我在 StackOverflow 中查询了具体的用法,所以这里我使用了 $0。但是无论何种情况,当我将它们中的每一个字符串单独映射到数组当中时,我就得到了我想要的结果。

对数组进行映射

当我们对数组进行映射的时候,我们所想的是,我有这样一个能将 A 映射为 B 的函数,然后我想要将其提升为能够将 A 数组映射为 B 数组。然后将 A 数组映射为 B 数组的东西我们称之为 f(x) 映射。所以普遍而言,我认为,f(x) 这个映射可以将 A 的提升——A 数组变化为 B 的提升——B数组。所以我们只要在各种情况下一遍又一遍地重复讲述,那么大家就会完全明白映射的机制。

所以当现在我用这种方式对其进行映射,或者当我使用 $0 进行映射的时候,我就能够再一次获得相同的结果,因为 map 是内置在 Swift 标准库当中的。但是再强调一点,这不意味者循环就很糟糕,所以映射并不会取代循环,所以我想要告诉大家的是,我希望使用映射和可空值 (optional)。

映射与可空值

这里有一个带有可空值的映射。这里有一个 myFaves 数组。同样也有一个 kimFaves 数组,呃,Kim 里面没有任何数值,这是一个空数组,我所要做的是,用 danielFaves 这个数组来获取它的第一个元素,但是当你向任何数组寻求首元素的时候,您可能会得到一个可选值,因为这个数组很可能不存在首元素。大家都知道,由于 kimFaves 里面没有任何值,但是 myFaves 里面却有。所以我会得到一个可空值。

因此如果我说,”从 danielsFaves.first 中获取一个字符串”,这将不起任何作用,因为 String 需要一个实际有值得 String 类型元素,但是这里我们传递的是可空的字符串,所以……如果我们使用了 if let ,那么我们就能够得到正确的结果,因为我们对可空字符串进行了解包,但是如果我们使用了 if let kim,那么将不会得到任何结果,因为这个值是空的,我们并不会对其进行解包,而是跳过 if 代码块。

所以我们想要将这个由一个字符串转换为另一个字符串的函数,提升为由一个可空字符串转换为另一个可空字符串的函数。所以现在我想先暂停一下,和诸位分享一下我的一个想法。假设我们有一个函数,能够将某个类型映射为另一个类型,假设是 String -> Int。然后我还有一个能将 Int 映射为 Double 的函数。

可视化工具

所以想象有这样一个可视化工具,让我可以把这些东西全部显示出来,由于它能够产生一个 Int 对象,并且接收 Int 为参数,我可以将它们组合在一起,从而将函数组合直观地表示出来,并且我还可以像集成电路一样来搭建它,只要让参数按照正确的顺序通过即可。我可以将这玩意儿开放出来,这是一个将 String 映射到 Int 的函数,因此我可以将任何一种类似的函数放到里面。现在我就得到了一条管道 (pipeline),然后匹配到一组 Int 对象,最后通过将这些东西组合在一起,就可以得到一个 String 映射到 Double 的函数,天,看看我接下来还可以做什么!

我可以这样做,嗯,我不希望提供一个单独的字符串,我想要提供一个字符串数组。因此结果应该是一个 Double 类型数组。所以也就是说,我将这个组合封装在映射当中了。我不希望提供一个字符串数组,我希望提供一个可空字符串,接下来我应该就能够得到一个可空的浮点数。所以我将这整个函数组合封装到了映射当中。这就是映射的真正作用,它帮助我将某种类型变换到另一种类型。

可空值

extension Optional where Wrapped == String {
    func searchString(using f:(Wrapped) -> String) 
                                            -> String? {
        switch self {
        case .none:
            return nil
        case .some(let value):
            return .some(f(value))
        }
    }
}

因此现在有了映射和可空值之后,就像我们对数组进行扩展一样,我们对可空值进行扩展,现在我让这个从字符串映射为另一个字符串的函数,也就是这里这个我传入的函数,我现在将让其生成一个将可空字符串转换为另一个可空字符串的函数,就像之前我们对数组所做的那样。

如果大家不记得的话,那么我可以再做一遍。在这种情况下,我会对 self 进行 switch 操作,如果你曾经和函数式程序员聊过的话,他们会告诉你“自然而然”。这实在是太糟糕了。但是,假设说,如果我给你一个 nil 值,那么你需要给我返回一个 nil。这里就没有什么可做的。但是如果我给的是不为空的东西的话,那么就需要给我返回不为空的数据了。

这个函数可以将字符串映射为另一个祖父穿,所以如果我给你一个可空字符串,并且这个可空字符串的内容为 nil 的话,那么就需要给我返回 nil。如果不是的话,那么就执行映射。所以,你需要用上这个 .Some 的东西,然后将其绑定到我所给定的值上,然后你会计算这个值,并将其封装起来,从而成为一个可空值,所以这当然是一个自然而然的事情。

也就是说,如果要获取 danielsFaves 的第一个元素的话,那么就会得到一个可空值,随后我们对这个可空值调用 searchString,借助这个函数,我就可以获取到 ”food” 和 ”cooking” 这两个元素,由于我们进行了解包,因此我会将解包操作隐藏起来。我将 if let 给隐藏了起来。然后我还以相同的方式将 switch 给隐藏了起来,所以 map 实际上并不等同于循环,而是隐含了将一个状态变换为另一个状态。而 kimFaves 里面是没有任何元素的,所以我尝试对其执行这个函数,很好,得到的结果是 .None,所以它将给我返回 nil。所以映射同可选值的结合,与映射同数组的结合非常类似。

我把这个从 A 转换为 B 的函数进行提升,成为从可空值 A 转换为可空值 B 的函数,同理,也可以转换为从 A 的映射转换为 B 的映射的函数。好吧?因此我可以如此这般,而不是去直接使用映射,就像我们之前在数组那边所做的那样,当我执行这段函数,我将能够得到 “food” 和 “cooking” 这两个元素,然后对 kimFaves 做同样的操作,我将得到 nil

所以,总而言之,我有一个将 A 转换为 B 的函数,然后借助这个 f(x) 的映射函数,我可以将其变为将 A 的映射转换为 B 的映射的函数。只要你信心十足,人们就会相信你知道自己在说什么。

实现属于你自己的映射

如果要深刻理解这些概念的话,那么就需要自行来实现映射的功能。当你碰到这样一种情况,你可能会想到:“哦,有什么能帮我解决这个问题吗?就是你了,映射!”所以,我会给大家简要展示一个例子,大家可以尝试进行练习,但是这种练习非常值得,因为 Swift 标准库并没有为大家提供这个玩意儿,也就是 Result 类型。


enum Result<Value, Error> {
    case success(Value)
    case failure(Error)
}

在 Swift 中,Result 类型出现的时间比 Error 要早,现在我们仍然有很多人在使用它,Result 类型的这两个参数皆为泛型。它需要一个 Value 类型或者 Error 类型,然后如果成功的话,那么就会将结果值封装在 .success 枚举值当中返回出来;如果失败的话,那么我就会将错误信息封装在 .failure 枚举值中返回。 这非常类似于可空值,其中 .failure 对应于 .none.success 对应于 .some。因此,对吧!如果我编写一个能将 Result 映射为另一个 Result 的函数,那么我就能得到 ResultA.error 映射为 ResultB.error 的玩意儿。 这两者均为 Error 类型,就像之前两者均为 nil 类型一样,但是不同之处在于,我要将 A 到 B 的映射,提升为 ResultA.errorResultB.error 的映射。 让我们对 Result 进行一下扩展。由于 ResultValueError 均为泛型,所以我的这个 A 到 B 的函数,将变成 ValueTargetValue 的函数,最后我就可以得到有 TargetValueError 类型的 Result 对象。

extension Result {
    func map<TargetValue>(_ f: (Value) -> TargetValue) 
                                -> Result<TargetValue, Error> {
        switch self {
        case .failure(let error):
            return .failure(error)
        case .success(let value):
            return .success(f(value))
        }
    }
}

接下来我要做的事情就和可选值那边一样了,我将对 self 执行 switch 操作,如果是 .failure,那么我就会映射出 Error 信息,就像我们之前映射出 nil 一样。如果不是的话,那么我就要绑定这个值,并用这个值来运算 f(x),然后这就是我所得到的结果。之后我就快速略过了,因为我想快点结束这个小练习。

映射的问题在于,大家总是会将其看作是一个容器 (container),它包含有某种类型的元素。这就和我们所说的 A 类型数组、可空值 A、包含值的 A Result 类型一样。

这并不是容器

我想举一个不是容器的例子。有一种映射,是将 Double 类型转换为其他类型。于是我创建了一个从 Double 转换为 String 的函数,一个从 Double 转换为 URL 的函数,以及一个从 Double 转换为 Any 的函数。所以无论函数的目标是什么,都直接使用泛型即可。所以这里我们要建立一个从 Double 转换为某种泛型的函数。

下面是我所创建的一个示例。这个函数具备一到四次方的乘积功能,从而生成一个 Double 数组。我传递一个 x 进去,随后它会返回xx⁴。也就是说,它会返回一个含有四个元素的数组。 所以我们如果传递了 5.2 这个 Double 数值的话,它就会返回 5.2 的一次方、二次方、三次方和四次方,也就是我们得到的结果值。为什么我们要这样做呢?实际上我们并不会这么做。大家只需要跟随我的思路。让我们再看另外一个例子。

这一次我只打算输出 description,所以我们需要有一个 Double 转换为 String 的函数,它简单地返回 x.description,所以说 5.2 这个 Double 数值的字符串表述就是 5.2 这个 String 类型。

现在,我们为这个类型来编写映射。函数是从 Double 类型转换为其他类型。由于它不属于容器,所以我需要执行相同的提升操作。因此这里就是映射的实际样子。

我会将这个 A 转换为 B 的函数传递进去,就像我们之前在其他映射当中所做的那样。其他映射都会接收这个 A 转换为 B 的函数。请记住,由于我是以 Double 到 A 的函数开始的,并且我想要以 Double 到 B 的函数来结束。因此我需要将它们组合起来。我要做的就是用这个 Double 到 A 的函数,通过 f(x),然后通过组合来给我相应的映射。

所以我的结果将会是这个 Double 到 B 的函数,我所做的方式就是用 Double 到 B 的 f(x) 组合。如果我把 self 给删除掉的话,这段代码就会更为清晰。好的,因此,我需要 A 到 B 的映射,所以这里我们得到了一个 Double 数组到 String 的映射。我们将所有的 Double 加在一起,然后将值打印出来。

Double 映射

那么 Double 类型的映射是什么样的呢?这里有一个从 Double 转换为 Double 数组的函数。此外还有转换为 String 的函数。那么 String 类型的映射又是什么样的呢?这里有一个 DoubleString 的函数。

所以我把这个 f(x) 映射从 Double 数组到 String,提升为这样一个可怕的东西。也就是这个函数,它将 DoubleDouble 数组的函数,转换为 DoubleString 的函数。 因此我就得到这个 A 的映射,也就是 DoubleDouble 数组的函数,此外还有这个 f(x),它接收 DoubleString 的函数为参数。不过我也很担心大家会有这个想法:”啊,我们听了你 25 分钟的讲演,我们现在仍然不知道你在说什么。”

关键在于,人们在函数式编程中通常讨论的东西就是映射,一旦你开始自行编写类似的代码,就发现映射其实并不是那么神秘。此外还有一种方法就是将这部分函数式核心隐藏在可变函数内部,并赋予其灵活的外部接口。因此,这就是为什么要用函数式编程的理由。

问答时间到!

**问:** 您提过,映射是函数式函数的一部分。那么我们还有其他东西,比如说规约 (reduce) 之类。那么同样也适用这个原则么?或者您有别的建议?

**答:** 嗯,简而言之,如果你使用了某种结构,比如说数组、可空值之类支持映射功能的东西,那么本质上我们将其称之为函数式结构 (functer)。因此映射实际上有些特别。此外还有一个我不是很想提及的事情,但是您又问出来了,就是说假设我们有类似 flatMap 之类的东西,我们本质上就是在使用 Monad,所以这个范畴比人们想象得要大很多。

为了方便起见,我们也有规约、筛选 (filter) 以及其他函数式的功能。这些功能的关键在于,它们都不会修改您传递进来的东西,它只是让您将东西传递进去,然后另一端将变化后的东西返回给你,但是之前传递的东西并未发生改变,所以对于数组而言,sortsorted 之间是存在差异的。如果你要对数组进行排序的话,你是否要修改它呢?还是通过另一端的结果,给我返回一个数组呢?

所以我希望你借助函数式的理由在于,可以将不会发生变化的代码分离出来。Gary Bernhardt 在他的讲演中谈到,将修改状态的代码分离出来。所以如果你这样做的话,你就会发现你的代码将更容易维护,我们的 iOS 代码对此非常适应。我知道,人们总是在争论:“这不是 MVC,这是 MVVM!”但是其实并不是。我们只是将修改状态的代码和不修改状态的代码分隔开来。我们只是用了不同的方法来匹配它们。谢谢。

About the content

This talk was delivered live in June 2017 at AltConf. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Daniel Steinberg

Daniel is the author of the books ‘A Swift Kickstart’ and ‘Developing iOS 7 Apps for iPad and iPhone’, the official companion book to the popular iTunes U series from Stanford University. Daniel presents iPhone, Cocoa, and Swift training and consults through his company Dim Sum Thinking. He is also the host of the CocoaConf Podcast.

4 design patterns for a RESTless mobile integration »

close