Swiftでアプリカティブスタイル

  • 117
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

Swift を書いていると Optional を多用しますが、次のように Optional に包まれた値同士を計算したいときに面倒です。

Swift
let a: Int? = 2
let b: Int? = 3

// a + b をしたい(ただし、 a か b が nil なら nil を得たい)

ぱっと思い付くのは次のような方法です。

方法1: 面倒すぎる

Swift
let r1: Int? = {
    if let a0 = a {
        if let b0 = b {
            return a0 + b0
        }
    }
    return nil
}()

方法2: nil が入ってたら即死

Swift
let r2: Int? = a! + b!

方法3: 結果が Int?? になってしまう(二重に Optional に包まれる)

Swift
let r3 = a.map { a0 in b.map { b0 in a0 + b0 } }

方法4: もはや何のために Optional を使ってんだか

Swift
let r4: Int? = a == nil ? nil : b == nil ? nil : a! + b!

アプリカティブスタイル

Haskell なら アプリカティブスタイル で次のように書けます。

Haskell
(+) <$> a <*> b

これを Swift でやってみましょう。

まずは、 <*> 演算子を実装します。 Haskell の Maybe<*> と同じですね。

Swift
infix operator <*> { associativity left }

func <*><T, U>(lhs: (T -> U)?, rhs: T?) -> U? {
    switch lhs {
    case .None:
        return nil
    case .Some(let transform):
        return rhs.map(transform)
    }
}

さて、問題は <$> です。 Swift の関数はカリー化されていないのでどうすれば良いでしょうか。

とりあえず、 2 引数の関数に特化して作ってみましょう。なお、 Swift では $ の文字を演算子に含むことができないので、ここでは <$> の代わりに <^> としています。

Swift
infix operator <^> { associativity left }

func <^><T, U, R>(lhs: (T, U) -> R, rhs: T?) -> (U -> R)? {
    return rhs.map { t in { u in lhs(t, u) } }
}

2 引数関数に部分適用し、 1 引数関数に変換しているところがポイントです。

早速これらを使ってアプリカティブスタイルで計算してみましょう! Swift の演算子は関数なので(例えば、 2 + 3(+)(2, 3) と書けます)、 + を適用してみます。 (+) となっているのは、 + を演算子ではなく関数として認識させるためです。

Swift
let a: Int? = 2
let b: Int? = 3

let r5 = (+) <^> a <*> b
println(r5) // Optional(5)

なんと! Haskell のアプリカティブスタイルそのままに計算できました!!

しかし、今は <^> を 2 引数関数限定で実装しているので、このままでは 3 引数以上の関数に適用できません。 Swift の関数はカリー化されていないので仕方ありません。 4 引数か 5 引数くらいまで <^> を実装しておけば実用上問題ないのではないでしょうか。

Swift
// 3 引数関数用
func <^><T, U, V, R>(lhs: (T, U, V) -> R, rhs: T?) -> (U -> V -> R)? {
    return rhs.map { t in { u in { v in lhs(t, u, v) } } }
}
// 4 引数関数用
func <^><T, U, V, W ,R>(lhs: (T, U, V, W) -> R, rhs: T?) -> (U -> V -> W -> R)? {
    return rhs.map { t in { u in { v in { w in lhs(t, u, v, w) } } } }
}

func add3(a: Int, b: Int, c: Int) -> Int {
    return a + b + c
}
func add4(a: Int, b: Int, c: Int, d: Int) -> Int {
    return a + b + c + d
}

println(add3 <^> a <*> b <*> c)       // Optional(10)
println(add4 <^> a <*> b <*> c <*> d) // Optional(17)

Swift は Optional Chaining も書きやすいし、アプリカティブスタイルもあれば Optional がいい感じに使えそうです。

<^>を一つにまとめる

それにしても、 <^> をいくつもオーバーロードするのは不細工です。

より汎用的なカリー化のための関数を用意して、 <^> の実装は一つで済ませましょう。

Swift
func curry<T, U, R>(f: (T, U) -> R) -> T -> U -> R {
    return { t in { u in f(t, u) } }
}
func curry<T, U, V, R>(f: (T, U, V) -> R) -> T -> U -> V -> R {
    return { t in { u in { v in f(t, u, v) } } }
}
func curry<T, U, V, W, R>(f: (T, U, V, W) -> R) -> T -> U -> V -> W -> R {
    return { t in { u in { v in { w in f(t, u, v, w) } } } }
}

func <^><T, R>(lhs: T -> R, rhs: T?) -> R? {
    return rhs.map { lhs($0) }
}

let c: Int? = 5
let d: Int? = 7

println(curry(+) <^> a <*> b)                // Optional(5)
println(curry(add3) <^> a <*> b <*> c)       // Optional(10)
println(curry(add4) <^> a <*> b <*> c <*> d) // Optional(17)

curry がはさまる分だけ面倒ですが、カリー化は <^> に限らず使えるより汎用的な処理なのでこちらの方が良さそうです。これなら Haskell の

Haskell
pure (+) <*> a <*> b

も次のように再現できます。

Swift
.Some(curry(+)) <*> a <*> b

curry さえ N 引数分実装しておけば、 Swift でもっと関数型言語っぽいことができて便利そうですね。

ライブラリ化

せっかくなので 15 引数まで対応して ApplicativeSwift というライブラリにしてみました。 Array についても実装したので次のようなこともできます。

println((*) <^> [1, 2] <*> [3, 4]) // [3, 4, 6, 8]
println([curry(+), curry(*)] <*> [1, 2] <*> [3, 4]) // [4, 5, 5, 6, 3, 4, 6, 8]

以上、「すごいHaskell」を読んでてアプリカティブスタイルが便利そうだったのでやってみました。

追記 (2015.08.06)

Swift で Functional Programming をするには次のライブラリが便利です。