在Swift上使用函数式编程

在本文中,我将尝试以最简单的方式说明如何尊重函数式编程的构建块,该模型经过数小时的研究,发现它并未得到充分利用。

Swift是一个功能对象混合体,因此它为我们提供了应用功能编程的支持,无论是类型推断还是灵活的功能管理。


快速回顾一下范式:

  • 函数是一个转换,它接受一个(或几个)参数并返回一个值。
  • 函数也是范例的一等公民 ,这意味着它是一个值,可以将其作为参数传递给另一个函数,获取结果甚至存储它。
  • 以Currying为基础,我们可以给函数提供比期望少的参数,因此它将给我们带来期望缺少参数的函数。 此概念称为部分应用程序
  • 我们也可以像在数学世界中一样在彼此之间编写函数,只要第一个函数的输出与第二个函数的输入匹配即可。
  • 我们还可以将函数作为其他函数的参数传递。 将其他函数作为参数接收的函数称为高阶函数
  • 如果没有支持不可变性的上下文,这一切都是不可能的,这意味着我们的函数不会更改我们的数据,而是会给我们提供副本,这是将函数应用于原始数据的结果。 每次我们调用具有特定值的函数时,预期结果都是相同的。

这些概念是范式的基础,将它们组合在一起可以赋予我们极大的灵活性,更高的代码可重用性百分比,并使我们可以构建更具声明性的组件。


现在,我们如何在Swift这样的混合语言中应用这些概念,并专注于功能的一部分?

职能和一等公民

在任何事情之前,我们定义“添加”功能:

  func add(number:Int,otherNumber:Int)-> Int { 
返回号码+其他号码
}

它的类型是:

 添加::(Int,Int)-> Int 

范式允许我们在恒定的情况下存储此函数:

 让storedAdd:(Int,Int)-> Int =添加 

或者也可以像这样直接定义:

 让加= {(num(num:Int,otherNum:Int)in num + otherNum} 

返回值是隐式的,类型是自动推断的。

这两个声明是等效的,因此它的使用取决于首选项和上下文。


定义函数后,假设我们要将5加到几个数字上:

 让foo = add(5,1)let bar = add(5,5)let bla = add(5,100) 

您在这里看到问题了吗? 在每种情况下,我们都重复相同的操作,即增加5。如果我们想再执行10次该怎么办? 样板代码。 如果我们要进行更改怎么办? 如果不是要加5,而要加20,该怎么办?复制粘贴不是一种选择。 这就是功能范式发挥作用的地方。

部分应用和咖喱

要部分应用某个功能,首先需要对其进行处理。

被咖喱意味着什么? 这意味着可以将其表示为一系列函数,这些函数采用单个参数并返回需要下一个参数的函数。

现在,在Swift中,默认情况下没有机制可以执行此操作,但是我们可以构建它。

怎么样?

通过执行Currying的定义,这非常容易:使函数接受一个参数,然后返回另一个等待下一个参数的函数。

回到我们的函数,让我们重新定义它以支持此定义:

 让添加= { 
(a:Int)在{
(b:整数)在a + b中
}
}

现在,函数的类型为:

 添加:: Int->(Int-> Int) 

这是一个将Int作为参数并返回另一个函数的函数,该函数接收一个Int作为参数并返回另一个Int。

这是魔术吗? 没有。

括号只是表明我们实现了想要的目标的一种方式。 我们获得了add函数的咖喱版本!

但是现在,是否总是有必要以这种方式定义我们的函数以便对其进行处理?

答案是否定的,我们可以定义一个通用的高阶函数来为我们完成这项工作。

高阶

高阶函数仅是接收另一个函数作为参数的函数。 在这种情况下,我们要归纳currying:

  func curry (_ f:@转义(A,B)-> C)->(A)->(B)->(C){ 
返回{a in {b in f(a,b)}}
}

@escaping批注允许保留函数“ f”以在以后执行。

该类型很明显,它的作用方式与前面的示例相同,但处于通用级别:现在,我们可以简化任何接收两个类型为AB的参数并返回值为C的函数

请注意,我们得到一个带有一个参数的函数,然后返回另一个带有一个参数的函数,依此类推。

现在,应用我们的归一化函数,我们得到:

  let curriedAdd =咖喱(添加) 

这两个函数的类型是:

 添加::(Int,Int)-> Int 
curriedAdd ::整数->整数->整数

我们可以应用以下功能:

  let a = add(1,2)// 3let b = curriedAdd(1)(2)// 3 

最后,我们可以部分应用此添加:

 让plusFive = curriedAdd(5) 
plusFive :: Int-> Int

所以:

  let foo'= plusFive(1)//结果:6let bar'= plusFive(5)//结果:10let bla'= plusFive(100)//结果:105 

通过一些简单的函数逻辑,我们消除了重复的代码。

组成

就像我们之前说过的,只要功能合适 ,就可以组成功能。

如果我们定义一个函数,使我们得到两个整数的乘积:

 让乘积= {(a:Int,b:Int)在* b}中 

我们想要得到一个数字的两倍,任何数字,我们已经看到我们需要咖喱部分申请:

 让doubleOf =咖喱(产品)(2) 
doubleOf :: Int-> Int

现在我们想要数字双精度的下一个。 按照传统方式,我们将执行以下操作:

 让nextFromDoubleTraditionally = {((a:Int)in 
plusOne(doubleOf(a))
}

由于括号和块语法,这很烦人。 这可以通过使用合成来解决。

一个定义,在操作员格式中不要乱打一遍,以便更好地阅读:

 中缀运算符>>:AdditionPrecedencefunc >> (_ f:@转义(T)-> U,_ g:@转义(U)-> V)->(T)-> V {返回{g(f($ 0)}}} 

因此,整个原始问题简化为:

 让nextFromDouble = doubleOf >> plusOne 

魔法。

为什么这样做? 看类型:

  doubleOf :: Int-> Int 
plusOne ::整数->整数

返回值类型为“ doubleOf ”与输入值类型为“ plusOne ”。 因此,当我们将Int值传递给此组合函数时,它就很合适。

现在,我们有了期望值的函数,并且独立于其在内部执行的操作而返回另一个值。

让我们走得更远。

想象一下,我们想使用’ doubleOf ‘获得一个数字的四倍。

 让quadOf = doubleOf >> doubleOf 

一个简单的例子,但是它显示了在构建新函数时如何以非常自然的方式重用我们的函数,就像堆积砖一样。

它还有助于读取,并易于查看数据通过转换所经过的路径。


注意:将来的帖子中将提供从OOP到FP的整个重构过程。

自从我们开始使用这种范例以来,在Codika中,我们提高了声明性,这在我们需要解释代码的作用时很有帮助。

我们也不会因重用大量代码而浪费时间。

我邀请您阅读有关此范例的更多信息,以及如何在您的项目中使用它。 这是一条没有回报的道路。


注意:我们在此处创建的composition函数从左到右读取,就像一连串的方法。 但是为了获得更好的阅读效果并使它类似于 Haskell ,我们可以使构图从右到左读取。 这是一个例子


Codika 是一家创新的软件开发公司,旨在激励其成员进行研究并分享他们的改进。 我们希望鼓励所有公司创新并提高社会生活质量。