type classのcoherenceという観点を中心にして、Scala 3の新しい文法に関して、(Scala 2と比較して)思想やお気持ちを解説した記事をあまり見かけたことがない気がしたので、 特にすごく新規性などがある話ではないはずですが、自分なりに書いてみようと思います。
まず、coherenceという単語や概念についてですが、詳細に正確に説明するのは面倒なので、例えば日本語なら以下などを読んでみてください
書きたいこととしては、
Scala 2でもScala 3でも、捉え方によってはこのあたりのcoherenceに関する機能は大きく変わってないと捉えることもできるし、
微妙に細かい部分が変わっているので、別の捉え方をすると、細かいけど重要な変更がされているのでは?ということを書いていきます。
「coherenceに関する機能は大きく変わってない」というのは、たとえばHaskellなどと比べると、相変わらず強くcompilerによって規制をされているわけではなくて、1つの型に対して複数のtype classのinstanceを定義して使い分けようとすれば、Scala 2までと同等に無理なく出来てしまう、ということです。
少なくとも自分が見た限りのScala 3の公式のdocumentなどで「coherence」という単語はあまり出てこないので、givenやusingをこのように設計した時に、それらをどの程度意識していたのか?はもっと詳細な作成時の議論を追ってみないとわかりません。
https://docs.scala-lang.org/scala3/reference/contextual/index.html
ただ、考古学そのものが目的ではなく 「現在のScala 3の言語仕様をこうやって捉えてみると(深く考えたことがない人にとっては)面白いかもしれないよ」 という話なので、そういう作成者側の詳細な意図や歴史の詳細を探ることも、今回は行いません。
type class自体の定義や、type classのinstance定義という観点において、Scala 3においてはScala 2とはそれなりな互換を保ちつつも、段階的に移行可能な、新たな文法が追加されています。
単にgivenやusingと言っても、type class以外にも ExecutionContext
などを引き回すような用途でも使われますが、今回はあくまでtype classを中心に説明するので、そちらはあまり触れません。
やっとこのあたりから本題に入るのですが、givenやusingが明確にimplicitと異なる点として
「名前の定義を省略出来る」
という点です。公式documentでは Anonymous Givens
という言い方がされていますが、どの程度言語仕様で決まってるのか謎ですが、厳密には Anonymous
というよりは名前定義が省略可能、の方が実際の挙動により近いです。なぜなら省略しても裏で勝手に名前が付与されて、その勝手に生成された名前で参照出来てしまうからです
(個人的にはその場合には名前で参照は出来ないで欲しかった・・・)
具体的にいうと、type classのinstance定義は、Scala 2までのimplicitでは、たとえば以下のように書いていたと思いますが
implicit val intMonoid: Monoid[Int] = new Monoid[Int] { // 以下の実装は略
Scala 3では以下のようになります
// 名前つける場合 given intMonoid: Monoid[Int] = new Monoid[Int] {
// 明示的に名前をつけない場合 given Monoid[Int] = new Monoid[Int] {
名前をつけないことも可能、というだけで、つけれないわけではない、という点は若干ややこしいかもしれません。
上記はvalでの例ですが def
で、引数を取る場合も同様に名前は省略可能です。
さて、なぜ「名前を省略可能」という仕様にしたのでしょうか?
「そんなの省略したいに決まってるだろ!やっとそういう機能が入ったのか!」
などと感じた人は、そもそも、おそらくこの記事を読む必要はありません。
たとえばHaskellにおいてtype classのinstanceを定義する構文において、instanceそのものの名前を定義しますか?しませんというか出来ませんね?なぜそれで普通の用途では困らないのでしょうか?
大まかにはそれと同じ理由なはずです。
他にtype classをほぼ言語組み込みで持ってるような各種言語に詳しくないというか調べるのが面倒なので、あまり多くの具体的な例を出せないのですが、たとえばPureScriptの方がよりScala 3に近いかもしれません。 PureScriptのdocumentを引用するのですが
instance Show Boolean where show true = "true" show false = "false"
If you're wondering, the generated JS code looks like this:
var showBoolean = { show: function (v) { if (v) { return "true"; }; if (!v) { return "false"; }; throw new Error("Failed pattern match at ..."); } };
If you're unhappy with the generated name, you can give names to type class instances. For example:
instance myShowBoolean :: Show Boolean where show true = "true" show false = "false"
PureScriptも、type classのinstanceに名前をつけることも可能、省略することも可能、という仕様です。
「type classのcoherence」と言った場合に厳密な定義があるのか、あったらそれはどういう意味か?は知らないというか、最初からそれ知ってる人にとってはむしろこの記事必要ないので、一旦 「1つの型に対して1つのinstanceのみが存在している」 くらいの言い方にしておきましょう。例としてはなんでも良いのですが、今type classを使う側として
「 Monoid[Int]
のinstanceを使いたい」
とします。
type classではない普通のプログラムにおいては
class Foo // これはtype classではないとする
があったときに
val foo1 = new Foo val foo2 = new Foo
と場合によって複数のinstanceがあったりして、当たり前ですが変数名で参照します。
しかし「1つの型に対して1つのinstanceのみが存在している」「IntのMonoidのinstanceを使いたい」という状況の時に「名前を知らないといけない」「名前をつけないといけない」というのは、よく考えれば単に面倒なだけで、本来は必要がないことではないでしょうか?
使う側は「IntのMonoidの定義があったらここに召喚してくれ!」という気持ちで summon[Monoid[Int]]
と書くのがある意味では標準的な書き方です。名前を指定して使ったりはしません。
( summon
は型だけ指定して使う標準ライブラリのメソッドであって、特定のtype classに限らず任意の全てのtype classで使えるメソッド)
この場合の命名は必然性がないので、極論するというか例えるなら、名前を付けたところでコメント程度の意味にしかなりません。コメントって全ての変数や定義に絶対に書くものではなく、必要なところに必要な量や内容しか書きませんよね?あえて例えるなら感覚としてはそれと近いかもしれません。 (ライブラリとして公開してバイナリ互換考える場合は若干別の観点があるが今回は触れない)
単に "名前" といった場合は、その言葉そのものを考え出すと、いろいろな役割があって、若干プログラムのテクニックというより、極論すると哲学的な思想的な話になってしまいますが、とりあえずプログラムを書く上での変数名、メソッド名、という文脈では、他のものと区別をつけるために名付ける、という側面があるはずです。
しかし、何度も言いますが「1つの型に対して1つのinstanceのみが存在している」ならば、定義するときも、参照するときも名前は必要ない、むしろ名前をつけるだけ面倒、名前は邪魔、とすら捉えることができるかもしれません。
稀によく(?)型がない?型が弱い?型推論が弱い?型が後付けの?言語において
「型を書くのが面倒だ!書きたくない!」
という話題で盛り上がっているのを見かけますが、それと似てるような?対照的なような?逆な?話として
「型さえ記述すればその文脈においてやりたいことというか実態のinstanceは引っ張ってこれるので、定義側でも参照側でも変数などの名前を付与するのが面倒だろ!」
という気持ちです。
この記事で言いたいことは実質とにかくそれが全てです。今まで定義側の例だけを書きましたが、参照する側で引き回す側も
def f[A](a: A)(implicit m: Monoid[A]) = // 実装省略
と、Scala 2までは名前を付与する必要がありました。ただ、type paramと全く同じ場合は
def f[A: Monoid](a: A) =
とcontext boundが使えたので、その場合はScala 2でも名前を付与する必要はありません。しかしこのtype paramと同じものが当てはまるか?は毎回そうなるとは限らないので、やはり名前をつけないといけないことはあります。
しかし、これもScala 3のusingでは
def f[A](a: A)(using Monoid[A]) =
と、名前を省略することが可能です。便利ですね?
実際にtype classを多用するScalaの多数の(まだScala 2でcross buildされている)ライブラリでは、どうせ名前は使われなくてどうでも良いものなので、1文字や2文字の機械的な名前が付けられる慣習が多くありました。
また、名前の省略についてのみ論点を絞って解説してみましたが、givenやusingには他にも微妙に今までのimplicitとは異なる仕様がありますが、それについては解説しません。
既存のScala 2コードをScala 3のスタイルに移行していく場合も、新規でScala 3を書いていく場合でも、出来るだけgivenやusingで名前を省略していくと、この違いを意識せざるを得ないことが多くなり
「あっ、ここimplicitを名前で参照してしまってるけど、これtype classとしてinstance 1つになってるのか?いやそもそもtype classとして設計してるのか?色々微妙なのでは???いっそのこと逆にimplicitやgivenで定義すること自体やめて全部名前で参照するように変えるか?」
といった気づきが増えると思います。
あと、名前の省略とは若干別観点ですが定義側が using
の部分に明示的に引数として渡す場合には呼び出し側にも using
を付与する必要があり、その点もtype classとしての使い方を意識するのには捉え方によっては良い影響かもしれません。
グローバルにinstanceが1つだけならば、わざわざ明示的に渡す必要は普通はないはず?だが、明示的に渡しているということは複数のinstanceを使い分けているのではないか?という気づきになるので。
また、関連して、型だけ指定してimportするという新しい機能もあります。
今までこういう視点があまりなかった人は、coherenceって雑に言ってカッコつけていくとかっこいい感じが出て厨二病感が出るので、無意味にcoherenceって多用していきましょう(?)