Generics and Functional programming in Swift

橙三吉。 提交于 2019-12-19 09:10:04

问题


The two variants of the sum function below are my attempt to repeat the lisp version introduced by Abelson and Sussman in the classic "Structure and interpretation of Computer Programs" book in Swift. The first version was used to compute the sum of integers in a range, or the sum of squares of integers in a range, and the second version was used to compute the approximation to pi/8.

I was not able to combine to the versions into a single func which will handle all types. Is there a clever way to use generics or some other Swift language feature to combine the variants?

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))
}

with

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

6 as result

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

25 as result

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

3.14079 as result


回答1:


To make it a bit easier I have changed the method signatures slightly and assume that it is good enough to work for func sum_ <T> (term: (T -> T), a: T, next: (T -> T), b: T) -> T {, where T is some kind of number.

Unfortunately there is no Number type in Swift, so we need to make our own. Our type needs to support

  • addition
  • comparison
  • 0 (the neutral element for addition)

New Protocols for the requirements of the function

Comparison is handled in the Comparable protocol, for the reset we can create our own protocols:

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

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

sum implementation

We can now implement the sum function:

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)
}

Making Int and Double conform to the protocols

+ is already implemented for Double and Int so protocol conformance is easy:

extension Double: Addable {}

extension Int: Addable {}

Providing a neutral element:

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

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



回答2:


Sebastian answers your question (+1), but there might be other simple functional solutions that leverage existing generic functions (such as reduce, which is frequently used for sum-like calculations). Perhaps:

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

Or:

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

These both generate an answer of 3.14079265371779, same as your routine.


If you really want to write your own generic function that does this (i.e. you don't want to use an array, such as above), you can simplify the process by getting the + operator out of the sum function. As Sebastian's answer points out, when you try to perform addition on a generic type, you have to jump through all sorts of hoops to define a protocol that specifies that the types that support + operator and then define these numeric types as conforming to that Addable protocol.

While that's technically correct, if you have to define each numeric type that you might use in conjunction with this sum function as being Addable, I'd contend that this is no longer in the spirit of generic programming. I'd suggest you rip a page out of the reduce book, and let the closure that you pass to this function do the addition itself. That eliminates the need to need to define your generics as being Addable. So that might look like (where T is the type for the sum that will be calculated and U is the type for index that is being incremented):

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
    }
}

And you'd call that like so:

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

Note, it's no longer a sum function as it's really just a "repeat executing this closure from a to b", hence my choice to rename this to apply.

But as you can see, we've moved the addition into the closure we pass to this function. But it gets you out of the silliness of having to redefine every numeric type you want to pass to the "generic" function as being Addable.

Note, this also addresses another problem that Sebastian solved for you, namely the need to implement neutralAdditionElement for every numeric data type, too. That was necessary to perform a literal translation of the code you provided (i.e. to return zero if a > b). But I've changed the loop such that rather than returning zero when a > b, it returns the calculated term value if a == b (or greater).

Now, this new generic function can be used with any numeric types, without needing to implement any special functions or make them conform to any special protocols. Frankly, there's still room for improvement here (I'd probably considering using a Range or SequenceType or something like that), but it gets to the your original question of how to use generic functions to calculate the sum of a series of evaluated expressions. To make it behave in a truly generic manner, the answer is probably "get the + out of the generic function and move it to the closure".




回答3:


Sebastian's answer has a decent way to make this "work" for a simple test case. However, as Rob's answer notes, something like an Addable protocol isn't quite in the spirit of functional programming (or of Swift in general, perhaps). Those solutions are both workable, so this answer will get more into the "spirit" issue.

Let's tell a tale of four operators:

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

Which of these have the same behavior? "Well, obviously the first three are the same," you might say, "and the last one is different: 1 + 1 = 2 regardless of whether you're dealing with Int or Int32, and that of course means the same thing as 1.0 + 1.0 = 2.0. But "1" + "1" = "11", so adding strings is completely different."

These assumptions are incorrect — all four operators have different behavior.

What happens when you add 1 and 2_147_483_647? (Let's ignore the String case for now — I think we can all agree that string concatenation is different from numeric addition. Right, JavaScript?) It depends on what type those values are, and possibly on other things, too.

  • If both are Int... well, are you compiled for a 32-bit or 64-bit CPU? Int is aliased to Int32 on 32 bit and to Int64 on 64 bit. Just got a shiny new iPhone 6? Great, your answer is 2_147_483_648.
  • If both are Int32 (or if you're compiled for 32-bit)... well, 2_147_483_647 is the largest possible signed 32-bit integer, so adding one to it produces an overflow. In Swift, adding with the + operator traps on overflow, so you crash. Swift forces you to think about whether you want overflow behavior (by choosing the + or &+ operator) so you don't blow up your rocket.
  • If both are Float, you get 2.14748365E+9. But wait, what's Float(2_147_483_648)? That's also 2.14748365E+9 — adding one did nothing! The IEEE 32-bit float format has 24 bits of mantissa, so if you add two numbers that differ by more than 2^24, there's not enough precision to store the result — the least significant digits vanish. And that's just the beginning of all the wild and wooly issues you can get into with floating-point arithmetic.

So what's the upshot of all this? Why are we splitting hairs over when addition is really addition?

Each operator in Swift is a separate function because each has different behavior — to put a finer point on it, each defines a specific contract regarding its behavior. You know that when you add two Int32 values, the sum must be representable as an Int32 or you'll trap, and that you have to do things differently if you need to handle overflow cases. You know that when you work with Floats, all arithmetic is subject to precision issues. The contracts defining those types determine these things.

Swift specifically, and strongly-typed functional(-ish) programming languages in general, tend to have a strict notion of types and the contracts that go with them, and strict rules about when you're using specific types. The idea here is type safety — always knowing what types you're dealing with helps you (and the compiler) reason about the contracts those types define.

When you unify + operators behind an Addable protocol, you're hiding the differences between those types. Sure, you can use that to make some short functions that work on all sorts of types. But at the cost of replacing strictly defined types with a fuzzy type... and the more you use fuzzy types, the less sure you can be about what your code will do with them in all cases.


Note: this operator isn't actually in the Swift library — I've simplified it here for brevity. Addition of Strings is actually performed by a generic operator that applies to all types implementing the ExtensibleCollectionType protocol, of which String is one.



来源:https://stackoverflow.com/questions/29089966/generics-and-functional-programming-in-swift

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!