どうも、あんま元気ではないですが、まぁ何とか生きてる @tobi462 です。
いや、こんな出だしで大丈夫か?って感じもしますが、大丈夫だ。問題ない。ということで久しぶりに技術記事を書いてみます。
Tl;Dr
- ローカル関数は、その関数内の変数をキャプチャする。
- 関数の引数をそのまま引き継いだ『部分適用された関数』を作りたいときなどに便利。
func foo(x: Int, y: Int) { func _bar(z: Int) { bar(x: x, y: y, z: z) // Capture `x` and `y` } _bar(z: 10) // => 13 _bar(z: 20) // => 23 _bar(z: 30) // => 33 } func bar(x: Int, y: Int, z: Int) { print(x + y + z) } foo(x: 1, y: 2)
ローカル関数とは?
Swift ではローカル関数、すなわち関数の中でネストされた関数を宣言することができます。
func foo() { func local(x: Int) { // Nested function (a.k.a local-function) print(x) // => 42 } local(x: 42) }
ローカル関数は、定義された関数内にスコープが限定されるため、他からは利用されない局所的な関数を作成するのに利用できます。
またクロージャと同様、ローカル関数はそのスコープの変数をキャプチャ(束縛)します。以下の例ではローカル変数である y
をキャプチャすることで、明示的に引数で渡すこと無くローカル関数内で y
を参照しています。
func foo() { var y = 10 func local(x: Int) { // This function capture `y` print(x + y) // => 52 } local(x: 42) }
引数を継承する関数呼び出し
ところで、関数に渡された引数をそのまま継承した関数を定義したいケースがあります。文章にすると分かりづらいですが、すなわち以下のようなコードです。
func foo(x: Int, y: Int) { bar(x: x, y: y, z: 10) // => 13 bar(x: x, y: y, z: 20) // => 23 bar(x: x, y: y, z: 30) // => 33 } func bar(x: Int, y: Int, z: Int) { print(x + y + z) } foo(x: 1, y: 2)
この例では、foo()
に渡された引数である x
と y
をそのまま関数 bar()
に引き渡していますが、最後の引数である z
だけが変化しています。
このコード例は非常にシンプルですが、やや重複している印象を受けます。何より 引数 x
と y
を固定する意図がある にも関わらず、それをコード上で表現できていないため、読み手にとっては「異なるケースもあるのではないか?」と予想して読まなければならず、つまるところリーダブルではありません。
転送用のローカル関数を定義する
先ほどのコード例は、 転送用のローカル関数を定義することで改善できます。
func foo(x: Int, y: Int) { func _bar(z: Int) { bar(x: x, y: y, z: z) // Capture `x` and `y` } _bar(z: 10) // => 13 _bar(z: 20) // => 23 _bar(z: 30) // => 33 } func bar(x: Int, y: Int, z: Int) { print(x + y + z) } foo(x: 1, y: 2)
ローカル関数 _bar()
が引数 x
と y
をキャプチャしているため、その後の呼び出しコードは非常にシンプルで明確なものになっています。
もし関数 bar
が他から参照されないのであれば、bar()
自体をローカル関数にしてしまっても良いでしょう。
func foo(x: Int, y: Int) { func _bar(z: Int) { print(x + y + z) // Capture `x` and `y` } _bar(z: 10) // => 13 _bar(z: 20) // => 23 _bar(z: 30) // => 33 } foo(x: 1, y: 2)
部分適用された関数(Partial Application Function)
ここからは余談ですが、引数がキャプチャされた転送用のローカル関数は、部分適用(partial application)された関数であるとみなせるかもしれません。
Swift の関数は標準でカリー化されていませんが、自前でカリー化することも可能で、例えば thoughtbot/Curry といった OSS を利用することで次のように記述できます。
func foo(x: Int, y: Int) { let _bar = curry(bar)(x)(y) // Create partial application function _bar(10) _bar(20) _bar(30) } func bar(x: Int, y: Int, z: Int) { print(x + y + z) } foo(x: 1, y: 2)
この例では、関数 bar()
の第一引数と第二引数に x
と y
を与えた部分関数 _bar
を作成して利用しています。これは先ほどのローカル関数とほぼ同様のことが実現できています。
しかし、一方でローカル関数に比べて以下のようなデメリットも存在します。
- カリー化すると引数ラベルが無効化されてしまう。
- 引数は順番にしか適用できない。
言い換えると、ローカル関数には上記のようなデメリットが存在せず、また一般的な Swift コードと同様に読むことが出来るので、カリー化や部分適用などの関数型言語の機能に慣れていなくても利用できるという利点があります。
まとめ
というわけで、今回は Swift のローカル関数を利用して、部分適用された関数を作成する方法について記事にしてみました。
「こんなコードを書きたいケースはない」と思う方もいるかもしれませんが、例えば SwiftPrettyPrint でもローカル関数を利用している箇所があり、覚えておくと役立つケースもあるのではないかと思います。
func string<T: Any>(_ target: T, debug: Bool) -> String { func _string(_ target: Any) -> String { string(target, debug: debug) } ...
とはいえ、ローカル関数は通常の関数よりも利用頻度は圧倒的に低いはずなので無理に多用すべきではないでしょう。また通常の関数と区別が付きやすいように、先頭に _
や __
をつけるなどのコーディング規約を導入しても良いかもしれません。