関西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:残念なことにクラスの委譲クラスは作成できないですが