Help us understand the problem. What is going on with this article?

Swiftのパワー(objc.io #16-1 日本語訳)

More than 5 years have passed since last update.

この記事はobjc.io, Issue #16, The Power of Swiftの日本語訳です。

Swiftのパワー

Issue #16 Swift, September 2014
By Chris Eidhof

まずはじめに、私は非常に偏っていることを認めなくてはなりません。私はSwiftが大好きです。私がCocoaのエコシステムに関わって以来起こったことの中で一番いい出来事だと思います。なぜそう思うか、それを私のSwift、Objective-C、Haskellの経験を共有することで伝えたいと思います。この記事で取り上げることはベストプラクティスということではなく(執筆時点でSwiftは新しすぎて確立したベストプラクティスはまだありません)、Swiftがどこで威力を発揮するのか、その例をお見せします。

少し個人的なバックグラウンドを説明すると、iOS、OS Xプラットフォームのプログラマーになる前、私は何年かHaskellを(他の関数型言語とともに)書いていました。私は今でもHaskellは経験した中で最も美しい言語の一つだと思っています。しかし、iOSは最もエキサイティングなプラットフォームだと感じた(今もそう感じている)ので、Objective-Cに転向しました。確かに最初Objective-Cは少々使いにくいところがありましたが、後に良さがわかるようになりました。

WWDCの場でAppleがSwiftを発表した時、私は本当に興奮しました。新しい技術の発表でこんなに興奮したことは近年他にありません。ドキュメントを読んだ後、Swiftによって関数型言語の素晴らしい知見を使いながらも、Cocoa APIとシームレスに統合できるということに気づきました。この二つの機能の組み合わせは非常にユニークだと思います。これほどうまく二つを合わせられる言語は他にありません。Haskellのような言語ではObjective-CのAPIを呼ぶことは困難ですし、Objective-Cで関数型プログラミングを行うのは難しいでしょう。

私は関数型プログラミングをユトレヒト大学在学中に学びました。学術的な文脈で学んだため、複雑な用語に圧倒されることはそれほどありませんでした。モナド、applicative functor、その他色々あります。関数型プログラミングを学ぼうとする人にとって、これらの用語は一つの大きな障害だと思います。

用語だけでなくスタイルも難解です。Objective-Cプログラマーになるにあたって、私たちはオブジェクト指向プログラミングに慣れ親しみました。そして多くのプログラミング言語がオブジェクト指向かそれに近いスタイルを採用しているため、私たちは多くの言語のコードを読むことができます。しかし関数型言語はそうはいきません。慣れていないと全くわけのわからないものに見えます。

それではなぜ関数型プログラミングを使うのでしょうか。奇妙で、多くの人が慣れておらず、学ぶにはかなりの時間がかかります。さらには、私たちはすでにオブジェクト指向プログラミングでどんな問題も解くことができるので、新しいことを学ぶ必要はない、そうでしょう?

私にとっては、関数型プログラミングは道具箱の中の一つの道具です。プログラミングについての考え方を変えるほど強力な道具です。問題を解くのに非常に役に立つことがあります。多くの問題にはオブジェクト指向プログラミングが適しています。しかし、関数型プログラミングで解くことで非常に多くの時間とエネルギーを節約できるような問題もあります。

関数型プログラミングを始めるのは少し辛いかもしれません。例えば、古いパターンを捨てなければなりません。私たちの多くが長年オブジェクト指向的に考えてきたため、これは非常に困難です。関数型プログラミングでは、不変のデータ構造と関数を考えます。オブジェクト指向言語では互いにメッセージを交わすオブジェクトを考えます。関数型プログラミングがすぐに理解できなければそれは良いサインです。あなたの脳はオブジェクト指向的に問題を解決する考え方にどっぷり浸かっているのです。

Swiftの機能の中でお気に入りの一つがオプショナルです。オプショナルによって存在するかもしれないし存在しないかもしれないという値を扱うことができます。Objective-Cではnilを許すかどうかはドキュメントに明記する必要がありました。オプショナルを使うことでこの責任を型システムに委ねることができます。オプショナル型の値を持っているということはnilになり得るということです。オプショナル型ではない値を持っているならば、それはnilにならないことがわかります。

例えば、以下のObjective-Cのコードを考えます。

- (NSAttributedString *)attributedString:(NSString *)input 
{
    return [[NSAttributedString alloc] initWithString:input];
}

一見無害に見えますが、もしinputがnilだとクラッシュします。これは実行時に初めてわかることです。使われ方によって、すぐに見つかることもあるかもしれませんが、リリース後に発覚し、ユーザー側でクラッシュさせてしまうことになるかもしれません。

同じAPIをSwiftで見てみましょう。

extension NSAttributedString {
    init(string str: String)
}

Objective-Cからの完全な移植に見えるかもしれませんが、Swiftではnilを渡すことを許容していません。もしnilを許すならば、APIは以下のようになります。

extension NSAttributedString {
    init(string str: String?)
}

「?」が付いていることに注意してください。これはなんらかの値またはnilを渡すことができることを意味します。型は非常に明確で、型を見るだけでどんな値を取りうるのか知ることができます。オプショナルをしばらく使っているとドキュメントの代わりに型を見ればいいことに気づくでしょう。そして間違えた時は実行時エラーではなくコンパイル時の警告で知ることができます。

アドバイス

可能であればオプショナルを避けてください。オプショナルはAPIを使う人にとってはちょっとした精神的障害になります。ですが、確実に有効な使い方があります。明確な理由で失敗する可能性がある関数があるとき、オプショナルを返すことができます。例えば、#00ff00のような文字列を色に変換することを考えてみます。もし入力がフォーマットに従っていなければnilを返したくなると思います。

func parseColorFromHexString(input: String) -> UIColor? {
    // ...
}

エラーメッセージを指定したい場合、標準ライブラリにはありませんが、Either型またはResult型を使うことができます。失敗の原因が重要である場合には有用です。"Error Handling in Swift"にその好例が示されています。

enum

enumはSwiftで新たに登場したもので、私たちがObjective-Cで慣れ親しんだどんなものとも違います。Objective-Cにもenumと呼ばれるものがありますが、あれは見栄えを良くしたintegerにすぎません。

ブール型を考えてみましょう。ブール型は2種類の値のいずれか、trueまたはfalseのみを持ちます。もう一つ別の値を追加することはできないということを理解することが重要です。すなわちブール型は「閉じて」います。ブール型が閉じていることの利点は、ブール型を使うどんな関数においても、truefalseだけを考えれば良いということです。

オプショナルについても同じことが言えます。nilか、値があるかの二つの場合のみです。オプショナルもブール型もSwiftでは同様にenumとして定義できます。一つだけ異なる点として、オプショナルのenumには割り当てられた値を持つ場合があります。それぞれの定義を見てみましょう。

enum Boolean {
    case False
    case True
}

enum Optional<A> {
    case Nil
    case Some(A)
}

これらは非常に似通っています。caseの名前を変えれば、違いは割り当てられた値だけです。仮にオプショナルのNilにも値を持たせれば、これはEither型となります。

enum Either<A,B> {
    case Left(A)
    case Right(B)
}

Either型は関数型プログラミングにおいて二つの選択肢を表したいときによく使われます。例えば整数値かエラーのどちらかを返す関数があるとすれば、Either<Int,NSError>を使うことができます。もしブール型か文字列のどちらかを辞書に格納したい場合、Either<Bool,String>を値の型として使うことができます。

理論的余談:enumは異なる型の和を表すため、sum typeと呼ばれます。Either型の場合、AとBの和を表しています。構造体やタプルは異なる型の積を表すため、product typeと呼ばれます。algebraic data typesを参照してください。

いつenumを使い、いつ他のデータ型(クラスや構造体)を使うのかを理解するのは少々難しいかもしれません。enumは取りうる値が限られたものを扱うときに最も便利なものです。例えば、GitHub APIのラッパーをSwiftで設計する場合、エンドポイントをenumで表すことができます。/zenというエンドポイントはパラメーターを一つも取りません。ユーザープロフィールを取得するためにはusernameを指定する必要があります。そしてユーザーのレポジトリを表示するには、usernameと、結果を昇順でソートするかどうかを指定します。

enum Github {
    case Zen
    case UserProfile(String)
    case Repositories(username: String, sortAscending: Bool)
}

APIのエンドポイントを定義することはenumの使い方として良い例です。APIエンドポイントのリストは有限であり、各エンドポイントに対応したcaseを定義できます。このエンドポイントの値をswitch文で使った場合、もしcaseに漏れがあると警告が出ます。したがってもしある時点でcaseを追加することになったら、私たちはこのenumに対してパターンマッチをしている全ての関数やメソッドを更新する必要があります。

enumを使う側はソースコードにアクセス可能でない限りcaseを追加することはできません。これはとても便利な制限です。BoolOptionalにcaseを追加できたとしたら、それを使っている全ての関数を書き換えなければなりません。

例として通貨換算器をを作ることを考えてみましょう。通貨は次のようなenumとして定義できます。

enum Currency {
    case Eur
    case Usd
}

そうすると任意の通貨に対応する記号を返す関数を書くことができます。

func symbol(input: Currency) -> String {
    switch input {
        case .Eur: return "€"
        case .Usd: return "$"
    }
}

さらにこのsymbol関数を使うことで、システムのロケールに応じてフォーマットされた文字列を作ることができます。

func format(amount: Double, currency: Currency) -> String {
    let formatter = NSNumberFormatter()
    formatter.numberStyle = .CurrencyStyle
    formatter.currencySymbol = symbol(currency)
    return formatter.stringFromNumber(amount)
}

ここで一つ大きな制限があります。通貨に関しては、APIのユーザーが後からcaseを追加できるようにしたくなることがあるでしょう。Objective-Cにおいてインターフェースに型を追加する一般的な方法はサブクラス化です。Objective-Cでは理論的にはどんなクラスもサブクラス化して拡張することができます。Swiftでもサブクラス化は依然として使えますが、クラスに対してのみ可能で、enumには使えません。しかしながら別のテクニックを(Objective-CでもSwiftでも)使うことができます。

通貨記号のためのプロトコルを以下のように定義するとします。

protocol CurrencySymbol {
    func symbol() -> String
}

するとCurrency型をこのプロトコルのインスタンスにすることができます。引数のinputはselfとして暗黙的に渡されるため不要になっていることに注意してください。

extension Currency : CurrencySymbol {
   func symbol() -> String {
        switch self {
            case .Eur: return "€"
            case .Usd: return "$"
        }
    }
}

そしてformat関数をこのプロトコルに適合する任意の型に対して使えるように書き換えられます。

func format(amount: Double, currency: CurrencySymbol) -> String {
    let formatter = NSNumberFormatter()
    formatter.numberStyle = .CurrencyStyle
    formatter.currencySymbol = currency.symbol()
    return formatter.stringFromNumber(amount)
}

これでコードの拡張性が非常に高くなりました。CurrencySymbolに適合する任意の型にたいしてフォーマットできるようになりました。例えば、ビットコインを表す新しい型を作ったとすると、以下のようにすぐにformat関数で使えるようにすることができます。

struct Bitcoin : CurrencySymbol {
    func symbol() -> String {
        return "B⃦"
    }
}

これは拡張性の高い関数を書く良い方法です。引数として個別の型ではなく適合すべきプロトコルを指定することで、APIのユーザーに対して型を追加する余地を残すことになります。enumの柔軟性を利用しながらも、プロトコルと組み合わせることでさらに表現力を高めることができます。オープンなAPIかクローズドなAPIか、ユースケースに応じて気軽に選択することができるのです。

型安全性

Swiftの大きな利点の一つは型安全性(type safety)だと思います。オプショナルで見たように、型をうまく使うことである種のチェックを実行時からコンパイル時に移すことができます。もう一つの例は、Swiftにおける配列の仕組みです。配列はジェネリックであり、同じ型の要素しか持つことができません。文字列の配列に整数を追加することはできません。これによって多くのバグを防ぐことができます。(もし文字列または整数が入る配列が欲しければ、上述したEither型が使えます。)

ここでも通貨変換器を例に、これを汎用的な単位換算器に拡張することを考えてみます。もし量を表すのにDoubleを使ったとしたら少々混乱するでしょう。例えば、100.0は100ドル、100キログラム、その他あらゆる100であるものを意味する可能性があります。我々にできることは、異なる物理量に対しては異なる型を作り、型システムの助けを借りることです。例えば、お金を表す型を以下のように定義できます。

struct Money {
    let amount : Double
    let currency: Currency
}

質量についてはまた別の構造体を定義できます。

struct Mass {
    let kilograms: Double
}

これで誤ってMoneyMassを加算してしまうようなことがなくなります。アプリケーションによってはこのような単純な型をラップするのに便利です。さらに、コードを読むのがずっと簡単になります。pounds:という関数があったとすると、

func pounds(input: Double) -> Double

型を見ただけではこれが何をするのか理解するのは難しいでしょう。ユーロをポンドに変換するのでしょうか? それともキログラムをポンドに変換するのでしょうか? 関数名を変えたり、ドキュメントを書くこともできますが(どちらもいいアイデアです)、3つ目の選択肢があります。以下のように型をより厳密にするのです。

func pounds(input: Mass) -> Double

この関数を使う人が何をするものなのかすぐにわかるようになっただけでなく、誤って単位の異なる値を入れてしまうことを防げるようになりました。もしこの関数にMoney型の値を入れて呼ぼうとしてもコンパイラが許可しません。さらなる改良点としては、返り値も単にDoubleを返すのではなく、より厳密な型にすることが考えられるでしょう。

不変性

もう一つSwiftのとても良い機能が言語レベルでの不変性(immutability)のサポートです。Cocoaにおいてはすでに多くのAPIが不変性の価値を示しています。なぜこれが重要かについての良いまとめとして、value objectsを参照してください。例として、私たちCocoa開発者はペアとなるクラスを良く使います(NSStringNSMutableStringNSArrayNSMutableArray)。NSArrayを受け取った時はそれが変更されないとみなすことができます。完全に保証するためにはcopyしなければなりませんが、そうすることで変更されることのないユニークで不変なコピーになることがわかります。

Swiftでは不変性の仕組みが言語に組み込まれています。例えば、可変の文字列を作りたい時は以下のようなコードを書きます。

var myString = "Hello"

一方、不変の文字列を作りたい時は以下のように書けます。

let myString = "Hello"

自分が知らない利用者が使うかもしれないAPIを作る際に、不変なデータが利用できることは大きな助けになります。例えば、配列を引数に取る関数があったとして、配列をイテレートしている間にその配列が変更されないことがわかっていると非常に使いやすくなります。まさにこの理由によって、不変なデータを使うとマルチスレッドのコードを書くのが格段に楽になります。

もう一つ大きな利点があります。不変なデータのみを扱う関数やメソッドを書くことで、型シグネチャが重要なドキュメントになります。Objective-Cでは多くの場合そうなっていません。OS XでCIFilterを使うことを考えてみましょう。インスタンス化した後にsetDefaultsメソッドを呼ぶ必要があります。これはドキュメントに書かれています。このようにインスタンス化した後、使う前にいくつかのメソッドを呼ばなくてはならないクラスはいくつもあります。問題は、ドキュメントを読まない限りどのメソッドを呼べばいいかが不明瞭であり、変な挙動に悩まされることになりかねないことです。

不変なデータを使うと、型シグネチャから何が起きているのかすぐにわかります。例えばオプショナルに対するmapの型シグネチャを考えます。オプショナルのTがあること、TUに変換する関数があること、結果はオプショナルのUになることがわかります。元の値が変更される余地はありません。

func map<T, U>(x: T?, f: T -> U) -> U?

配列に対するmapも同様です。これは配列のエクステンションとして定義されていて、入力される配列はselfです。TUに変換する関数を引数に取り、Uの配列を返すことがわかります。これは不変な関数なので、元の配列が変更されないこと、結果も不変な配列であることがわかります。これらの制約が型システムに組み込まれていて、コンパイラによって強制されるため、ドキュメントを参照したり、何が変更されるのか正確に記憶しておかなければならないストレスから解放されます。

extension Array {
    func map<U>(transform: T -> U) -> [U]
}

まとめ

Swiftには興味深い新たな可能性がたくさん存在します。特に気に入っているのが、これまで我々がドキュメントを読んで確認しなければならなかったことをコンパイラがやってくれるようになった点です。妥当だと思えば、このような新しい機能を利用することを選択できます。既存の枯れた技術を使い続けることもできますが、コードの一部に新しい機能をオプトインで入れていくこともできるのです。

以下は私の予想です。Swiftは私たちのコードの書き方をいい方向に劇的に変えていくでしょう。Objective-Cからの移行には何年かかかると思いますが、多くの開発者は移行を進め、後戻りすることはないでしょう。すぐに移行する人も、長い時間がかかる人もいると思います。それでも、やがてほとんどの開発者がSwiftによってもたらされる恩恵に気づくと確信しています。

gonsee
カレンダーシェアアプリ「TimeTree」のiOSアプリを開発しています。個人では「陣痛時計」、「ごみの日アラーム」というアプリをリリースしています。Perfumeエバンジェリスト。
https://simplebeep.net/iphone-apps
jubileeworks
「毎日に、新しい"なくてはならない"を創る」を経営理念に掲げ、共有カレンダーサービス TimeTree の開発・運営をしています。
https://timetreeapp.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away