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には表現されているのである。

こういう話は楽しい。

参考文献