mike-neckのブログ

Java or Groovy or Swift or Golang

ジェネリクス勉強会に行ってきた件

表題のとおりです。

connpass.com

おっさんなのにわりと知らないことも多くて、不勉強を実感しました。

HaskellGHC.Genericsはあまりよくわかってないので、ちゃんとやるべきですね…

なお、当日のツイートは #ジェネリクス勉強会 since:2017-06-24 until:2017-06-25 で検索すれば見つかると思います。


で、 @kmizu さんがした、抽象型でジェネリクスの主要な概念を実装するという 発表内容 が面白く、高階型もエミュレートできるとのことでしたので、家に帰ってから発表されてた内容を思い出しつつ自分でも書いてみました。

題材としては Functor(二番煎じ) + Maybe Functor(実装が簡単)を用いています。


まず関数を定義します。

trait Fun1 {
  type I
  type O

  def apply(i: I): O
}

次に高階型を定義します。(発表中に気づいて、「なるほど」と思った)

trait Hk1 {
  type E
}

次に Functor を定義します

trait Functor {self =>
  type F <: Hk1
  type A
  type B

  //noinspection ApparentRefinementOfResultType
  def map(o: F {type E = self.A}, f: Fun1 {type I = self.A; type O = self.B}): F { type E = self.B }

  def map(f: Fun1 {type I = self.A; type O = self.B}): Fun1 {
    type I = F { type E = self.A }
    type O = F { type E = self.B }
  } = new Fun1 {
    override type I = F { type E = self.A }
    override type O = F { type E = self.B }

    override def apply(input: F { type E = self.A }): F { type E = self.B } = self.map(input, f) 
  }  
}

ちなみに僕は初めて trait Foo {self => と書いたときの self が何を指しているのかがわかりました

@kmizuさんの発表では最初の map 関数だけ定義されていましたが、Haskellfmap f を新たに関数として用いているパターンを見かけるので、 map(f: A => B): F[A] => F[B] となるような関数も定義してみました(完全に自己満足です)。

で、 Maybe 型を定義します。

trait Maybe extends Hk1 {
  def getValue: E
}

とりあえず、具体的に IntMaybe を定義します

object NothingInt extends Maybe {
  override type E = Int
  override def getValue: Int = throw new NoSuchElementException("this is Nothing")
}

case class JustInt(value: Int) extends Maybe {
  override type E = Int
  override def getValue: Int = value
}

同様に StringMaybe も定義します

object NothingString extends Maybe {
  override type E = String
  override def getValue: String = throw new NoSuchElementException("this is Nothing")
}

case class JustString(value: String) extends Maybe {
  override type E = String
  override def getValue: String = value
}

ジェネリクスを使えば Just[A]Nothing を定義すれば済みますが、型1つずつに型をつくらないといけない辺りが実用性が…と感じられますが、コンピューターが自動でこのようなボイラープレートなコードを生成するのであれば気にならないと思います(コンパイル時に型を決定してこれと同様なバイナリを生成するなどの作戦はあるかと思う)。

そして Functorインスタンスを作ります

object MaybeIntToStringFunctor extends Functor {
  override type F = Maybe
  override type A = Int
  override type B = String

  override def map(o: Maybe { type E = Int }, f: Fun1 { type I = Int ;type O = String }): Maybe { type E = String } =
    o match {
      case NothingInt => NothingString
      case JustInt(v) => JustString(f(v))
    }
}

では、実際にこれを使ってみたいと思います。

// Fun1 を作るのが面倒なので、通常の関数から Fun1 を作る関数を作る
def fun[A,B](f: A => B): Fun1 { type I = A; type O = B } = new Fun1 { type I = A; type O = B; override def apply(i: I): O = f(i) }

// とりあえず、 Functor のインスタンスを implicit な変数にあれする
implicit val maybeIntToStringFunctor = MaybeIntToStringFunctor

// map という関数を作って Functor の map 関数を呼べるようにする
def map(f: Fun1{ type I = Int; type O = String})(implicit tci: Functor { type A = Int; type B = String }): Fun1{ type I = tci.F { type E = Int }; type O = tci.F { type E = String } } = new Fun1 {
  type I = tci.F { type E = Int }
  type O = tci.F { type E = String }
  override def apply(i: I): O = tci.map(i,f)
}

// map関数にFun1を適用して Maybe をmappingする関数を作る
val intToStringMapper = map(fun(i => "the value is " + i))

// ↑の関数に JustInt(10) を適用する
intToStringMapper(JustInt(10)) // -> JustString("the value is 10")

// ↑の関数にNothingInt を適用する
intToStringMapper(NothingInt) // -> NothingString

f:id:mike_neck:20170625010223p:plain

という感じでScalaよくわかってませんが作れました


この Functor ですが、 String から Int にマップする場合にはまた別の Functorインスタンスを作る必要があり、この形で人間がコードを書くのは若干辛いものがありました…(本当はモッを作ろうとしたけど、やめた)