【问题标题】:Generics and Functional programming in SwiftSwift 中的泛型和函数式编程
【发布时间】:2015-03-17 01:41:48
【问题描述】:

下面的 sum 函数的两个变体是我尝试重复 Abelson 和 Sussman 在 Swift 中经典的“计算机程序的结构和解释”一书中介绍的 lisp 版本。第一个版本用于计算一个范围内的整数之和,或一个范围内的整数平方和,第二个版本用于计算 pi/8 的近似值。

我无法将版本组合成一个可以处理所有类型的函数。有没有巧妙的方法来使用泛型或其他 Swift 语言特性来组合变体?

func sum(term: (Int) -> Int, a: Int, next: (Int) -> Int, b: Int) -> Int {
    if a > b {
        return 0
    }
    return (term(a) + sum(term, next(a), next, b))
}

func sum(term: (Int) -> Float, a: Int, next: (Int) -> Int, b: Int) -> Float {
    if a > b {
        return 0
    }
    return (term(a) + sum(term, next(a), next, b))
}

sum({$0}, 1, {$0 + 1}, 3)

结果为 6

sum({$0 * $0}, 3, {$0 + 1}, 4)

结果为 25

8.0 * sum({1.0 / Float(($0 * ($0 + 2)))}, 1, {$0 + 4}, 2500)

结果为 3.14079

【问题讨论】:

  • 结构/枚举怎么样?

标签: swift functional-programming generics


【解决方案1】:

为了让它更简单一点,我稍微更改了方法签名,并假设它足以适用于 func sum_ <T> (term: (T -> T), a: T, next: (T -> T), b: T) -> T {,其中 T 是某种数字。

不幸的是,Swift 中没有 Number 类型,所以我们需要自己制作。我们的类型需要支持

  • 补充
  • 比较
  • 0(加法的中性元素)

功能要求的新协议

比较在Comparable 协议中处理,对于重置我们可以创建自己的协议:

protocol NeutralAdditionElementProvider {
    class func neutralAdditionElement () -> Self
}

protocol Addable {
    func + (lhs: Self, rhs: Self) -> Self
}

sum 实现

我们现在可以实现sum 函数:

func sum <T where T:Addable, T:NeutralAdditionElementProvider, T:Comparable> (term: (T -> T), a: T, next: (T -> T), b: T) -> T {
    if a > b {
        return T.neutralAdditionElement()
    }
    return term(a) + sum(term, next(a), next, b)
}

使IntDouble 符合协议

+ 已经为 DoubleInt 实现,因此协议一致性很容易:

extension Double: Addable {}

extension Int: Addable {}

提供中性元素:

extension Int: NeutralAdditionElementProvider {
    static func neutralAdditionElement() -> Int {
        return 0
    }
}

extension Double: NeutralAdditionElementProvider {
    static func neutralAdditionElement() -> Double {
        return 0.0
    }
}

【讨论】:

  • Sebastian,非常感谢您的回答,这正是我想要的,聪明的。
【解决方案2】:

Sebastian 回答了您的问题 (+1),但可能还有其他利用现有通用函数的简单函数解决方案(例如 reduce,它经常用于类似 sum 的计算)。也许:

let π = [Int](0 ..< 625)
    .map { Double($0 * 4 + 1) }
    .reduce(0.0) { return $0 + (8.0 / ($1 * ($1 + 2.0))) }

或者:

let π = [Int](0 ..< 625).reduce(0.0) {
    let x = Double($1 * 4 + 1)
    return $0 + (8.0 / (x * (x + 2.0)))
}

这些都生成3.14079265371779 的答案,与您的例程相同。


如果您真的想编写自己的通用函数来执行此操作(即您不想使用上面的数组),您可以通过将+ 运算符从@987654327 中取出来简化过程@ 功能。正如塞巴斯蒂安的回答所指出的那样,当您尝试对泛型类型执行加法时,您必须跳过各种麻烦来定义一个协议,该协议指定支持 + 运算符的类型,然后将这些数字类型定义为符合Addable 协议。

虽然这在技术上是正确的,但如果您必须将可能与此 sum 函数结合使用的每个数字类型定义为 Addable,我认为这不再符合泛型编程的精神。我建议你从reduce 书中撕掉一页,让你传递给这个函数的闭包自己做加法。这消除了将泛型定义为Addable 的需要。所以这可能看起来像(其中T 是要计算的总和的类型,U 是正在递增的索引的类型):

func apply<T, U: Comparable>(initial: T, term: (T, U) -> T, a: U, next: (U) -> U, b: U) -> T {
    let value = term(initial, a)
    if a < b {
        return apply(value, term, next(a), next, b)
    } else {
        return value
    }
}

你会这样称呼它:

let π = apply(Double(0.0), { return $0 + 8.0 / Double((Double($1) * Double($1 + 2))) }, 1, { $0 + 4}, 2500)

注意,它不再是sum 函数,因为它实际上只是“从ab 重复执行此闭包”,因此我选择将其重命名为apply

但正如您所见,我们已将添加项移至传递给此函数的闭包中。但它使您摆脱了必须将要传递给“通用”函数的每个数字类型重新定义为 Addable 的愚蠢。

注意,这也解决了 Sebastian 为您解决的另一个问题,即需要为每个数字数据类型实现 neutralAdditionElement。这是对您提供的代码进行字面翻译所必需的(即,如果a &gt; b 则返回零)。但是我已经更改了循环,这样当a &gt; b 时它不会返回零,而是在a == b(或更大)时返回计算出的term 值。

现在,这个新的泛型函数可以用于任何数字类型,而无需实现任何特殊函数或使它们符合任何特殊协议。坦率地说,这里仍有改进的余地(我可能会考虑使用RangeSequenceType 或类似的东西),但它涉及到您最初的问题,即如何使用泛型函数来计算系列的总和评估的表达式。为了使其以真正通用的方式运行,答案可能是“将+ 从通用函数中取出并将其移至闭包”。

【讨论】:

  • 关于Addable 之类的精神不正确的观点非常好。整数的加法和浮点数的加法不是同一个操作,因此试图将这一事实隐藏在通用接口后面会使该接口的用户暴露于未定义的行为。 Swift 是关于避免未定义的行为——因此数字类型之间没有隐式转换等。
  • 很好的答案,罗伯! @rickster:addition 与协议中定义的其他函数有何不同?
  • @Sebastian:这可能值得一个完整的答案......我想我会发布一个。
  • @zrocky 回复:Addable 需要一些隐式转换,我不会那样描述它。这是我的观点:sum 的泛型版本需要为某些参数调用某些函数,并且因为它对泛型参数类型一无所知,所以它使用协议来规定可以在哪些参数上调用哪些函数。您正在比较ab,所以它们必须是Comparablesum 正在调用 + 函数,所以它必须是 Addable。而且你返回零,所以它也必须符合NeutralAdditionElementProvider(例如Zeroable;lol)。
  • 归根结底,Sebastian 向您展示了一种完全按照您的要求进行操作的方法。我试图向您展示如何将这些特定于上下文的函数从sum 中提取出来,从而产生更通用的函数并完全绕过对所有这些新协议的需求。 rickster 就所涉及的一些问题提供了更多背景信息。
【解决方案3】:

Sebastian's answer 有一个不错的方法可以使这个“工作”用于一个简单的测试用例。然而,正如Rob's answer 所指出的,类似Addable 协议的东西并不完全符合函数式编程的精神(或者可能是一般的Swift)。这些解决方案都是可行的,所以这个答案将更多地涉及“精神”问题。

让我们来讲述四个运算符的故事:

func +(lhs: Int, rhs: Int) -> Int
func +(lhs: Int32, rhs: Int32) -> Int32
func +(lhs: Float, rhs: Float) -> Float
func +(lhs: String, rhs: String) -> String // See note below

其中哪些具有相同的行为? “嗯,显然前三个是一样的,”你可能会说,“最后一个是不同的:1 + 1 = 2,不管你是在处理Int还是Int32,当然意思是一样的就像1.0 + 1.0 = 2.0。但是"1" + "1" = "11",所以添加字符串是完全不同的。”

这些假设是不正确的——所有四个运算符都有不同的行为。

当您添加12_147_483_647 时会发生什么? (让我们暂时忽略String 的情况——我想我们都同意字符串连接不同于数字加法。Right, JavaScript?)这取决于这些值的类型,也可能取决于其他因素。

  • 如果两者都是Int...那么,您是为 32 位还是 64 位 CPU 编译的? Int 在 32 位上别名为 Int32,在 64 位上别名为 Int64。刚买了一部闪亮的新 iPhone 6?太好了,你的答案是2_147_483_648
  • 如果两者都是Int32(或者如果您编译为32 位)...好吧,2_147_483_647 是最大可能的有符号32 位整数,因此向其添加1 会产生溢出。在 Swift 中,使用 + 运算符添加会在溢出时陷入陷阱,因此您会崩溃。 Swift 迫使您考虑是否需要溢出行为(通过选择 +&amp;+ 运算符),因此您不需要 blow up your rocket
  • 如果两者都是Float,你会得到2.14748365E+9。但是等等,Float(2_147_483_648) 是什么?这也是2.14748365E+9 — 添加一个没有任何作用! IEEE 32 位浮点格式有 24 位尾数,因此如果将两个相差超过 2^24 的数字相加,则没有足够的精度来存储结果——最低有效数字消失了。这只是您可以使用浮点运算处理的所有wildwooly 问题的开始。

那么这一切的结果是什么?当加法真的是加法时,我们为什么要分叉?

Swift 中的每个操作符都是一个单独的函数,因为每个操作符都有不同的行为——为了更好地说明它,每个操作符都定义了一个关于其行为的特定合同。您知道,当您添加两个 Int32 值时,总和必须可以表示为 Int32 或者您将 trap,并且如果您需要处理溢出情况,您必须采取不同的方式。您知道,当您使用Floats 时,所有算术都会受到精度问题的影响。定义这些类型的合同决定了这些事情。

特别是 Swift,以及一般的强类型函数式 (-ish) 编程语言,往往对类型和与之相关的契约有严格的概念,以及关于何时使用特定类型的严格规则。这里的想法是type safety — 始终了解您正在处理的类型有助于您(和编译器)推理这些类型定义的合约。

当您在 Addable 协议后面统一 + 运算符时,您隐藏了这些类型之间的差异。当然,您可以使用它来制作一些适用于各种类型的简短函数。但是以用模糊类型替换严格定义的类型为代价......并且您使用模糊类型的次数越多,您就越不确定您的代码将在所有情况下如何处理它们。


注意:这个操作符实际上不在 Swift 库中——为了简洁起见,我在这里对其进行了简化。 Strings 的加法实际上是由一个泛型运算符执行的,该运算符适用于实现ExtensibleCollectionType 协议的所有类型,String 就是其中之一。

【讨论】:

  • 请务必将您对塞巴斯蒂安评论的答案发布出来,期待进一步的见解。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多