はじめに
Java で higher kinded polymorphism を実現するを読み、これをSwiftに移植すれば何かの役に立つかもしれないと考えた。この記事の完全なソースコードはGitHubの次のリポジトリから入手できる。
この記事を読んで疑問や改善点がある場合には、気軽にコメントなどで教えて欲しい。
Higher Kinded Polymorphismとは?
Higher Kinded Polymorphismは日本語で高階多相などと呼ばれるもので、例えばSwiftで次のようなプロトコルFunctor
1を作成したいとする。
protocol Functor {
associatedtype F
associatedtype E
func mapF<B>(_ f: (E) -> B) -> F<B>
}
型パラメータF
には、たとえばArray
やSet
など何か型パラメータを取るような型パラメータを用いる。しかしSwiftはこのような何か型パラメータを取るような型パラメータを書くことができず、次のようにコンパイルエラーとなる。
Cannot specialize non-generic type 'Self.F'
たとえばプログラム言語Scalaではこれをこのように書くことができる。
trait Functor {
type F[_]
type E
def mapF[B](f: E => B): F[B]
}
このように、型パラメータが何かの型パラメータを取ることで、型パラメータF
がArray
やSet
など何であっても動作するような抽象的な関数を定義できるようになる。このような多相性のことをHigher Kinded Polymorphismと呼ぶ。
SwiftでHigher Kinded Polymorphism
まず、次のようなクラスApp
とプロトコルNewtype1
を作る。
class App<T, A> {
var underlying: Any
init(_ a: Any) {
underlying = a
}
}
protocol Newtype1 {
associatedtype T
associatedtype A
func inj() -> App<T, A>
}
extension Newtype1 {
func inj() -> App<T, A> {
return App(self)
}
}
クラスApp
は2つの型パラメータを取り、コンストラクターに渡された引数をメンバ変数underlying
に格納しておく。また、プロトコルNewtype1
は関数inj
を提供し、これはApp<T, A>
への変換を表す。
これらを利用してたとえばArray
を高階に扱いたいときは次のようにする。
class ArrayConstructor { }
extension Array: Newtype1 {
typealias T = ArrayConstructor
typealias A = Element
}
extension App where T: ArrayConstructor {
func prj() -> Array<A> {
return (self.underlying as! Array<A>)
}
}
このようにすることで、次の2つの操作が可能となる。
- 任意の型
A
について、Array<A>
から関数inj
でApp<ArrayConstructor, A>
へ変換する - 任意の型
A
について、App<ArrayConstructor, A>
から関数prj
でArray<A>
へ変換する
このようにすることで、今まで具体的になっていない型Array
を型パラメータとして渡すことはできなかったが、クラスApp
とArrayConstructor
を利用してApp<ArrayConstructor, A>
という形で渡せるようになった。
同様に二分木を表すTree
についても次のように作ることができる。まずはTree
を次のように定義する。
indirect enum Tree<Element> {
case Leaf
case Node(l: Tree<Element>, a: Element, r: Tree<Element>)
func toString() -> String {
switch self {
case .Leaf:
return "L"
case let .Node(l, a, r):
return "N(\(l.toString()), \(a), \(r.toString()))"
}
}
}
そして次のようにエクステンションを定義する。
class TreeConstructor { }
extension Tree: Newtype1 {
typealias T = TreeConstructor
typealias A = Element
}
extension App where T: TreeConstructor {
func prj() -> Tree<A> {
return (self.underlying as! Tree<A>)
}
}
これらを利用して最初に例として挙げたプロトコルFunctor
を作成する。まずプロトコルFunctor
を次のように定義する。
protocol Functor {
associatedtype F
associatedtype E
func mapF<B>(_ f: (E) -> B) -> App<F, B>
}
最初の例と異なり、Functor
は最後の結果をApp<F, B>
という型で返すようしている。
型Array
の値がプロトコルFunctor
の関数mapF
が利用できるように、次のようなコードを実装する。
extension Array: Functor {
typealias F = ArrayConstructor
typealias E = Element
func mapF<B>(_ f: (E) -> B) -> App<F, B> {
return self.map(f).inj()
}
}
Tree
の場合はやや複雑だが次のようになる。
extension Tree: Functor {
typealias F = TreeConstructor
typealias E = Element
private func loop<B>(_ x: Tree<E>, _ f: (E) -> B) -> Tree<B> {
switch x {
case .Leaf:
return .Leaf
case let .Node(l, a, r):
return Tree<B>.Node(l: loop(l, f), a: f(a), r: loop(r, f))
}
}
func mapF<B>(_ f: (E) -> B) -> App<F, B> {
return loop(self, f).inj()
}
}
最後に、本当にArray
やTree
から関数mapF
が呼び出せるのかを確認する。
print( [1, 2, 3, 4, 5].mapF({ (x: Int) -> Int in return x + 1 }).prj() )
let tree: Tree<Int> =
.Node(
l: .Node(l: .Leaf, a: 1, r: .Leaf),
a: 2,
r: .Node(l: .Leaf, a: 3, r: .Leaf)
)
print( tree.mapF({ (x: Int) -> Int in return x + 1 }).prj().toString() )
これは次の2つの結果を求める。
-
Int
の配列の全ての要素を$+1$した配列を表示する -
Int
の二分木の全ての要素を$+1$した二分木を表示する
結果は次のようになる。
[2, 3, 4, 5, 6]
N(N(L, 2, L), 3, N(L, 4, L))
このように、正しく関数mapF
が実行できていることが分かる。
まとめ
このようにしてSwiftでもHigher Kinded Polymorphismを実現することができた。今回はArray
やTree
のような型パラメータを1つ取るような型のみを例に挙げたが、たとえばEither
のような2つの型パラメータを取る型もクラスApp
を入れ子にしてNewtype2
を作ることで同様に実装できると考えられる。これらによってより高度なプログラムを書くことができればいいと思う。
参考文献
-
Functor
は関数型界隈で意味のある言葉だが、この記事ではFunctor
が何を意味するのかを知っていなくてもよい。 ↩