Swift不思議発見〜Swift3での変更に隠された本当の意図を探る〜

  • 79
    Like
  • 0
    Comment

§0. 序

こんにちは。iPhoneメンターのとうようです!

この記事はLife is Tech ! Advent Calendarの16日目の記事です。とうとう残す日数もわずかとなってきましたね:relaxed:

当初はAndroidとiOS開発の比較でもしようかなとか思ったりしてたわけですが、先日メンター間で壮絶な議論を起こした疑問

なんでSwift3でUIColorのデフォルトカラーがメソッドからプロパティになったのか?

の答えについて紐解いている内に面白くなってきたので以下の場所を読み解いていきながら、変更の意図というものを探っていきたいと思います。

Swift3に関する資料

とりあげた疑問点は独断と偏見で気になるだろうところからピックアップしました。(他にもコメントしていただければ調べて更新するかもしれません)

§1. UIColorのデフォルトカラー

疑問

最初は冒頭で書いたUIColorのデフォルトカラーに関してです。例えば青色を取得したいときなど今までは

swift2
UIColor.blueColor()

といったようにメソッドを利用していました。ですがこれがSwift3で

swift3
UIColor.blue

というようにプロパティで取得するように変わりました。この変更は今までのものに慣れていた人にとっては少しびっくりしたかもしれませんが、純粋に考えるとただ定数を返すだけのものがメソッドであった今までがおかしかったと考えるのも自然である気がします。
ではどうしてこのタイミングでそれが修正されたのでしょうか?

答え

その答えはSwift API Design Guidelines(以下便宜上SwiftADGと略します)の

Document the complexity of any computed property that is not O(1).

という項目にあります。この意味するところはオーダーがO(1)じゃないプロパティにはそのことがわかるように注釈を付けましょうというものですが、これはさらに踏み込むとオーダーがO(1)じゃないものはプロパティにはしないでといっている風にもとれます。さらに言ってしまえばO(1)ならプロパティ、オーダーがそれ以上ならメソッドにしようという風にも解釈することが可能です。

これに則ってSwift3ではUIColorのデフォルトカラーはあきらかにO(1)なのでプロパティに変更されました。
少々乱暴な推論にも思えますが、同じ理由で今までプロパティだったものがメソッドに、メソッドだったものがプロパティになっているもののいくつかがSwiftEvolution内のApply API Guidelines to the Standard Libraryの中で実際に紹介されています。プロパティからメソッドに変わったものとしてはStringのlowercaseStringなどがあげられています。これは文字列を全て小文字にして返すプロパティなのですが、扱う文字列の長さをnとしてO(n)は最低でもかかる処理であるのでこのような措置がとられたと考えられます。


ここから脱線

ところで...

勢いのままここまで来てしまいましたが、そもそもオーダーとかO(1)とかなんやねんって人が多いと思うのでここで一旦その説明をしたいと思います。
オーダーとはコンピューターサイエンスの用語でプログラムの計算量を指します。つまりその処理がどのくらいの量の計算を必要としているかの目安になるものです。オーダーが大きければ大きいほど計算する量が増え、時間もかかるようになるので、処理のロジックもといアルゴリズムを研究している人たちはなるべくこのオーダーが小さくなるようなアルゴリズムを考え出すことを目標に取り組んでいます。

具体的に見ていきましょう。先程から何度も出てきたO(1)というのは代入や四則演算などの単純な処理だけの、繰り返しの入らないプログラムのオーダーです。
その次に大きい...というと厳密ではないですが、オーダーがO(n)というのはプログラム全体で一番大きい繰り返しがn回のループであるということになります。オーダーでは括弧の中にnのような文字が入るのですが、この定数倍は無視するというのが基本なので例えば次のようなメソッドたちがそれにあたります。(nは処理に与えられる入力の大きさが入る変数になっています。そしてそこに扱うデータサイズを代入することで実行時間を見積もっていくことになります。)

O(n)のメソッドたち
func nLoop(n: Int) -> Int {
  var ret = 0
  for i in 0 ..< n {
    ret += 1
  }
  return ret
}

func twoNLoop(n: Int) -> Int {
  var ret = 0
  for i in 0 ..< n {
    ret += 2
  }
  for i in 0 ..< n {
    ret -= 1
  }
  return ret
}

func n2Loop(n: Int) -> Int {
  var ret = 0
  for i in 0 ..< 2n {
    ret += 1
  }
  return ret
}

オーダーは概算するものなのでこのように処理してる回数が厳密にはn回じゃないものもO(n)と計算されます。逆に以下のように繰り返し文が入れ子になってくるとO(n)じゃなくなってきます。

O(n)じゃないメソッド
func nnLoop(n: Int) -> Int {
  var ret = 0
  for i in 0 ..< n {
    for j in 0 ..< n {
      ret += 1
    }
  }
  for i in 0 ..< n {
    ret -= 1
  }
  return ret
}

ここでつくったnnLoop()の実際のオーダーは

O(n^2)

と計算されます。このように一つの処理のかたまりの中で一番時間がかかる部分でオーダーは計算されます。オーダーには大体出てくる種類が決まっていて、正確にはグラフアルゴリズムのオーダーなどここに無いものもありますが以下のようになっています。(logの底はすべて2です。)

O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n)

もっと詳しく知りたい方はぜひアルゴリズムの教科書などを読んでみたりネットで検索してみたりしてください。

脱線終わり


§2. Enumの命名規則がどうしてlowerCamelCaseになったのか

疑問

今までenumのcaseに関してはすべて大文字からはじまるようになっていました。しかし今回からこれがすべて小文字からはじまるように修正され、それにあわせて多くのライブラリも自作のenumをlowerCamelCaseに変更しました。

この変更は基本的にXcodeの自動変換で対応できますが、さらにこれに加えて名前を簡潔にしろという項目にしたがったライブラリを使っていた人たちは移行したとたん大量のエラーが出て直すのに苦労したかもしれません。(僕もそうでした😅)

答え

基本的にはSwiftADGの

Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase.

という記述にしたがっています。型やプロトコルの名前以外はlowerCamelCaseにしろというものです。この記述の理由がSwiftEvolutionなどで記載されているところを特定することはできませんでしたが、この変更は”紛らわしさを減らす”という言語として目指す一般的な目標にかなっていると言えるでしょう。

ここで言う型やプロトコルというのはSwift上ではclassstructenumprotocolで定義されるものです。これら四つとその他全てをしっかり名前的に区別したのです。Swiftには型推論可能なところは.(ドット)の前を省略できる記法があるので、classのプロパティとenumのcaseを同じように書けるということも書いている人に余計なストレスを与えないという意味でいい変更なのではないかなと感じました。

§3. インクリメント・デクリメントが消えたわけ

疑問

Swift2.2から非推奨となりSwift3で完全につかえなくなったインクリメントとデクリメント、すなわちn++m--といったものですが、これらはなぜ消されなければならなかったのか?という疑問です。

これらはC系言語やJavaなど有名な高級言語によくついており、その記述量の少なさから使われる機会も多いものです。なぜSwiftでは消されなければならなかったのでしょうか?

答え

この項目について調べるのはかなり簡単でした。SwiftEvolutionsの中にRemove the ++ and -- operatorsというものがあってここにインクリメント、デクリメントの利点と欠点がまとめられています。

簡単に訳してみると、まず利点としては

  • 使い所があまり制限されず、記述量も少なくて便利
  • C系言語や有名言語でも採用されていてとても馴染み深い

の二つがあげられています。これに対して欠点は七つもあげられていました。

  • Swiftが初めて学ぶプログラミング言語になる学習者の負担を増やしてしまう
  • +=とくらべた記述量の差がそこまで大きくはない
  • Swiftで=+=などCから持ってきた演算子は統一して返り型をVoidとしているが、インクリメント・デクリメントはこれに当てはまらない
  • インクリメントを使ったCスタイルのfor文の優れた代替物がすでにSwiftには多く実装されている
  • インクリメント・デクリメントをつくったコードは挙動を予測しずらいという意味で可読性が下がる
  • Swiftの評価方法は明確に定義されている(well-definedである)がインクリメント・デクリメントが入るとそれが曖昧になる
  • 使える型が限られている

だいたいこんな感じです。和訳が間違っていたらすいません。。。

ここで言われていることを総括するとすなわち、インクリメント・デクリメントというのは書き方や使う場所によって結果が変わるという性質がありそれが入ったコードはわかりやすく誰でも使える言語という理念に反するからやめよう...とこんな感じです。
例えばCなどでこういうコードを書くのはNGです。

NG.c
n++ + n++

実際に次のようなコードを書いてみます。

test.c
#include <stdio.h>

int main() {
    int n = 0;
    printf("%d\n", n++ + n++);
    printf("%d\n", n);
    return 0;
}

書いた人の意図としてはおそらく最初の出力が0、次が2になっていればいいのかなと思うのですが実際実行してみると次のようになります。

$ gcc test.c
test.c:5:21: warning: multiple unsequenced modifications to 'n' [-Wunsequenced]
    printf("%d\n", n++ + n++);
                    ^     ~~
1 warning generated.
$ ./a.out
1
2

コンパイラに怒られてしまった上に、意図とは違う結果になってしまいました。
このようにインクリメントを真の意味で使いこなすには少し初心者には敷居が高く、そういった意味でプログラミングを普及させるという意図も含まれたSwiftという言語にはそぐわなかったのでしょう。

§4. 第一引数にもラベルが必要になった理由

疑問

Swiftは今までもメソッドの第二引数以降にはラベルが必要でした。それがSwift3になり第一引数も必要というように統一されました。
例えば

Define
func add(_ a: Int, _ b: Int) -> Int { return a + b }
func delete(withProperty str: String?) {
  guard let del = str else { return }
  ...
}
func add2(a: Int, b: Int) -> Int { return a + b }

といったように書いた場合

Use
add(1, 2)
delete(withProperty: "hello.com")
add2(a: 10, b: 2)

のように使うことになります。

答え

こちらもSwiftEvolutionにMotivationとして記載されています。
Establish consistent label behavior across all parameters including first labels

ここらへんで性能がよくなったと言われるGoogle翻訳にMotivationの項目をまるごと投げてみましょう!!!(ということでなげてみたのがコチラです。)

現在の技術水準では、Swift 2の方法および機能は、ローカルおよび外部の名前をラベルパラメータに組み合わせる。これらの区別されたシンボルは、内部実装と外部消費の名前を区別します。デフォルトでは、パラメータリストの最初に表示されるSwift 2パラメータ宣言は、その外部名を省略しています。 2番目以降のパラメータは、ローカル名を外部ラベルとして複製します。これらのスウィフト2のルールの下で、func foo(a:T、b:U、c:V)はfoo(_:b:c :)を宣言し、foo(a:b:c :)を宣言しません。歴史的に、このラベルの振る舞いはSwift 2で標準化されました。以前は別々のデフォルト動作を使用していたメソッドと関数のパラメータ命名規則を統一していました。新しい統一アプローチは、最初のパラメータラベルがメソッドシグネチャの最初の部分に含まれていたObjective-C命名規則に近似しています。ほとんどの場合、Swift 2の開発者は、このアプローチを模倣し、パラメータ名から関数名またはメソッド名にラベル名を移動させる呼び出しを作成するよう奨励されました。 Swift 3の新しく承認されたAPI命名ガイドラインは、このアプローチを揺さぶった。彼らはメソッドと関数の最初の引数ラベルをより徹底的に受け入れました。更新された命名のガイダンスは、Swift 3で最近承認された自動Objective-C API変換ルールによってさらにサポートされています。これらの改訂されたガイドラインでは、最初の引数ラベルが推奨されますが、これに限定されるものではありません。デフォルトのメソッドと関数第1引数が前置詞句を使用するファクトリメソッドを実装するメソッドと関数メソッドと関数が単一の抽象化の分割フォームを表すメソッドと関数第1引数ラベルもイニシャライザの標準です。この拡張されたガイダンスは、最初の引数ラベルの使用の範囲を広げ、第1引数の例外の正当性を弱める。パラメータ宣言が一様に動作することを保証することで、スウィフトの明快さと一貫性の目標をサポートします。この変更により、最も単純で最も予測可能な使用法が作成され、名前付けタスクが簡素化され、混乱が少なくなり、言語への移行が容易になります。

いやー優秀ですね...大体これでもうわかっちゃいます笑

理由として重要なところは最後の数行ですね。つまり、言語仕様として一貫性をもたせてSwiftの導入を簡単にするという目標にそった変更ということです。

最初のほうなどを読むと感覚的に、メソッドはfuncで書いた文が解釈されて適切にラベルとかが省略されて宣言されているのであって、メソッドを使う時に中で補完されるというわけではないという細かな気付きもあったりして面白いです。

まとめ

正直なところを言うとあらためて考えてみればSwift3でかわった所のほとんどが命名規則だったりしたわけで、最初の疑問ほど調べて面白いと思えるものはなかったですが、こういう機会も重要だなと感じました。

暗記が苦手な人はこういったコードのもつ意図を汲み取るようにすると、プログラミングも楽しくなるしコードも自然と覚えられるようになるし一石二鳥なのでは無いかなと思うのでぜひ気になったら調べてみてください!!
もちろんSwift EvolutionのProposalを全て読むっていうのもアリですね笑(絶対につらいですが、@mono0926さんのSwiftニュースレターでちょくちょく紹介されているのでその度に読むというのはアリかもしれません。)

その他にも似たような記事はすでにいくつか書かれているのでぜひそちらも参考にしてみてください!

あとSwift Evolutionsを分析しようという試みがあるらしく、コチラで公開されているのでよかったら試してみてください!

さてさて、長ったらしい文を続けてしまいましたが明日はiPhoneメンターを率いるスーパーメンター、特技は恋ダンス!えぐっちです!!どんな記事を書いてくれるのか楽しみですね(≧∇≦)b