最近Prismの勉強をしています。 勉強といってもHaskell力がさほどないのでコードとにらめっこしている時間が長いのですが。 その途中で面白い記事を見つけました。 Haskellで出てくる様々なFunctorを紹介した記事です。今回はこれらをSwiftで定義してみようという記事です。
通常の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では様々なProtocol
でmap
が実装されているため 馴染み深いと思います。HaskellでFunctor
クラスとして定義しているのに対し、 Swiftでは様々なProtocol
でmap
が実装されています。 これはジェネリックパラメータが異なる型をProtocol
で表現できないためです。
// × Functor<T>という表記ができない (Swift2.1現在)
protcol Functor<T> {
func map<U>(f: T -> U) -> Functor<U>
}
そのため、CollectionType
のmap
がリストを返さざるをえないのはなんとも残念な感じです。
// ex. CollectionType
func map<T>(@noescape transform: (Self.Base.Generator.Element) -> T) -> [T]
ただし、抽象化してしまうとdefault implementation
が機能しなくなるため、 default implementation
を使うという方針が変わらない限りこのような抽象化はされないような 気がします。
次に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 }
次に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 -> b
とg :: c -> d
を引数に取り、f a c
からf b d
にマップします。 Swiftっぽい表記をするならば、f<A, B>(a: A) -> B
とg<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)
最後に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>
にマップされます。
応用事例などはこれから勉強します。