2014年7月30日水曜日

[Swift] カリー化関数の話(Curried Functions)

最近目にすることの多かったカリー化関数(Curried Functions)について。

カリー化(currying)とは、複数の引数を取る関数について、最終的に引数を一つにするよう部分適用を行うこと。カリー化した関数は元の関数の(多くの場合)最初の引数だけを取り、戻り値は残りの引数を取って結果を返す関数、となるようだ。

言葉の定義はともかくカリー化できると、引数の値は動的だがある文脈では固定であって欲しい、という場合に便利になる。

最初の例としてわかりやすかった ijoshsmith.com の例。を少し改変してみる。
例えば、元の関数が以下のようなケース。
func appendSeparatorToStrings(strings: [String], separator: String) -> String {
    return separator.join(strings)
}

appendSeparatorToStrings([ "A", "B", "C" ], "-") // "A-B-C"
appendSeparatorToStrings([ "D", "E", "F" ], " ") // "D E F"
上記は String 配列を区切り文字で結合した String を返す関数だが、あるシーンで「改行で結合する関数」が必要になった、という想定でカリー化を考える。

カリー化は、戻り値が「結果を返す関数」になるので、シンプルに考えた場合は「ネスト関数(Nested Function)」として実装する。
// ネスト関数として定義 [1]
func appendSeparator(separator: String) -> ([String] -> String) {
    func appendStrings(strings: [String]) -> String {
        return separator.join(strings)
    }
    return appendStrings
}
Swift の場合は、カリー化関数の専用の記法があるらしく次のように書くことができる。
// カリー化記法で定義 [2]
func appendSeparator(separator: String)(strings: [String]) -> String {
    return separator.join(strings)
}
要は、わざわざネスト関数を書かなくても良くなっている。

このカリー化により、ある文脈では改行結合、またある文脈では空白結合、するような関数を手に入れることが出来る。
let appendNewlineToStrings = appendSeparator("\n")
appendNewlineToStrings(strings: [ "A", "B", "C" ]) // "A\nB\nC"

let appendSpaceToStrings = appendSeparator(" ")
appendSpaceToStrings(strings: [ "D", "E", "F" ])   // "D E F"
複数の場所で必要な関数をそれぞれ定義するよりも割が良い、ということなのだろう。

以下のように、似た役割の定義を増やしていくのに比べると、カリー化関数の方が品が良いのがわかる。
func appendNewlineToStrings(strings: [String]) -> String {
    return appendSeparatorToStrings(strings, "\n")
}
func appendSpaceToStrings(strings: [String]) -> String {
    return appendSeparatorToStrings(strings, " ")
}
// ...
これをリファクタリングすると、やはりネスト関数のようになるだろうし、最終的にカリー化の書き方が出来た方が便利だ。

もっとも、この程度の例ではクロージャを使って以下のように書けてしまうので、ネスト関数もカリー化記法も必要ない。
func appendSeparator(separator: String) -> ([String] -> String) {
    return { separator.join($0) }
}
どの書き方でも意味も型も同じだし、どれが分かりやすいかという程度のものだろう。両方の記述を覚えておけば、その時に適切な書き方を選択できるし、知らない記述を前に面食らうことも無い。


ネスト関数(やクロージャ)とカリー化記法の違いとしては、カリー化記法の場合は外部名(External Parameter Name)を省略できない、というのがある(外部名とアンダースコアによる省略については 前の記事 を参照)。試しにカリー化記法 [2] にてアンダースコア(_)による省略を行ったところ、Xcode6-Beta5 時点では定義場所で警告、呼び出し箇所でエラーとなった。
// ネスト関数 ([1]) として定義した場合は、外部名指定は必要ない
appendSeparator("\n")([ "A", "B", "C" ])
appendNewlineToStrings([ "A", "B", "C" ])

// Swift のカリー化記法([2])を使った場合は、外部名が必要になる
appendSeparator("\n")(strings: [ "A", "B", "C" ])
appendNewlineToStrings(strings: [ "A", "B", "C" ])
しかし、以下のように型指定してやることで外部名は必要なくなった(*A)。
// Swift のカリー化記法([2])で定義した appendSeparator
let appendNewlineToStrings: [String] -> String = appendSeparator("\n")
appendNewlineToStrings([ "A", "B", "C" ])  // 外部名が必要なくなった。
これは推測だけど、カリー化記法はネスト関数のシンタックスシュガーで、カリー化記法の場合の第二引数(と言って良いのか?)は自動的にハッシュ(#)修飾されている、ような印象*1。どちらの書き方でも同じ型(関数型)で受け取ることができる。

ちなみに appendSeparator 自体の型は以下のようになる。
let appendSeparatorFunction: String -> [String] -> String = appendSeparator
-> が増えると、どことどこが結合するか分かりづらいが、「ネスト関数」の簡易記法であるという前提で、型が同じであることをふまえると、
let appendSeparatorFunction: String -> ([String] -> String) = appendSeparator
であることがわかる。右からグループ化していけば良い、ということは Apple の The Swift Programming Language: Types に書いてあった。

より冗長に、関数の記法 () -> () に乗っ取って書くとすれば以下のように書けるだろう。
let appendSeparatorFunction: ((String) -> (([String]) -> (String))) = appendSeparator


さらに興味深いのは、インスタンスメソッドのカリー化利用が可能だ、という点。
これは oleb.net で紹介されていた。
class BankAccount {
    var balance: Double = 0.0

    func deposite(amount: Double) {
        balance += amount
    }
}
通常は以下のように使うが、
let account = BankAccount()
account.deposite(100)
以下のようにカリー化した書き方が出来る。
let depositor = BankAccount.deposite
depositor(account)(100)

let accountDepositor = BankAccount.deposite(account)
accountDepositor(100)
型はこんな感じになる。
let depositor: BankAccount -> (Double -> ()) = BankAccount.deposite
Apple 的には T -> U -> R な書き方していたり ole さんは T -> (U) -> (R) だったりで、どれがスタンダードになってくるのか分からないが、自分はなるべく () で結合順序が明示されていた方が分かりやすい。

さらに元の記事ではインスタンスメソッドのカリー化を利用した Target-Action の実装例が載っている。実践的な使い方の例としてかなり有益。


ところで「結合順序」とは言ったものの、「値はすべてタプルである」という話があって、
// すべての値はタプル
let a = 10
println( a.0 )
println( a.0.0 )
dankogai さんの記事(Swiftの関数の引数は、常に一つ)から、関数の引数定義も実はタプルであるのが読み取れる。
func call<A,R>(f: A->R, a: A) -> R {
    return f(a)
}

func add(x: Int, y: Int) -> Int {
    return x + y
}

call(add, (21, 21))
詳しいことは元の記事を見てもらうとして、
言われてみれば確かに「関数の引数定義」は「タプルの名前付き定義」の書き方と同じ。
() は、もしかして「結合順序」と言うより、タプルの境界を定義するものと考えた方が良いのかも、しれない*2

例えば、先に検証したカリー化記法で外部名を外す方法(*A)は、以下のように別名を付けることが出来た。
// Swift のカリー化記法([2])で定義した appendSeparator
let appendNewlineToStrings: ((otherName: [String]) -> (String)) = appendSeparator("\n")
appendNewlineToStrings(otherName: [ "A", "B", "C" ])  // 別の外部名の指定が必須になる
これはなかなか面白い。

最初の外部名を外した例(*A)では、無名タプルで定義したので指定がいらなくなった(ラベルが外された)のに対して、この例ではラベル付きタプルで宣言されたので指定が必須になっている。


今回のカリー化の話は「すべての {} は関数ブロックである」ことや「関数がファーストクラスである」ことなど、一貫性の裏付けとして非常に興味深かった。
「すべての値はタプル」といい、記法がたくさんあって複雑にも見えるが、このあたりの Swift の一貫性は素晴らしいと思った次第*3


参考


...
*1: 正直、このあたりは Apple のドキュメントでは物足りないので、詳しい説明が欲しいところ。良い解説がどこかに無いだろうか?
*2: 気にし過ぎない方が良いかもしれない(結果的に結合順序だし)。追々調査したい。
*3: 最近のモダンな言語ならばどれもそうなのだろうけど。

3 件のコメント:

  1. Great article, thanks for sharing! Do not forget to come back to my blog. Indonesian bloggers greetings :)

    返信削除
  2. thank you for sharing, I will be happy to visit your blog. I hope Andari also visit my blog at hadirkanlah.com a, thank you for sharing!

    返信削除
  3. Thanks for the article interesting, and very nice blog gan ... I hope you also visit my website and read my article also an,
    Bisnis Online

    返信削除