シスアーキ in はてな

シスアーキ(自称)の技術ブログ

関西Kotlin勉強会 反省会

さる9/19(土) に関西Kotlin勉強会に行ってきました!

connpass.com
connpass.com

勉強会に懇親会、みんなに会えて楽しかったよ〜〜!!!(((o(*゚▽゚*)o)))

とま〜、それはいいのですが、この勉強会で発表したLT資料について、

「資料の内容が技術的に正しくなく、フェアでもない」

と多方面から注意を受ける事態となりました。(−_−;)

この記事はその資料の検証と反省を目的としたものです。
既に資料は取り下げたのですが、迷惑をおかけした方たちへのお詫びと反省をかねて、問題の資料内容について自分なりに検証したいと思います。

反省資料の内容

問題となった資料の構成は次のとおりです。

Scalaで拡張関数を実装したけど、implicitばかりでつらいよね
・Kotlinの拡張関数は簡単にかけるよ!

ここでの問題は、

Scala でFunctorのコードを書いているが間違っている(技術的に正しくない)
Scalaのコードが型クラスを実現しようとしているのに対して、Kotlinのコードはそれを実現しようとしていない(フェアじゃない)

が指摘された内容だと考えています。

Scala でFunctorのコードを書いているが間違っている

まずこれについて。

trait MyFunctor[F[_]] { self =>

  def myMap[A, B](fa: F[A])(f: A => B): F[B]
}
final class MyFunctorOps[F[_],A](val self: F[A])(implicit val F: MyFunctor[F]) {

  def myMap[B](f: A => B): F[B] = F.myMap(self)(f)
}

上記コードについては問題ないと思います。問題は次のコード。

// BAD Logic!!!
implicit val listMyFunctor = new MyFunctor[List] {
  override def myMap[A, B](fa: List[A])(f: (A) => B): List[B] = {
    def myMapFunc(acc: List[B])(l: List[A])(f: A => B): List[B] = {
      l match {
        case (head :: tail) => myMapFunc(acc :+ f(head))(tail)(f)
        case _ => acc
      }
    }
    myMapFunc(Nil)(fa)(f)
  }
}

mapの実装を不必要にややこしく書いています。

// BAD Logic!!!
implicit def listMyFunctor[A](l: List[A]):MyFunctorOps[List, A] =
  new MyFunctorOps[List, A](l)

暗黙の型変換を目的としたロジックが不適切です。
このロジックだと、Functorを実装した分だけ同じような定義をしないといけないです。

とりあえず、まーまー、正しいかと思われるコードについて書きました。
まだ自信ないですが。。

implicit val listMyFunctor = new MyFunctor[List] {
  override def myMap[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}

まず、myMapの実装を簡単に書きました。実装内容は今回の資料の対象ではないので。

  implicit def toMyFunctorOps[F[_], A](fa: F[A])(implicit F: MyFunctor[F]) =
    new MyFunctorOps[F, A](fa)

次に、暗黙の型変換を目的としたロジックを修正しました。
MyFunctorの実装を型ごとに暗黙の引数で受け取れるように修正したことで、同じ定義を書く必要がなくなりました。

Scalaのコードが型クラスを実装しようとしているのに対して、Kotlinのコードはそれを実現しようとしていない

先ほどのFunctorは型クラスを実現しようとしていました。
型クラスはアドホック多相 を実現するものです。
アドホック多相とは、

異なる型の間で共通したインターフェースでの異なる振る舞いを定義済みの型に対して拡張する

ものらしいです。
それを実現する為に、

・型クラスの定義
インスタンスの定義
・型クラスの利用

という手順を踏む必要があります。

今回拡張するFunctorについて、Functorは型パラメータをもつ型です。
先ほどのコードでは、型クラスの定義でFunctor値の型をHigher-kind Genericsを用いて定義し、インスタンスの定義、および型クラスの利用の為にimplicit parameterとimplicit conversionの機能を用いています。

資料の中で「implicitだらけでScala怖い」と表現した件について、僕の現状のScala力では読みにくいという感想でした。ただ、その比較としてKotlinの拡張関数で以下のコードを提示し、Kotlinが簡単と表現したのはフェアじゃないですね。

fun <T, R> List<T>.myMap(f: (T) -> R): List<R> = this.map(f)

上記コードだと、

定義済みの型に対して拡張する

は実現できても、

異なる型の間で共通したインターフェースでの異なる振る舞い

は実現できていないです。上のようなコードだけで良ければ、Scalaのimplicit conversionでも簡単に実現できます。*1

implicit class ToFunctorList[A](l: List[A]) {
  def myMap[B](f: A => B) = l.map(f)
}

ちなみにですが、Kotlinの拡張関数はただのシンタックスシュガーです。

fun <T, R> myMap(l: List<T>, f: (T) -> R): List<R> = l.map(f)

のような関数定義があり、第一引数の型をレシーバとして呼び出せる簡易構文を提供しているみたいです。
実際に、上の定義を同じパッケージ内で定義すると、コンパイラJVM上では同じシグネチャだよって怒られます。

KotlinでFunctorを定義してみる

はい、ではフェアである為にKotlinでFunctorの定義を頑張ってみます。
ただ、KotlinにはHigher-kind Genericsもimplicitのような機能もありません。ですので、違う方法で

異なる型の間で共通したインターフェースでの異なる振る舞いを定義済みの型に対して拡張する

を実現できるよう考えてみました。

・インタフェースの定義
・Delegation機能を用いたインタフェース実装の定義
・拡張関数を用いてインタフェース実装への型変換

interface MyFunctor<A> {

    fun myMap<B>(f: (A) -> B): MyFunctor<B>
}
class MyFunctorList<A>(val list: List<A>) : MyFunctor<A>, List<A> by list {

    override fun myMap<B>(f: (A) -> B): MyFunctor<B>
        = MyFunctorList(list.map(f))

    override fun toString() = list.toString()
}

fun <A> List<A>.asFunctor(): MyFunctorList<A>
    = MyFunctorList(this)
fun main(args: Array<String>) {

    println(arrayListOf(1, 2, 3, 4, 5).asFunctor().myMap { n -> n * 3 })
}

以上です。
KotlinではClass Delegation*2の機能がありますので、拡張する型がインタフェースであれば、委譲クラスを簡単に作成できます*3
Scalaのように強力ではないですが、

異なる型の間で共通したインターフェースでの異なる振る舞いを定義済みの型に対して拡張する

は実現できたのではないかと思います。

今回の反省

LTはライトニングトークであって、軽いノリで誤った資料でいい訳ではない

ってことですね。
深夜のテンションで酒飲みながら資料作るのはもうやめようっと。。

修正版の資料はこちらにアップしました!

www.slideshare.net
また、上記コードは以下にアップしています。
kozake/ScalaStudyFunctor · GitHub
kozake/KotlinStudyFunctor · GitHub

<09/23 追記>
上記Kotlinコードにおいても、Scalaで実現していることを実現出来ていないと指摘を受けました。
確かに実現できていないので、比較対象としては不適切です。あくまで参考レベルの記事とさせてください。

*1:まあ、資料の中でもあくまでネタで、Scalaでも簡単に実現できるとは書いていましたが。。

*2:Delegationhttp://kotlinlang.org/docs/reference/delegation.html

*3:残念なことにクラスの委譲クラスは作成できないですが