会社の勉強会でSwiftの発表をした話

会社のiOSチーム勉強会で発表の順番が回ってきたのでSwiftの良さについて発表をしました。会社の人にすすめられたので、せっかくだし資料を公開します。が、公開前提に作っていないし準備も不足だったので、資料だけ見てもあまり伝わらないと思います(すみません)。次回はもっと改善したい。

主題としては、Optionalという概念をちょっと違った角度から見ると類似物がいろいろあるよ、という話(とその準備)です。今更Swift入門なんかやっても仕方がないのでちょっと進んだ話題を取り扱おうとしたのですが、だいぶ力不足だった感があります。

補足

まったく言及していませんが、要するにモナドの話をしています(chainと呼んでいるものが>>=に対応します)。60ページで

a.chain({ $0.b }).chain({ $0.c }).chain({ $0.i })
// と
a.chain({ $0.b.chain({ $0.c.chain({ $0.i }) }) })

が等価だ、と書いて説明を省いていますが、これがモナド則の結合律に対応しています。だからといってOptional Chainingを>>=にすり替えたのは、いささか強引でよくなかったかもしれませんが……

BFTaskというのはBolts Frameworkに含まれている非同期処理のライブラリで、JavaScriptで言うところのPromiseになっています。PromiseというだけならSwiftネイティヴの実装もいろいろあるのですが、この勉強会の前の回がたまたまこれを題材としていたので(87ページのがその時のスライド表紙でした)、それを使いました。ObjCのライブラリだとid型がAnyObjectになってしまってSwiftのIntStringがそのまま使えないので、やむなくNSNumberNSStringにしています。


Optional<T>.mapの話

前置き

Swiftのライブラリ定義を眺めていると、こんなものに出くわした。

enum Optional<T> : Reflectable, NilLiteralConvertible {
    // (略)

    /// Haskell's fmap, which was mis-named
    func map<U>(f: (T) -> U) -> U?

    // (略)
}

これの話をする(ちなみに上の引用はSwift1.0のもので、1.1ではコメントが変わっている)。

Optional Chaining

よく知られているように、Swiftでは型Tと型T?とは別のものなので、T型のメソッドをT?型でそのまま呼ぶことはできない。たとえば:

class SomeClass {
    func someFunction(a: Int) -> String { /* ... */ }
}

let neko: SomeClass? // = ...
let mimi = neko.someFunction(0)  // これは書けない。nekoはSomeClass?型だから。
let mimi = neko!.someFunction(0) // これは書けるが、もしnekoがnilだったら死ぬ。

こういうときは、Optional Chainingを使えばこのように書ける。

let mimi = neko?.someFunction(0) // これは書けて、型はString?型となる。

これはつまり、SomeClass型のInt -> String型のメソッドを、SomeClass?型のInt -> String?型のメソッドへと「読み替えた」と思うことができる。

// これは仮想的な記法であって本当はこんな定義はできないが
class SomeClass? {
    // こういうメソッドに「読み替えた」と思える
    someFunction(a: Int) -> String? { /* ... */ }
}

別の読み替え

そういう読み替えができるなら、こういう読み替えもほしくなるのが人情だろう。

func f(x: T) -> U { /* ... */ }   // これを
func f(x: T?) -> U? { /* ... */ } // これに読み替える

mapはまさにそのような読み替えを行う。

func someFunction(a: Int) -> String { /* ... */ }

let a: Int? // = ...
let b = someFunction(a)     // これは書けない。
let b = a.map(someFunction) // これは書けて、bはString?型になる。

mapという名前

なぜこれがmapという名前なのか。ここで配列型にもmapというメソッドがあることを思い出すだろう。

extension Array {
    // (略)

    /// Return an `Array` containing the results of calling
    /// `transform(x)` on each element `x` of `self`
    func map<U>(transform: (T) -> U) -> [U]

    // (略)
}

T?Optional<T>の、[T]Array<T>の略記だったことを思い起こせば、この二つの定義はOptionalかArrayかの違いを除いて同じもののように見える。

実際、同じなのだ。Arrayのmapは、配列の中の値を写像して新しい配列に入れるものだし、Optionalのmapも、Optionalの中の値を写像して新しいOptionalに入れる。ArrayのmapT -> Uの関数を[T] -> [U]の関数に読み替え、OptionalのmapT? -> U?の関数に読み替えるのである。

ところで

Arrayのmapがそうであったように、Optionalのmapにもインスタンス・メソッドではないバージョンもある。

/// Return an `Array` containing the results of mapping `transform`
/// over `source`.
func map<C : CollectionType, T>(source: C, transform: (C.Generator.Element) -> T) -> [T]

/// Haskell's fmap for Optionals.
func map<T, U>(x: T?, f: (T) -> U) -> U?

ArrayではなくCollectionTypeに対して定義されているので多少対応が見づらいが、同じものである。

Haskell's fmapとの関連

ところで冒頭の引用には、“Haskell's fmap, which was mis-named”なるコメントがつけられていた(今はないのだが)。これの話をする。

このfmapというのは、Functor型クラスのfmapのことである。型クラスというのは、気分的にはSwiftのプロトコルのようなもので、型に対して制約を課す。Functor型クラスの定義はこう:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

これは前節で触れた、インスタンスメソッドではないほうのmapの定義に近い。引数の順番は違うし、他にもいろいろと違う点はあるのだが、だいたい「ある型がFunctorであるとは、fmapという関数を持つことである」と読むことができる。

ではFunctorとは何か。これは元は(Haskellにありがちなように)圏論の用語で、日本語では関手と呼ばれる。関手とは何かという話をすると、これは知らない言葉が倍々ゲームで増えることになるのでここでは立ち入らない。HaskellにおいてFunctorがなにを宣言しているかというと、(とてもおおらかに言えば)「この型は性質のよい箱を作る」ということだ。ここで「性質のよい」と呼んでいるのは、「中身の型がわかっていて、任意の写像で移した箱を作れる」という意味である。たとえばHaskellのリストは、次の定義でFunctorになっている。

instance Functor [] where
    fmap = map

要するにリストはリストとしてのmapfmapとしてFunctorである、という話である。このあたりが“mis-named”なのだろう。

(ただし、ここで要請されるのは型が一致するというだけの話であるから、箱に入れてfmapで写像したものと、普通に写像してから箱に入れたものとが一致するとは限らない。写像の合成が一致する(fmap (f . g) == fmap f . fmap g)べき(should)とされているが、文法上の制約ではない。)

まとめ

そしてこの意味で、Array<T>Optional<T>もFunctorなのである。どちらも型TからTを入れる箱の型を作る操作であって、箱の中身を移して新しい箱を作るmapという関数を持つ。Optionalと配列とを同一視するFunctorという視点が、このmapには表現されているのである。

こういう話は楽しい。

参考文献


Associated valueのあるenumは自動でEquatableにならないという話

事象と原因

以下のコードはコンパイルに失敗する。

enum Enum {
    case Value
    case WithNumber(Int)
}

let b = Enum.Value == Enum.Value // エラー: "Cannot invoke '==' with an argument list of type '(Enum, Enum)'"

お前はなにを言っているんだ、と思ったが、どうやらこれは正しい挙動のようだ。

公式Blogによれば、

Simple enums that have no associated data (like MyBool) are automatically made Equatable by the compiler, so no additional code is required.

のようである。なるほど、case WithNumber(Int)の行をコメントアウトするとコンパイルに成功する。

対応

当然、自分でEnumをEquatableにして、==

func ==(lhs: Enum, rhs: Enum) -> Bool {
    switch (lhs, rhs) {
    case (.Value, .Value): return true
    case (.WithNumber(let left), .WithNumber(let right)): return left == right
    default: return false
    }
}

と定義すればよい。switchをネストしなくてよいのはタプルとパターンマッチの強みであろう。where left == right: return trueとすることもできる。

とはいえ、このケースはAssociated valueがIntで自明にEquatableなのだから、察して大目にみてほしい、とは思う。

関連: Hashableについて

The Swift Programming Languageによれば、Associated valueつきenumはHashableでもなくなるようだ。

Enumeration member values without associated values (as described in Enumerations) are also hashable by default.

Associated valueつきenumは普通のenumとは似て非なるものと考えたほうがよいのかもしれない。

参考文献


if文にBool?をそのまま投げると非直感的な評価をされる話(昔話)

注意

この挙動はβ5で変更されました。 具体的にはif optionalFalseが禁止され、nil判定はif optionalFalse != nilと明示的に書くようになりました。これによって以下のような曖昧さはなくなります。

以下は昔話として残しておきます。

昔話

if文の条件式にOptional型を渡すと(当然ながら)nilか否かで判定が行われる。

したがって以下は当然の帰結である。

let optionalFalse: Bool? = false

if optionalFalse { println("\(optionalFalse) Evaluated as true") }
// prints: "Optional(false) Evaluated as true"

ちゃんとunwrapしなくてはいけない。

if optionalFalse! { println("\(optionalFalse) Evaluated as true") }
// prints nothing

ところでこれは:

if !optionalFalse! { println("?") }
// prints "?"

!(optionalFalse!) = !(false) = trueと評価されるらしい。(!optionalFalse)!は評価できない。