13
13

More than 5 years have passed since last update.

Functors in Swift

Posted at

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


最近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>にマップされます。

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

参考

13
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13