Protocol extensions and the death of the pipe-forward operator

So a while back I wrote about using some functional programming techniques in Swift to solve the Luhn credit card checksum problem.

One of the suggestions in the article was defining and using |> to avoid the annoying tennis-match bouncing left and right between free functions and methods.

Since 2.0 came out, I’ve been thinking I really needed to update some of my articles to the new syntax, and started on this one. Except as protocol extensions are the new hotness, using them would probably be the more “Swift-y” way of writing this. So here’s a solution using new 2.0 features.

As a reminder, here’s the requirements, in the form of wikipedia’s description of the algorithm:

  1. From the rightmost digit, which is the check digit, moving left, double the value of every second digit; if the product of this doubling operation is greater than 9 (e.g., 8 × 2 = 16), then sum the digits of the products (e.g., 16: 1 + 6 = 7, 18: 1 + 8 = 9).
  2. Take the sum of all the digits.
  3. If the total modulo 10 is equal to 0 (if the total ends in zero) then the number is valid according to the Luhn formula; else it is not valid.

So, here in turn are each of the components, all written as extensions.

First, converting characters to integers. Similar to String.toInteger becoming Int.init(String), here’s an extension of Int:

extension Int {
    init?(c: Character) {
        guard let i = Int(String(c)) else { return nil }

        self = i
    }
}

Here I’m using guard to check the value is convertible and return nil if not. Yes, I know, this is hardly different from an if let/else, but I like the use of guard whenever I want to handle a failure case like this “up front”.

It would be nice to make this a protocol extension rather than an extension of Int. All the std lib integer types support a from-string initializer, after all. But this would involve creating a protocol like IntegerStringConvertible and then extending all the integers to conform to it, since such a common grouping doesn’t exist.

The previous post defined mapSome, which takes a transformation function that returns an optional, and returns an array of only those values that weren’t transformed into nil:

extension SequenceType {
    func mapSome<U>(transform: Generator.Element -> U?) -> [U] {
        var result: [U] = []
        for case let x? in lazy(self).map(transform) {
            result.append(x)
        }
        return result
    }
}

(the for case let x? is using the new pattern-matching syntax that lets you for over only the non-nil values in a sequence)

But as of 2.0, flatMap has been enhanced to do this exact thing. So we no longer need to define it.

We’re going to use this along with the character-to-integer initializer to extract only the numbers from the credit-card string, like so, using the new 2.0b2 feature of using an init as a function:

":123-456:".characters.flatMap(Int.init)
// returns [1,2,3,4,5,6]

(note, we have to do this on the .characters view now, since String is no longer itself a sequence).

Next, the a function that let you apply a transformation only to every n th value in a sequence:

extension SequenceType {
    func mapEveryNth(n: Int, transform: Generator.Element -> Generator.Element)
        -> [Generator.Element]  {

            // enumerate starts from zero, so for this to work with the nth element,
            // and not the 0th, n+1th etc, we need to add 1 to the ifIndex check:
            let isNth = { ($0 + 1) % n == 0 }

            return enumerate().map { i, x in
                isNth(i) ? transform(x) : x
            }
    }
}

Then, a sum on any sequence that contains integers:

extension SequenceType where Generator.Element: IntegerType {
    func sum() -> Generator.Element {
        return reduce(0, combine: +)
    }
}

and an extension on any integer to check if it’s a multiple of another:

extension IntegerType {
    func isMultipleOf(of: Self) -> Bool {
        return self % of == 0
    }
}

And finally, to put them all together into a single extension to String that validates the checksum:

extension String {
    func luhnchecksum() -> Bool {
        return characters
            .flatMap(Int.init)
            .reverse()
            .mapEveryNth(2) { $0 < 5 ? $0*2 : $0*2 - 9 }
            .sum()
            .isMultipleOf(10)
    }
}

let ccnum = "4012 8888 8888 1881"
print( ccnum.luhnchecksum() ? "👍" : "👎" )

This now looks very similar to the version that used the |> operator – but without having to define any custom operator at all. Which feels like a win to me.

Yes, you could have done all this before using extensions of concrete objects in 1.2. But most of these extensions are more general than that – for example, mapSome can work on the string character view, but also arrays, dictionaries, sets.

Anyway, now back to playing with beta 2…

4 thoughts on “Protocol extensions and the death of the pipe-forward operator

  1. Are there any reason to use the custom sum instead of directly .reduce(0, combine: +)? I mean as we have the closure in the mapEveryNth exposed directly in the luhnchecksum method, I don’t see any advantage to extract the reduce step to its own method. What I’m missing?

    • I think it’s a question of how re-useable `sum` is. Summing a sequence is not specific to the problem – there are lots of times when you’d find being able to do it useful. Since that’s the case, may as well write an extension for it and make the code more readable. But “doubling and then collapsing to a single digit” is very specific to the Luhn algorithm so not worth polluting every sequence with.

Leave a reply to airspeedvelocity Cancel reply