【Haskell や圏論が出てこない】Scala で型クラスを完全に理解した話
以下の記事は 2018 年 2 月 に Qiita に投稿した記事の移植です
pixivFANBOX にも投稿しています。ご支援頂ける場合はよろしくお願いいたします。
TL;DR
結論から。
型クラスはちょっとすごいオーバーロード であり 型に対して静的にメソッドを挿入する方法 です。
この記事は
- Haskell や 圏論等の難しそうな知識・議論なしで型クラス概念を具体的に理解する
- 型クラスが実は単なるすごいオーバーロードで、ちょっとした便利なものだと実感できる
- 型クラスとインターフェイスの違い論争で消耗しなくなる
- 型クラスを知っているとなにがオトクなのかわかる
- Haskell 等の高度な抽象化の恩恵を Java 的な慣れ親しんだシンタックスで書ける Scala は便利でたのしいと感じる
ことを目的としています。
対象読者
- 関数型とか型クラスとかモナドとかよく聞くけど全然わからない…でも理解したい気持ちはある
- 型クラスの概念はいろいろ読んでなんとなく分かるけど説明が Haskell ばかりで実感がわかない
- 型クラスがなんなのかはぶっちゃけどうでもいいが、便利なのかどうか、どう便利なのかを知りたい
Scala 自体はわからなくても大丈夫です。
まえおき
最近またにわかに 型クラス が話題になっていました。
モナドや圏論同様、Haskell の神秘じみた概念の一つして定期的に話題になっていて、なんだかすごくてかっこよさそうなんだけれど、そもそも Haskell をよく知らないし、理論色の強い解説が多くなかなかスッキリ消化できない方が多いのではないかと思います。私もその一人です。Haskell とかさっぱりです。
しかし 型クラスが本当に素晴らしいものなら Haskell を使わなくても素晴らしいといえるはず ということで、この記事では Pure Scala で型クラスを具体的に理解することを目指します。
"型クラス" とはなにか
最初に、型クラスそのものではなく、"型クラスという誤解を招きやすい名称" について整理したいと思います。Java や C++ 等にはじまる多くのオブジェクト指向言語では クラス は特別なキーワードとして扱われてきました。そして、それは 型 と呼ばれる概念とほぼ一対一対応しています。
この慣習がまず 型クラス という言葉をわかりづらくしてしまっているように思います。そこで一旦、OOPL での型やクラスという概念を忘れて、本来的な言葉の定義を考え直してみます。
型 は
けい【型】[漢字項目]の意味 - goo国語辞書 - goo辞書 1. 同形のものをいくつも作るとき元になるもの。いがた。「原型・紙型・母型」 2. 基準となる形。タイプ。「型式/定型・典型・模型・類型」
となっており、鋳型等、同じ形をしたものを作るもの・その枠といった意味です。もうちょっとプログラム的に言えば 一定の決まったデータ構造の約束 と言えそうです。
一方 クラス は
Weblio - class (共通の性質を有する)部類、種類、(学校の)クラス、学級、組、(クラスの)授業(時間)、(編物教室などの)講習、クラスの生徒たち、同期卒業生、同年兵
となっており、共通の性質をもった集まり であると言えそうです。
つまり、型クラスとは型のクラス = 似たような性質を持った型の集まり、というふうに読み取ることができ、これは実際型クラスをよく説明する表現になっています。
型クラス = 共通の性質を持った型の集まり
というわけで前置きが長くなりましたが、言葉の意味を明らめたところで実際に型クラスとはどういうもので、型クラスが一体何を実現してくれるのか見ていきたいと思います。
型クラスを見て体験する
まずは、型クラスがどんなコードの見た目をしていて何ができるのかを確認してみます。Scala のコードですが、Scala を知らない人でも雰囲気がつかめれば大丈夫ですので、細かいシンタックスは気にせず見てみてください。
まずは、以下のような仕様の実装について考えます。
sum
という List を受け取ってその合計を返す関数を定義したいとします。この際、sum
が引数に受け取る型は、ただ 1 つの List[Int]
などではなく、合計という概念が適用できる型 を幅広く受け取れるようにしたいでしょう。このように多くの型を受け取れることを「多相」と呼びます。
言い換えると、合計という概念が適用できるある型 A
を要素に持つ List[A]
を引数に取る多相なメソッド sum
を作りたい、です。ここで、型 A
は拡張できない or 拡張したくない既存の型とします。
ここでいったん合計という概念の詳細は考えずに、まずは Int
と String
の2つに対応してみましょう。自然に実装できるでしょうか?
sum メソッドのナイーブな実装
def sum(xs: List[Int]): Int = xs match { case Nil => 0 case head :: tail => head + sum(tail) } def sum(xs: List[String]): String = xs match { case Nil => "" case head :: tail => head + sum(tail) } sum(List(1, 2, 3)) // => 6 sum(List("a", "b", "c")) // => "abc" sum(List(true, false)) // Kaboom! Can't compile
型クラスがでてくるかと思いきや拍子抜けしたかもしれませんが、実はオーバーロード / アドホック多相で上記の仕様を満たすことができます。簡単にコードの説明をすると
- 引数の List を
match
(switch
のようなものです) でNil
と それ以外で場合分け Nil
だったら受け付ける型のデフォルト値を返す- List が要素を持つならその先頭の値
head
と List の残りの要素tail
の総和を足す
ということをやっています。
オーバーロードの引数の型として、合計という概念が適用できる型を列挙し、それぞれの実装を与えることで自然に型にそった処理を実現できています。
この実装によって明らかになった性質がいくつかあります。
- 合計という概念が適用できる型を明に列挙することで、それ以外の型で利用しようとした場合は静的にコンパイルエラーにすることができる
- オーバーロードの制約となる型は既存の型であり、既存の型を修正したり拡張したりせずに新たに振る舞いを追加できる
- 合計というセマンティクスは共通だが、型ごとに実装は異なる
同時に、この実装にはいくつか問題を指摘することができるかと思います。
まず、sum(xs: List[Int]): Int
と sum(xs: List[String]): String
でほとんど実装が重複してしまっています。DRY じゃないですね。
また、今の sum
は Int
と String
のみにしか対応できていません。もちろんこれら以外の合計が適用できる型には対応できていないですし、仮に多くの型のオーバーロードを用意したとしても、 sum
の利用者が追加する独自の型に対応することはできません。つまり、拡張性・再利用性が無い状態になってしまっています。
これらの問題を解決して拡張性を得るためには、合計という概念が適用できる型 をきちんと取り扱う必要がありそうです。
そこで少し今の sum
の実装を見直してみます。問題点として挙げた 実装の重複 という観点から、Int
と String
で共通な部分と異なる部分を抽出してみます。
共通な部分は
- リストの要素の たぐり方
- リストが
Nil
だったときには、引数の型のデフォルト値を返す - 引数の型同士の加法を利用した合計の実装
異なる部分は
Int
、String
それぞれのデフォルト値Int
、String
それぞれの加法の具体的な実装
です。
ここから List に内包される 合計という概念が適用できる型 の条件として 加算が定義できる型 であることが言えそうです。このような加算が定義できる型を Addable
な型と呼ぶことにしましょう。そうすると、sum
メソッドは Addable
な性質を定義できる型の集合 を要素として持つ List を受け入れたいと言えます。
それができる すごいオーバーロード が 型クラス ということになります。
早速上記の仕様の型クラスでの実装をみてみましょう。
sum メソッドの型クラスでの実装
def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match { case Nil => addable.unit case head :: tail => addable.add(head, sum(tail)) } trait Addable[A] { val unit: A def add(x: A, y: A): A } implicit object AddableInt extends Addable[Int] { override val unit: Int = 0 override def add(x: Int, y: Int): Int = x + y } implicit object AddableString extends Addable[String] { override val unit: String = "" override def add(x: String, y: String): String = x + y } sum(List(1, 2, 3)) // => 6 sum(List("a", "b", "c")) // => "abc" sum(List(true, false)) // Kaboom! Can't compile
Scala を知らない方は trait
や object
、implicit
等の見慣れないキーワードが出てきてちょっと混乱するかもしれませんが、落ち着いてオーバーロードの実装と見比べてみると概形が見えてくるのではないかと思います。Scala の説明もしながら、順を追って見ていきます。
まず sum
メソッドの実装を 1 ヶ所に集中するために、型パラメータ A
を導入しました。
def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match { case Nil => addable.unit case head :: tail => addable.add(head, sum(tail)) }
早速いくつか Scala 特有の記法があるため簡単に補足します。 def
はメソッドを定義するキーワードで、def hoge(): Piyo
のような形で使います。最後の Piyo
が戻り値の型です。Scala ではメソッドのパラメータを複数の パラメータリスト として記述することができます。なので ()
が 2 つありますが、単に引数が並んでいるだけだと思ってもらって大丈夫です1。2 個目のパラメータリストにあるキーワード implicit
は型クラスを実現する重要な機能で、オーバーロードと同様に 型情報を元にコンパイラが暗黙的に挿入すべき値を解決します。つまり、Addable[A]
の型を見て自動的にその型に適合した addable
が挿入されます。型によって実際に呼び出すメソッドが決まるオーバーロードそっくりですね。
ここで、あらためて sum
の型 A
に関する実装について、共通の部分と異なる部分を再確認してみます。
共通な部分は
- リストの要素の たぐり方
- リストが
Nil
だったときには型A
のデフォルト値を返す - 型
A
同士の加法を利用した合計の実装
異なる部分は
- 型
A
のデフォルト値 - 型
A
の加法の実装
です。
sum[A](xs: List[A])(implicit addable: Addable[A]): A
メソッドには共通の List のたぐり方が実装されています。対して型 A
に共通の振る舞いが sum
の次の Addable[A]
に定義されています。
trait Addable[A] { val unit: A def add(x: A, y: A): A }
trait
というのは実装の持てる interface のようなものです。ここには見ての通り、ある型 A
のデフォルト値が unit
という変数で表され、加法が add(x: A, y: A): A
というシグネチャで宣言されています。ここで宣言した Addable
= 加算が定義できる 性質が、合計という概念を適用できる型の条件でした。
さて、ここまでで sum
に共通な要素を記述することができました。ここからはオーバーロード同様に、合計という概念が適用できる型それぞれについて Addable
の実装を与えていきましょう。
implicit object AddableInt extends Addable[Int] { override val unit: Int = 0 override def add(x: Int, y: Int): Int = x + y } implicit object AddableString extends Addable[String] { override val unit: String = "" override def add(x: String, y: String): String = x + y }
またしても implicit
キーワードが出てきましたが、ここまでくればもう分かる通り、この implicit
は sum
メソッドの引数における implicit
とこの実装を紐付けるための目印になっています。オーバーロードでは、共通のメソッド名で異なる引数型であれば自動的にオーバーロードであることがコンパイラに伝わりますが、implicit
を利用する場合にはコンパイラに明示的に教えてあげる必要があります。
object
というのは シングルトンオブジェクト のことで、Scala では言語組み込みの仕様になっています。object
は定義と同時に型の名前でアクセスできる static なクラスのようなものだと思ってください。このシングルトンオブジェクトで、Int
、String
それぞれの型に固有の処理を実装として与えています。
さて、一通り眺めてきましたが、いまここを読んでいる方は上記のシングルトンオブジェクトの実装がオーバーロードのように見えてきているのではないでしょうか?もう一度、実際に並べて比べてみます。
オーバーロードと型クラスの比較
オーバーロード
def sum(xs: List[Int]): Int = xs match { case Nil => 0 case head :: tail => head + sum(tail) } def sum(xs: List[String]): String = xs match { case Nil => "" case head :: tail => head + sum(tail) }
型クラス
def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match { case Nil => addable.unit case head :: tail => addable.add(head, sum(tail)) } trait Addable[A] { val unit: A def add(x: A, y: A): A } implicit object AddableInt extends Addable[Int] { override val unit: Int = 0 override def add(x: Int, y: Int): Int = x + y } implicit object AddableString extends Addable[String] { override val unit: String = "" override def add(x: String, y: String): String = x + y }
型クラスのほうがちょっと実装が長くなってしまっていますが、オーバーロードの実装で問題の一つだった重複した実装が、ジェネリックな sum
メソッドによって一本化できています。
さらに、オーバーロードのもう一つの問題点だった拡張性についても、型クラスでは改善しています。今、合計という概念が適用できる型として Double
を発見し、Double
についても sum
メソッドが利用したくなったとします。この時、sum
の実装については手を加えずに、sum
を拡張することができます。
implicit object AddableDouble extends Addable[Double] { override val unit: Double = 0.0 override def add(x: Double, y: Double): Double = x + y } sum(List(1.0, 2.0, 3.0)) // => 6.0
さらにさらに、自作の有理数を扱う Rational
クラスをつくったとしましょう。Rational
クラスには Rational
同士を加算する add
メソッドが定義されているとします。この時、sum
の実装については手を加えずに
implicit object AddableRational extends Addable[Rational] { override val unit: Rational = Rational(0, 1) override def add(x: Rational, y: Rational): Rational = x.add(y) } sum(List(Rational(1, 2), Rational(1, 3), Rational(1, 4))) // => 13/12
気持ちよくなってきた。
というわけで型クラスを見てきましたが、もう型クラスについては怖くなくて、オーバーロードのようにすっかり手に馴染むようになってきたのではないでしょうか?
型クラスとは
ここで、型クラスの性質をまとめてみます。
- あるジェネリックなメソッドが 受け入れられる型の集まり を型クラスの実装として明に列挙することで、それ以外の型では静的にコンパイルエラーにすることができる(型クラス制約)
- 型クラス制約を与える型は既存の型でよく、既存の型を修正したり拡張したりせずに新たに振る舞いを追加できる
- 型クラス制約を受ける型の振る舞いは共通だが、型ごとに実装は異なる
- 既存の型クラスに適合させたい型が増えた場合には、その型クラスの実装を提供するだけでよい(たとえば
sum
メソッドに手を入れる必要がない)
これは前述のオーバーロードの満たす性質と近く、より抽象的な / 拡張に対して開いたものになっています。特に、ライブラリ実装者が型クラスを用いることで、利用者はライブラリによってなされた実装に対して手を加えることなくそれを拡張することができます。
実際、今回扱った sum
メソッドでは Double
や Rational
といったすでに存在する一般の型について、sum
メソッドだけでなく型そのものについても直接変更を加えず、後付で Addable
な性質を実装することができました。
これらが、型クラスが便利で、コードをより抽象的で整理されたものにしてくれるといわれる所以ではないかと思います。
そもそも実は、型クラスという仕組み自体が この記事で名前の出せないあの言語 で一般性の高いアドホック多相 / オーバーロードを実現するための仕組みとして導入されました。これについては別記事で扱っていますが、型クラスがちょっとすごいオーバーロード というのは言い過ぎではないはずです。
型クラスがわかってきたところで、よくある 型クラスとインターフェースの違い について考えてみます。
型クラスとインターフェースとの違い
型クラスは、ある型に対するメソッドの存在を約束するという点でインターフェースと似ており、よく比較されています。実際、Scala の型クラスの実装でも trait Addable[A]
のようなかたちで、trait
による抽象化が行われていました。
では、インターフェース(トレイト)と型クラスではなにが違うのか、sum
や Addable
の実装についてインターフェースを用いたサブタイプ多相による実装を考えてみます。
Addable[A]
インターフェースを継承したクラスの値を扱う sum
trait Addable[A] { val value: A def add(x: Addable[A]): Addable[A] } class AddableInt(override val value: Int) extends Addable[Int] { override def add(x: Addable[Int]): Addable[Int] = new AddableInt(value + x.value) } def sum[T](xs: List[Addable[T]]): Addable[T] = xs match { case Nil => null case head :: Nil => head case head :: tail => head.add(sum(tail)) } sum(List(1, 2, 3).map{ new AddableInt(_) }).value // => 6
渡された List が空の場合はデフォルト値が取得できないので null
を返してしまったり、そういう設計のため add
や sum
の引数・戻り型が A
でなく Addable[A]
になってしまっていたり、 List[Int]
を包み直したりなかなか難儀な感じになってしまいましたが、ほぼ似たような実装ができています。が、型に紐づく処理を挿入するためだけにすべての値をインターフェースに適合した型にラップしており無駄が多いし素直じゃないですね。
Java だとこういうときは ストラテジーパターン が自然かもしれません。
AddStrategy[A]
インターフェースをストラテジーとする sum
def sum[A](xs: List[A])(addStrategy: AddStrategy[A]): A = xs match { case Nil => addStrategy.unit case head :: tail => addStrategy.add(head, sum(tail)) } trait AddStrategy[A] { val unit: A def add(x: A, y: A): A } object AddStrategyInt extends AddStrategy[Int] { override val unit: Int = 0 override def add(x: Int, y: Int): Int = x + y } sum(List(1, 2, 3))(AddStrategyInt) // => 6
もうおわかりだと思いますが、これはほぼ完全に型クラスによる実装と同じです。implicit
による自動挿入だけがない状態になっています。
インターフェース・サブタイピング、Scala でできないこと
インターフェースの実装でできていなこと、ひいては Scala でできないことは、結局のところジェネリックなメソッドの型パラメータ A
からスタティックなメソッドが呼べないことです。(既存の型 A
に対して unit
等の拡張ができないというのもありますが...2)
// これはできない def sum[A](xs: List[A]): A = xs match { case Nil => A.unit case head :: tail => A.add(head, sum(tail)) }
Scala は pure な OOP を標榜しており、method は常にそれの所属するインスタンスを必要とします。Scala に static
キーワードがなく、実質的な static
として機能するのがシングルトンオブジェクトである object
であることからも、Scala がメソッドの呼び出しに常にインスタンスを必要とするオブジェクト指向を目指していることがわかります。
そこで、型に紐付いた static なメソッドを呼び出すためのプレースホルダ が implicit object
だったわけです。上記の A
と下記の addable: Addable[A]
が対応していることがわかるかと思います。
// 型クラスによる A の addable への置き換え def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match { case Nil => addable.unit case head :: tail => addable.add(head, sum(tail)) }
型クラスとインターフェースの違いまとめ
まとめると、インターフェースと型クラスは似ているようですが、そもそも目的の違うもので比べるものではない ということです。
インターフェースはそれを継承したものにその 実装を強制する仕組み です。この仕組自体は型クラスの実装でも用いられていました。
一方型クラスは、その約束された実装を 型情報に基づいて自動的に挿入する 仕組みでした。型クラスを実現するためには挿入する実装を約束するために、インターフェースが必要 でした。
型クラスとインターフェースは似たような性質を見かけ上持っているため比較されたりしていますが、そもそも対置して比較できるものではなく、むしろ型クラスがインターフェースに依存している、協調的な関係になっています。
インターフェースでの実装でも示した通り、インターフェース自体は様々な使い方ができ、ポイントだったのは 型に対する実装をデータ型と同じ場所に置くか、違う場所に置いて適宜挿入するか ということでした。
この、型に対する実装をインターフェースで約束して、データ型と違う場所に置いて自動的に挿入してくれる仕組みこそが型クラスの正体でした。
その点でも、型クラスが アドホック多相・オーバーロードのための仕組み = 型情報に基づいて自動的に実装を挿入する仕組み であるということが納得できるかと思います。
長々とやってきましたが、最後に型クラスを知っているとなにが嬉しいかを紹介して終わりにしようと思います。
おまけ - なぜ型クラスとインターフェースが混同されてしまったか
おそらく、型クラスの原点である Haskell において、型クラスがインターフェースの仕組みを暗黙に内包していることが原因ではないかと思います。
前述の通り、型クラスは仕組み上、実装の約束をするためのインターフェースを必ず必要とします。しかし、Haskell はオブジェクト指向な言語ではないため、そもそもインターフェースというオブジェクト指向の仕組みを持ちませんでした。継承とかないですからね。
そのため、型クラス導入の際に、型クラスを実現するキーワード class
が実質的にインターフェースの部分を担うことになりました。
このことが型クラスとインターフェースが混同されて議論される原因の一端ではないかと思っています。おまけおわり。
型クラスを理解すると嬉しいこと
型クラスを理解しているとなにが嬉しいでしょうか。個人的には、ライブラリやフレームワーク等の抽象的なコードを書く側でない限り 型クラスによる実装を書く機会はめったにない と思います。
では、型クラスの知識はムダなのかというとそんなことはありません。ライブラリを作成する際はもちろん、 ライブラリを利用する・コードを読む際にとても役立ちます。
Scala のライブラリでは、標準ライブラリも含めて型クラスによる実装が様々なところに見つけられます。Scala のコレクションのように 高階型 を伴う実装や Json パーサー等の型によって異なる実装を挿入したい場合において型クラスは非常によく用いられているため、それらを利用する際に型クラスの知識があると シグネチャから実装の意図が容易に汲み取れます。
たとえば、Scala の Json パーサーの 1 つである play-json
の Json.fromJson
メソッドのシグネチャを見てみます。
def fromJson[T](json: JsValue)(implicit fjs: Reads[T]): JsResult[T]
型クラスの知識があると、ドキュメントがなくても Reads[T]
型の意図するところ、求められる実装がこのシグネチャからわかるのではないでしょうか?実際にこのメソッドを使うコードを示します。
case class Cat(name: String, age: Int) object Cat { implicit val jsonReads: Reads[Cat] = Json.reads[Cat] // 型クラスの実装の提供 } val json = Json.parse( """{ "name" : "Tama", "age" : 4 }""" ) val cat = Json.fromJson[Cat](json) // ここに implicit に jsonReads が渡されている // --> val cat = Json.fromJson[Cat](json)(jsonReads) と同じ
Json.reads[Cat]
は実はマクロで、Cat
型の実装から Json を Cat
型にパースする Reads[Cat]
のインスタンスを生成してくれます。Cat
型に特殊化した実装を型クラスのインスタンスとして提供しているわけですね。
このように、ユーザー側で定義した型について、型クラスを利用することで型固有の処理をあとから利用者が差し込めるようになっています。型の定義場所にその型のパーサを implicit
に提供することで、実際に Json をパースするところでは型に固有のパーサーを意識しなくて済むようになっています。
というわけで、型クラスを知っていると利用者としてもメリットが有ることがわかったかと思います。自分で積極的に使わなくても、コードを読む際に重宝するという点で、Scala におけるデザインパターンの一つとして捉えても差し支えないでしょう。
どうでしょうか?型クラスがどんなものか具体的に理解できたでしょうか。
本当に型クラスの指すものがこれであっているのか気になる、型クラスの定義自体を知りたい、という場合は型クラスの原点 How to make ad-hoc polymorphism less ad hoc を読んだ話も参照してみてください。
型クラス完全に理解した。
補足1 - sum
メソッドと fold
今回の sum
メソッドの実装は、デフォルト値と加算という処理を使って List の値を走査して単一の戻り値を作っていました。これについて、関数型と呼ばれる言語を扱ったことがある人は fold
が思いついたのではないかと思います。まさに fold
は sum
のようなリスト構造をたぐる処理の一般的表現になっています(より一般性があるのは foldLeft
/ foldRight
ですがここでは簡単のため fold
とします)。
def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1
Scala のコレクションの根幹になっている GenTraversableOnce[+A] にこの定義がありますが、これはデフォルト値 z
と、 A1
型の 2 つの値を取って A1
型の値を返す関数オブジェクトを引数にとっており、それぞれが Addable
の unit
と add
に相当してることがわかると思います。fold
を使って sum
と同様の実装を与えてみます。
List(1, 2, 3).fold(0){ (acc, i) => acc + i } List(1, 2, 3).fold(0){_ + _} List(1, 2, 3).fold(AddableInt.unit){ AddableInt.add }
2 行目は省略記法で、1 行目と意味するところは同じです。fold
では型クラスやオーバーロードよりさらに柔軟に、利用するその場で処理を記述できるようになっています。これは関数を一級市民として扱うことで、関数型という型 の宣言ができるようになり、それがインターフェースとして機能しているわかりやすい例だと思います。3 行目はわかりやすさのため、という名の悪ふざけです。
fold
に対して型クラスが優れている点は、型に応じて自動的に処理が挿入される点ですが、概ねの場合はこういった関数オブジェクトを渡すことでなんとかなるのではないかと実は思っています。実際、ここで挿入される関数オブジェクトは List(1, 2, 3)
から推論される型 Int
により (Int, Int) => Int
であることが静的に解決されるので、型的に安全になっています。
補足2 - Context bound な型クラス制約の書き方
Scala の型クラスについてはもうちょっとオシャレに書く方法が用意されています。Context bound というやつです。
def sum[A: Addable](xs: List[A]): A = xs match { case Nil => implicitly[Addable[A]].unit case head :: tail => implicitly[Addable[A]].add(head, sum(tail)) }
引数として渡されていた (implicit addable: Addable[A])
をなくして、implicitly[Addable[A]]
というかたちで Addable[A]
の実態を参照できるようになりました。おしゃれではあるのですが、見た目が直感的じゃないので賛否両論かもしれません。
参考
Martin Odersky. Poor man's Type Classes Philip Wadler. How to make ad-hoc polymorphism less ad hoc JSON automated mapping