Posted at

Functors in Swift

More than 3 years have passed since last update.

つい最近投稿したブログの転載です


最近Prismの勉強をしています。

勉強といってもHaskell力がさほどないのでコードとにらめっこしている時間が長いのですが。

その途中で面白い記事を見つけました。

Haskellで出てくる様々なFunctorを紹介した記事です。今回はこれらをSwiftで定義してみようという記事です。


  1. Covariant Functors

  2. ContraVariant Functors

  3. Bifunctors

  4. Profunctors


Covariant Functors

通常のFunctorのことです。Haskellでは以下のように表記します。

class Functor f where

fmap :: (a -> b) -> f a -> f b

f aという型がfmapによりf bという型になリます。

Swiftっぽく説明するため図解するとこんな感じでしょうか。

             f

A +--------------> B
X<A> X<B>

Aというジェネリックパラメータを持つ型X<A>が関数f(x: A) -> Bとともに

新たな型X<B>へとマップされます。Swiftでは様々なProtocolmapが実装されているため

馴染み深いと思います。HaskellでFunctorクラスとして定義しているのに対し、

Swiftでは様々なProtocolmapが実装されています。

これはジェネリックパラメータが異なる型をProtocolで表現できないためです。

// × Functor<T>という表記ができない (Swift2.1現在)

protcol Functor<T> {
func map<U>(f: T -> U) -> Functor<U>
}

そのため、CollectionTypemapがリストを返さざるをえないのはなんとも残念な感じです。

// ex. CollectionType

func map<T>(@noescape transform: (Self.Base.Generator.Element) -> T) -> [T]

ただし、抽象化してしまうとdefault implementationが機能しなくなるため、

default implementationを使うという方針が変わらない限りこのような抽象化はされないような

気がします。


Contravariant Functors

次にContravariantクラス。

class Contravariant f where

contramap :: (b -> a) -> f a -> f b

まず図解してみます。

             f

B +--------------> A
X<A> X<B>

先ほどとの違いは、mapする時の関数fにおける引数と返り値の型が逆になっていることです。

これを満足できるような型としては、ジェネリックパラメータの型を引数にとるような関数を

持っている場合が挙げられます。具体例を書くとこんな感じ。

struct Predicate<T> {

let getPredicate: T -> Bool

func contramap<U>(g: U -> T) -> Predicate<U> {
return Predicate<U>(getPredicate: { self.getPredicate(g($0)) })
}
}

let odd: Predicate<Int> = Predicate(getPredicate: { $0 % 2 != 0 })
let str: Predicate<String> = odd.contramap{ $0.characters.count }


Bifunctors

次にBifunctorクラス。

class Bifunctor f where

bimap :: (a -> b) -> (c -> d) -> f a c -> f b d

Bifunctorクラスは2つの型引数を取るクラスです。

(3つ取るのをtrifunctorsと表記するらしい)

まずはこれも図解してみます。

               f

A +--------------> B
| |
| |
X<A, C> | | X<B, D>
| |
| |
C +--------------> D
g

f :: a -> bg :: c -> dを引数に取り、f a cからf b dにマップします。

Swiftっぽい表記をするならば、f<A, B>(a: A) -> Bg<C, D>(c: C) -> D

2つの関数により、X<A, C>からX<B, D>にマップすると言うことになります。

Swiftで2つのtype parameterをセットにとるものとしてEither<T, U>がよく用いられます。

enum Either<A, C> {

case Left(A)
case Right(C)

func bimap<B, D>(f: A -> B)(_ g: C -> D) -> Either<B, D> {
switch self {
case .Left(let x):
return .Left(f(x))
case .Right(let x):
return .Right(g(x))
}
}
}

let left: Either<String, String> = Either.Left("0")
let right: Either<String, String> = Either.Right("0.0")
let f: String -> Int = { Int($0)! + 1 }
let g: String -> Float = { Float($0)! + 2.0 }

left.bimap(f)(g) // Either<Int, Float>.Left(1)
right.bimap(f)(g) // Either<Int, Float>.Right(2.0)


Profunctors

最後にProfunctorです。

class Profunctor f where

dimap :: (a -> b) -> (c -> d) -> f b c -> f a d

               f

B <--------------+ A
| |
| |
X<B, C> | | X<A, D>
| |
| |
C +--------------> D
g

Profunctorクラスも2つの型引数を取るクラスです。

Bifunctorの図と見比べてもらえばわかるように、上の矢印の向きが逆になっています。

Covariantに対するContravariantのような感じですね。

Haskellにおける具体例としては(->)が挙げられています。

(->)は関数の中置き表示でして、A->B->C->Dという風にぐるっと回ってこれるというものです。

詳しくは参照ページにて解説してもらうとして、Swiftではどう書けるのか。

どのようなサンプルが説明に適しているのかわからなかったのですが、以下のようにdimapが定義可能です。

struct Indexed<I, A, B> {

let runIndexed: I -> A -> B

func dimap<C, D>(f: C -> A)(_ g: B -> D) -> Indexed<I, C, D> {
return Indexed<I, C, D>(runIndexed: { index in
return { g(self.runIndexed(index)(f($0))) }
})
}
}

ここで定義したIndexed<I, A, B>が関数dimap(f)(g)により

Indexed<I, C, D>にマップされます。

応用事例などはこれから勉強します。


参考