Swift地图之外的世界

在这篇文章中,我将谈论一些我们在Swift中很少听到的东西: 应用函子。 这些使我们可以用最少的代码执行一些非常强大的操作。

这使用的是我上次介绍的HKT(高级类型)框架的扩展和简化版本。 我们看过Swift中的Functors总而言之Functor是“容器”(例如Array或LinkedList之类的序列),它们具有一个称为fmap的超能力映射 ,该映射可转换容器的内容,但使容器本身保持完整。 即使您从LinkedList中的数据开始,常规映射也会始终返回Array。

在本文中,我们将进一步介绍这个想法。 fmap (和标准库中的普通地图 )在某些方面非常有用 ,但肯定会受到限制。 例如,当您想使用具有多个参数的函数来转换容器时会发生什么? 这是我们可以使用应用函子解决的问题。

守则

和以前一样,代码可以在游乐场(包括一些挑战!)和XCode项目中使用,以添加到自己的代码中。

污染数据

通过一个激励性的示例,假设我们正在编写一个Web应用程序来计算退休储蓄。 由于这是公共应用程序,因此我们担心我们可能会传递恶意数据。 处理此问题的一种方法是将所有外部Web数据视为已污染 (就像Ruby一样)。 然后,我们就有一个干净的过程来检查数据并将其标记为安全

在Swift中,让我们使用枚举为WebData建模:

(因为我们正在使用上一篇文章中的HKT框架,所以存在可构造的一致性和TypeParameter

有趣的是,受污染的数据无处不在-任何一个输入被污染的函数都会有一个受污染的输出(或者等效地,只有所有输入都是安全的,函数的输出才是安全的)

因此,假设我们有一个普通函数,该函数接受3个输入并计算一个输出。

接下来,我们从网络上获得此功能的3个输入:

我们要做的是使用这些输入来运行退休金计算; 但返回一个WebData值,该值仅在所有输入都安全的情况下才是安全的

这是一种实现方法,其中包含一个很大的switch语句。 这行得通,但是有很大的缺点:它的功能很长(因为有3个参数,所以要在开关中测试2³= 8个组合); 我必须专门为WebData编写函数(而原始函数仅使用常规的Int,Double等); 我将对我要在应用程序中使用的每个功能重复练习。

但是, WebData是一个可应用的函子 -我们将在稍后看到它的含义-因此,我们可以免费使用HKT框架(或多或少)提供的一些帮助程序功能。

在这种情况下,我们将使用框架中的Appl3 将3个参数的函数应用于3个应用函子的列表。 这样,我只需一行代码即可获得与长switch语句相同的效果, 不必更改我的原始功能。

假设我们现在清除一些数据:

对于那些喜欢“延续风格”的人,您也可以使用它,它允许具有任意数量的参数的功能:

如您所见,这比您自己解包WebData容器要简单得多。 这也是可以用于许多其他类型的技术。 稍后我们将看到一些示例。

但是首先,让我们更详细地解释应用函子。

应用函子

应用函子 (或通常只是“ Applicative ”)是一种容器类型(如Sequence,Array或LinkedList;或WebData-或实际上是Optional),它具有appure几个特殊功能 这些 允许我们:

  • 首先,“浏览”容器列表;
  • 其次,从每个容器中提取数据;
  • 第三,以数据项为参数进行计算。
  • 第四,将结果包装回容器的原始类型。

您可以将其视为功能强大的Functor,它允许带有多个参数的“映射”。

给定包含P类型的应用函子类型P (例如Array,WebData等),这是Applicative的appure的协议² 要求

您可能已经注意到,定义只需要一个自变量的函数(A)→B。 我之前提到的多参数函数呢? 应用函子的强大功能是您不必担心它们-这将在Under The Covers部分中进行说明。

为您免费

为了使任何类型成为应用函子, 您需要做的只是将类型标记为符合Applicative协议,并提供appure函数。

当您这样做时,由于Sourcery的强大功能,您将免费获得很多东西:

  • Appl2Appl3Appl4的定义, 这些定义使您可以将2参数,3参数或4参数函数应用于应用参数列表,并为您进行拆包/重新包装。
  • ApplReduce的定义,它接受一个(+)之类的2值函数,并在一系列应用程序上运行“ reduce”,展开每个结果并将其包装回应用程序中。 我们将在下面看到一个示例。
  • Functor的定义-如果您的类型还不是Functor,它将自动获得Functor(和fmap )定义! (问题:不用看代码,您能找出如何使用 ap pure 为类型构造fmap吗?)
  • 运算符f p的定义,代表“将函数f应用于应用参数p ”(并返回应用)
  • 运算符fp q的定义表示“ fp是包装函数的应用程序,解开函数,将其应用于q并返回应用程序”

最后两个对于以功能上的“延续样式”链接应用程序非常有用,并且实际上是框架在后台运行的方式。 有关更多详细信息,请参见“封面”部分。

  • …此外,Sourcery将为您生成ApplicativeTag (和FunctorTag ),以使机制能够正常工作。 我不在这里讨论这些; 您可以从HKT文章中获得更多详细信息。 幸运的是,使用框架不需要真正了解它们。

让我们尝试为WebData实现它们

在这里,我们将切换可能的情况,并且仅在输入安全 ³时返回安全结果。 这有点像我在本文顶部编写的“ big switch语句”代码,但是请注意,我只查看了4种不同的可能性,而不是8种(为什么?请参阅“掩护之下”部分)

给定这些定义,下面是一个使用ApplReduce通过对整数求和来减少整数WebData数组的示例。 如果任何一个整数被污染,整个结果将是。 显然,这非常强大,并且请注意,我无需做任何“特殊”操作即可让ApplReduceWebData上工作-提供appure即可

各地应用

我们也可以将其他东西变成应用程序! 例如:

选装件

可选还符合应用函子“容器”的定义,其中“包含类型”是可选的(例如,“ Int? ”中的“ Int ”)。 这是appure的定义:

这是我们可能如何使用它们:

您可能想尝试在没有应用函子的情况下实现此功能; 太乱了! (请参阅游乐场代码)

数组

这是appure的定义:

这是我们可能如何使用它们:

这可能有点令人惊讶:在数组上应用函数会返回将函数应用于每个数组的每个元素的所有可能组合 。 这就是Array作为应用函子的“标准”定义。 但是通常您不希望这样:实际上,还有另一种将函数应用于数组列表的方法,这就是ZipArray所做的。

“邮政编码”数组

处理数组的另一种方法是采用每个数组并逐元素地应用函数。 例如,如果您有一个三值函数g和3个数组[a,b],[c,d]和[e,f]:您得到的数组将是[ g (a,c,e), g (b ,d,f)]。 您可能将其称为“压缩”数组; 因此,该框架提供了一个ZipArray类型,该类型可以做到这一点:

我在这里没有显示appure的定义,您可以在示例代码中看到它们。

如您所见,使用Array和ZipArray可以构建一些其他操作很难完成的复杂操作:这要归功于它们都是Applicative Functor。

掩护之下

如果您只想 使用 框架, 则不必阅读本节

我在上面提到过,Applicative Functor可以用于“任意”数量的参数,但是我仅针对2、3和4参数函数显示了Appl2,Appl3和Appl4。 如果我想要5或8参数函数怎么办? 这是可能的,但是我没有将其构建到Applicative代码中,而是想向您展示如何使用“延续样式”自己做到这一点。 这很有趣,因为它从根本上解释了应用函子实际上是如何“起作用”的,即我上面提到的“巧妙窍门”。

实际上,如果您遍历代码,您将看到Appl2…Appl4最终都使用“延续样式”以实现其功能。

例如,Appl2包含以下内容:

而Appl3包含以下内容:

(其中f咖喱函数; pqr是输入的适用函子参数,并提升为Construct类型,例如WebData;而和是自定义运算符)

上面的模式依赖于f是“咖喱函数”。 Currying使我们能够“分解” n个参数的函数,并将其变成每个参数一个的n个 “微型函数”。 一个“迷你功能”的结果馈入下一个“迷你功能”,直到最后一个给出最终结果为止。 所以一个带类型的函数

g: (A,B,C)-> D将被咖喱化为f :(A)->(B)->(C)-> D。

函数curry正是这样做的,这是它查找具有3个参数的函数的方式:

如果我们看一下上面的Appl3定义,您也许可以看到它们如何结合在一起。 (f p)就像只将“ A ”值输入到咖喱函数中一样; 因此它实际上返回类型为(B)->(C)-> D的“迷你函数”。

然后,( … q )部分(其中…表示“ f p的结果”)就像将“ B ”值输入到咖喱函数中一样,从而返回另一个类型为“ C)->D。

最后,( … r )将C值输入到curried函数中,最后返回值D。

因此,如果您想使用具有5参数功能的应用函子,则可以根据需要创建Appl5 。 或将函数的咖喱版本传递给和运算符。 像这样:

进一步阅读

我们在这里进入了一些复杂的想法,MiranLipovača对Learnyouahaskell的出色论述确实为我澄清了这些事情。 Bartosz Milewski和他的《程序员的类别理论》也是一本很好的指南(也向math3ma.com的Tai-Danae Bradley大喊大叫)Brandon Williams和Stephen Celis向我展示了函数式编程如何对Swift开发人员有用,而Matthew Johnson高等类型的游乐场的原始版本为我提供了有关如何实际工作的线索。

下一篇文章: Monad Magic

¹ 在这里,我没有讲清楚为什么我首先要对有污染的数据执行功能,这很危险! 请注意,Ruby限制了受污染数据的某些功能。

² 我实际上有点“作弊”。 适用的(和Functor)实际上只是空协议,它们触发 Sourcery 生成一些样板代码。 如果您的Functor类型不包含’fmap’函数,或者Applicative类型不包含’pure’和’ap’,则Sourcery版本将生成带有注释的错误,指出您需要采取哪些措施来解决此问题。 我会随着时间的推移改善这一点(例如,Swift 4.1具有更好的报告错误的功能)

³您会注意到 pure会 返回一个安全值:可能有些令人惊讶,因为我们想要默认一个新的WebData值来进行污染。 这实际上是“应用函子定律”的结果,我在这里没有真正涉及到。 要了解原因,请尝试将其切换为“污染”并查看失败的原因。