https://www.youtube.com/watch?v=_IY-5iq_cCE
今回からは A Swift Tour
の新しい項目「関数とクロージャー」について眺めていきます。現代のプログラミングではお馴染みの関数と、Swift で活躍する場面の多いクロージャー。それらの初歩的なところからじっくりおさらいしていきましょう。どうぞよろしくお願いしますね。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #41
00:00 開始 00:52 条件付きの型エイリアス 04:57 関数の概要 08:04 引数リスト 09:34 練習問題 16:57 独自の引数ラベル 21:19 関数シグネチャー 27:10 複数の値を戻り値で返す 33:00 分割代入 34:58 inout による戻り値 38:42 複数の値にアクセス 41:07 関数を入れ子にする 50:40 入れ子で定義した関数を戻り値として使う 51:10 関数を入れ子にする場面 53:38 クロージング —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #41
では、始めていきますね。今日は関数とクロージャーのお話に入る予定ですが、その前に、前回の勉強会のアーカイブを見ていたら、何気なく流してしまった知らなかったことがいくつかあったので、その辺りを補足しようと思います。
前回の勉強会では、繰り返し処理の話、とりわけ「範囲」の話をしていました。この中で「CountableRange」が「typealias」になっているという話をみんなで確認していました。それについて、改めて見直してみると、当たり前のように流してしまった部分がありました。特に、typealias
にwhere
節が使えるという点についてです。このtypealias
は、自分でも知っていたのか忘れていたのか分かりませんでしたが、紹介しておこうと思います。
昔、typealias
というと、自分の名前で例えば Distance = Int
のようにして、Int
型を Distance
というキーワードで使えるようにするのが基本的なtypealias
の使い方でしたよね。その後、例えば ItemList = Array<T>
のようにしてジェネリクスを指定することもできるようになりましたが、どれも最初の頃の基本的なtypealias
の使い方でした。
これが、ある時からジェネリクスにも対応して、例えば typealias ItemList<T>
として、T
と指定することができるようになりました。そして where T: StringProtocol
のように条件を付けることができるようになったんです。T
が StringProtocol
に準拠していれば let a
や let b
として使え、例えば Substring
も使えるという動きになります。しかし、StringProtocol
に準拠していないものではエラーになります。
このような書き方ができるというのをアーカイブを見て改めて気づいたので、紹介しておきます。これ、当たり前ですかね。アソシエイティブタイプのような感じで表現力が高まっていると感じます。このような表現の幅が広がるのは面白いですね。
では、余談はこれくらいにしまして、続いて関数とクロージャーのお話に入っていこうと思います。これは、Swiftの基本的な部分なので、特に難しい話は出てきませんが、見どころが多い分野でもありますので、ゆっくりと見ていきましょう。
まず関数の定義についてお話します。関数は func
というキーワードで定義します。引数名とその型、そして戻り値の型を指定する必要があります。引数リストはカッコ内にカンマ区切りで記載し、戻り値の型は矢印(->
)で指定します。関数を呼び出す時には、関数名に続けてカッコで引数リストを渡します。
例えば次のように書きます:
func exampleFunction(param1: String, param2: Int) -> String {
// 関数の本体
}
これは func
というキーワードから始まり、関数名、カッコ内に引数リストを記載します。引数リストは必要に応じてカンマで区切り、その後に戻り値の型を矢印で指定します。そして関数を呼び出す時には、関数名の後にカッコで渡したい値を指定する形です。
特に難しい概念ではなく、他のプログラミング言語とも比較してそれほど違いはありません。戻り値の表現が若干異なるかもしれませんが、それ以外はシンプルなものです。
大切なポイントとしては、引数リストの定義の部分です。どの名前でどの型の引数を受け取るかを記載し、関数を使う時にはその型に対応した値を渡してあげるということです。特に初めての人にとっても、他の言語とあまり変わらないので、恐れずに触れてみてください。
以上が関数の基本的な定義と使い方です。次に進む前に何か質問があればどうぞ。 そういう雰囲気で、コンパイル時には型が重要で、ランタイムではインスタンスになるという感じでしょうかね。このあたりは余談として、少し頭の片隅に入れておくと役に立つかもしれません。そこで紹介しておきました。
さて、ここで久しぶりの練習問題です。先ほどのコードの下にそのまま書いてありますが、このコードから引数を削除してみましょう。そして、新たに引数を追加して「本日のランチスペシャル」を挨拶文に含めてみましょう、という問題が出ています。これは関数を少しでも使ったことがある人にとっては、それほど難しい問題ではないように感じるかもしれません。この練習問題を通じて、自分で引数を定義できるようになることが目的でしょう。
これから実際にやってみようと思いますが、ここでちょっとスライドを作っている際に「ランチスペシャルって何だろう?」と個人的に気になったので軽く調べてみました。私の中では「スペシャルランチ」という言葉は聞いたことがあるけど、「ランチスペシャル」は初めて聞いたので、辞書などで調べてみました。どうやらどっちも似たような意味ですが、「スペシャルランチ」よりも「ランチスペシャル」の方が「当店自慢の料理」みたいな雰囲気になるという解説がありました。
この情報を踏まえた上で、練習問題を解いていこうと思います。先ほどのコードをコピーして、新しいページに貼り付けてみます。実行してみると、挨拶文が表示されるはずですが、動かないですね。先ほどまで動いていたのに動かない。別のプレイグラウンドページを作ると動かなくなる可能性があるようです。再現実験してみますが、とりあえずiOSに変えて進めましょう。
まず、day
引数を削除するので以下のようになります。削除したので関連する変数も削除しておきましょう。そしてAPIからday
がなくなったので、それも削除します。これで第1問目は完了です。
続いて第2問、本日のランチスペシャルを追加してみましょう。today's lunch special
という英語表記にします。「本日の」が抜けるので、today's lunch special
とします。以下のように引数として渡して挨拶文に含めます。
func greet(lunchSpecial: String) -> String {
return "Hello! Today's lunch special is \\(lunchSpecial)."
}
練習問題はこれで終わりですね。関数の基本としてはこれでOKでしょう。実行してみると動きました。プレイグラウンドページを使うと時々動かないのかもしれません。また、「ランチ」の綴りを間違っていると指摘があったので修正しました。
こんな感じで練習問題は完了です。次に進みましょう。次は独自の引数ラベルのお話です。 Swiftでは、関数の引数名をラベルとして使用するよう規定されています。また、引数名に独自のラベルを追加したり、アンダースコアを用いてラベル名を省略することも可能です。たとえば、以下のコードでは Person
を省略し、 Day
の代わりに On
を使用しています。
func greet(person: String, on day: String) {
print("Hello \\(person), today is \\(day)")
}
この場合、関数を呼び出すときには次のようになります。
greet("Alice", on: "Tuesday")
ラベル名を外で使う際に、自然な表現にするために変更することができます。内部引数名を使用することで、関数内部では元の引数名を使い続けることができるため、コードの可読性が向上します。さらに、Swiftでは関数シグネチャー(関数の署名)も引数ラベルを含めて認識されるため、同じ名前の関数を異なるラベルでオーバーロードすることが可能です。
例えば、次のコードでは greet
関数を異なるラベルでオーバーロードしています。
func greet(person: String) {
print("Hello \\(person)")
}
func greet(_ person: String, on day: String) {
print("Hello \\(person), today is \\(day)")
}
Swiftの特徴として、引数ラベルの有無を含めて関数を区別できるため、オーバーロードが可能です。これにより、同じ関数名でも異なる引数ラベルを持つ複数の関数を宣言できます。
さらに、タプルを用いて関数を呼び出す例を考えます。以下のように values
というタプルの配列があったとします。
let values = [("Bob", "Tuesday"), ("Alice", "Wednesday")]
これに対して map
を使用して greet
関数を適用する場合、以下のように書きます。
values.map { greet($0, on: $1) }
このコードは values
の各要素に対して greet
関数を適用し、それぞれの person
と day
を引数として渡します。
このように、Swiftでは関数の引数ラベルを柔軟に扱うことができ、外部引数名と内部引数名を使い分けることで、コードの可読性や再利用性を高めることができます。 オーバーロードしていなければ、こういう書き方も可能になっています。オーバーロードしていなければというか、違うラベルのオーバーロードがなければ、こういう書き方ができます。この問題にぶつかるのは、公開関数のような機能を使っていくときですね。特に、文字列に変換したい場合に String
のイニシャライザがたくさんあるため、どれを適用するかを決定する際にこの考え方が使えるという感じです。
オーバーロードすると、たとえば16行目でどの greet
関数を使えばいいのか分からなくなるため、コンパイルエラーが発生します。そのため、明確に指定する必要があります。また、変数に関数を代入するときも同様に、どの greet
を使用するのかを具体的に指定できます。
このように関数の署名が扱われていることは少し難しい話ですが、徐々に理解していけば問題ありません。関数はこのように扱われているということを理解しておけば、引数ラベルの周りはOKです。
次に進みましょう。次は、複数の値を戻り値で返す方法についてです。これは、以前のこの勉強会でも話した気がしますが、改めて見ていきましょう。重要なポイントは、タプルを使うことで複数の値を返すことができるということです。
実際にコードを見ていった方が分かりやすいかもしれませんね。プレイグラウンドを使ってみましょう。たとえば、このようなコードがあります。
以下のようにしてみましょう:
func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
var min = scores[0]
var max = scores[0]
var sum = 0
for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let result = calculateStatistics(scores: [5, 3, 8, 7])
print("Min: \\(result.min), Max: \\(result.max), Sum: \\(result.sum)")
こういった関数があります。私は改行を多用する方が分かりやすいので、このように改行を入れます。見るべきポイントは、ここが引数リストで、矢印 ->
によって引数リストと戻り値が隔てられていることです。右側が戻り値の型になっています。この場合はタプルで (min: Int, max: Int, sum: Int)
となっています。
慣れてくると、この形が自然に見えるようになります。この関数の場合、最小値、最大値、合計値を計算し、それらをタプルで返しています。受け取る側では、そのタプルから3つの値を取り出して処理ができます。
例えば次のように使用します:
let statistics = calculateStatistics(scores: [5, 3, 8, 7])
print("Minimum: \\(statistics.min)")
print("Maximum: \\(statistics.max)")
print("Sum: \\(statistics.sum)")
これにより、最小値が3、最大値が8、合計が23と求めることができます。実際にプレイグラウンドで確認すると、この結果も正しそうです。
このようにして、タプルを使って複数の値を返す方法について学びました。他にもさまざまな方法があるかもしれませんが、タプルは特に便利です。 こういうふうに、結果が要は3つ得られるので、これで結果を返すとき、例えば結果を表示したいときには、「ミニマムバリュー」でresult.min
、「マキシマムバリュー」でresult.max
、「トータル」でresult.sum
のように、3つのものを表示してあげることができます。
このあたりは、タプルの話を以前にもしたことがありますが、一個のタプルとして受け取り、それぞれに名前を指定する方法の一つです。昔ながらの方法では、それぞれを分けて保存しておきたいときに、それぞれ変数分解することがありましたが、タプルだともっと便利に書けます。
わざわざ3つに分けて使う必要はなく、一気に3つの値を代入することができます。これを動かしてみると、普通に動きます。例えば、以下のようになります。
let (min, max, sum) = calculateStatistics()
こういうふうに書いてあげると、3つの値が返ってくることが実感できると思います。これがマルチプルリターンタイプの例です。
このように、タプルを使うことで、関数の戻り値を複数にすることができます。C言語のようなプログラミング言語では、戻り値は1つだけという発想が一般的ですが、Swiftではタプルのおかげで複数の戻り値を返すことができます。
ちなみに、C言語的な発想を持っていると、次のような感じになってしまいます。
func calculateStatistics(_ nums: [Int], min: inout Int, max: inout Int) -> Int {
min = nums.min() ?? 0
max = nums.max() ?? 0
return nums.reduce(0, +)
}
この方法だと、呼び出す側で変数を用意しておかないといけません。
var min = 0
var max = 0
let sum = calculateStatistics(nums, min: &min, max: &max)
これでは冗長で、初期化も必要です。こういう時にタプルを使うと、以下のようにシンプルに書けます。
func calculateStatistics(_ nums: [Int]) -> (min: Int, max: Int, sum: Int) {
let min = nums.min() ?? 0
let max = nums.max() ?? 0
let sum = nums.reduce(0, +)
return (min, max, sum)
}
let result = calculateStatistics(nums)
print("Min: \\(result.min), Max: \\(result.max), Sum: \\(result.sum)")
このように、戻り値に対してラベルを付けることで、よりわかりやすく、すっきりとしたコードになります。複数の値を返す場合でも、インデックス(例えばresult.0のような)でアクセスすることもできます。
要するに、Swiftのマルチプルリターンタイプの概念は非常に重要で便利なものです。inout
についても、まだまだ面白いところがありますが、それは追って触れることにしましょう。
次の話題に移ります。
複数の値にアクセスするということができるとのことです。先ほどのタプルの話ですね。タプルで複数の値が返ってきた場合、その値から任意の値を抽出できるのです。具体的には、ラベル名または0から始まるインデックスで取り出すことができます。
これがポイントです。タプルを使って、複数の戻り値を受け取ったとき、ラベルで書くか、あるいは順番通りにインデックスでアクセスするか、どちらでも可能です。 もうちょっとせっかくだから聞きたいという場合は、コメントなり声でなり好きな方で突っ込んでくださいね。結構長いので、この勉強会でいつタプルの話が出てくるかなどわからない点もあるので、聞きたいときにはサクッと聞いちゃってください。
では、関数の難しいところへ話が移ります。関数の入れ子についてのお話です。関数を入れ子にできるというのは、今時ではもう当たり前ですけど、ちょっと前の言語だとできたりできなかったりすることがありました。言語仕様によって違いますが、Swiftにおいては関数の入れ子が可能です。
具体的には、関数の定義の中で新たに関数を定義し、その中で使うことができるということです。これをする理由は、複雑な処理を分割したい場合、その分割したコードが他の関数では使い必要がない場合に便利です。局所的なもののとき、このように入れ子にしてあげると、その処理はこういうものですよという名前付けができ、名前を使って整理するという使い方ができます。
特に難しいことはなく、あんまり出番はないかなーと個人的には思いますが、使わないことはないです。一般的に、関数を入れ子にするシーンもあるかと思います。では、実際にコードを書いてみましょう。
ビルドアクションをコードで実現する場合を考えましょう。例えば、enum
でアクションを定義します。
enum Action {
case build
case clean
case archive
}
そして、関数でそのアクションを実行するようにしましょう。
func perform(action: Action) {
switch action {
case .build:
// ここでビルドの処理を実行
print("Building...")
case .clean:
// クリーンの処理を実行してからビルド
print("Cleaning...")
print("Building...")
case .archive:
// アーカイブの処理
print("Cleaning...")
print("Building...")
print("Archiving...")
}
}
この例で、ビルドの処理が複数の場所で使われているとしますが、同じ処理を繰り返さないように関数を入れ子にして整理するとよいですね。
func perform(action: Action) {
func build() {
print("Building...")
}
func clean() {
print("Cleaning...")
}
func archive() {
clean()
build()
print("Archiving...")
}
switch action {
case .build:
build()
case .clean:
clean()
build()
case .archive:
archive()
}
}
このようにすることで、関数build
、clean
、archive
がそれぞれの役割を持ち、スイッチ文も明瞭に書けます。重要なポイントは、入れ子にした関数はその中でしか使えないという点です。ですから、例えばperform(action: .build)
の外でbuild()
を直接呼び出すことはできません。これは、名前空間とは全然違います。 なので、本当にこのアクションというものについて、その中だけでしか使わないものを作成します。プライベート関数のようなイメージに近いですね。この書き方もプライベート関数のように使用する機会が少ないわけではないですが、機会が減ってくると思います。それと同じような感覚で入れ子にする関数も、対比値が限られてくるかなと感じます。ただ、この辺りはあくまで感覚的なもので、例えば、変数を入れ子にするのは当たり前です。関数や計算型プロパティを入れ子にすることも同様です。例えば、計算型プロパティを入れ子にして使うことは少ないですが、私自身は時々利用します。関数の中で利用すること自体は、入れ子にしているのが変数か関数かの違いだけです。もし外で使いたい場合には、その外のスコープに置きます。こういう感覚で関数を使えるのが今回お話したい大事なポイントです。
ちなみに、中でしか使えないとは言いましたが、戻り値が Void
を返す関数についても話します。Void
を返す関数を返すことも可能です。例えば、中で定義した関数をリターンして外に投げることもできます。
コメントをいただいたように、スコープを狭める観点からすると、一つの複雑なメソッドを複数のメソッドに分割するよりも入れ子にして外から使われないようにする方が良いのかどうか、その判断が求められます。どちらが良いかは具体的な状況によりますね。私はだいたい入れ子にするよりも分割して並列で実装することが多いです。ただ、必要がないと判断した場合には入れ子にしています。
例えば、今回のアクションのビルドについて話すと、例として中に入れていますが、実際は外に出すことが多いです。もう少し詳細を言うと、ビルドやクリーンを担当しているのはコンパイラだろうと思いますので、コンパイラクラスを作成してそこに持たせるかもしれません。ファンクビルド
があるように、どこに所属させるのが適切かを考えます。入れ子にして閉じておくかどうかは、機能分割が必要になってから判断することがあり、個々の状況に応じて最適な方法を選びます。
また、Swiftのスコープの概念は少し特殊で、関数よりもファイルを単位としてパブリック、ファイルプライベート、ローカルなどの捉え方になります。このため、わざわざ入れ子にするほどでもない場合もあります。C言語的には入れ子にしたほうがスコープを汚さないという感覚があるかもしれませんが、Swift的な感覚で考えると、ローカルに出しても問題ないとか、外に出しても問題ないと判断する場合もあります。この辺りも頭の片隅に入れて、どれが適切かを考えると面白いと思います。
この辺りのファイルのアクセス範囲についてはさらに詳細にお話ししたい部分もありますが、今日は時間になってしまったので、別の機会にお話ししたいと思います。では、今日の勉強会はこれで終わります。お疲れさまでした。ありがとうございました。