今回はFP in Scalaの2nd edition用のリポジトリを見ていて気づいたScala3の構文の使い方について、調べたことのまとめです。
extensionについて
Scala3のextension
は、既存のクラスに後からメソッドが追加できる構文です。
Extension Methods | Scala 3 Language Reference | Scala Documentation
つまり、Scala2におけるenrich my libraryパターン
の新しい書き方、というわけです。
https://scala-text..io/scala_text/implicit.html
同じimplicit
というキーワードが色々な意味で使われるようになってしまったので、Scala3を契機に再整理されました。
リポジトリの例では、型パラメータを取るクラスに対して特定の型を指定したメソッドだけをコンパニオンオブジェクト内にextension
で用意しておくことで、その型の時だけメソッド呼び出しのコンパイルが成功する、という使われ方でした。
実際のコードは以下のような内容です(実際のコードはTree
型に対するものでしたが)。
package example enum List[+A]: case Cons(h: A, t: List[A]) case Nil def foldRight[B](z: B, f: (A, B) => B): B = this match case Nil => z case Cons(x, xs) => f(x, xs.foldRight(z, f)) end List object List: extension (l: List[Int]) def sum: Int = l.foldRight(0, (x, y) => x + y) end List
Int
のListに対して呼び出すと、当たり前ですが、コンパイルが通って「3」という結果が表示されます。
import List.* @main def main() = val list = Cons(1, Cons(2, Nil)) println(list.sum)
List[String]
に対して呼び出そうとしても型が合わないので、コンパイルエラーになります。
import List.* @main def main() = val list = Cons("one", Cons("two", Nil)) println(list.sum)
-- [E008] Not Found Error: /xxx/ListExtension.scala:25:15 ------ 25 | println(list.sum) | ^^^^^^^^ | value sum is not a member of example.List[String]. | An extension method was tried, but could not be fully constructed: | | example.List.sum(list) failed with | | Found: (list : example.List[String]) | Required: example.List[Int] 1 error found Error: Errors encountered during compilation
コンパニオンオブジェクトの中にまとめておけば、明示的なインポート無しに使える点が便利です。
もちろんScala2においても従来のimplicit構文を使って、下記のように書けます。
package example sealed trait List[+A] { def foldRight[B](z: B, f: (A, B) => B): B = this match { case Nil => z case Cons(x, xs) => f(x, xs.foldRight(z, f)) } } case class Cons[+A](h: A, t: List[A]) extends List[A] case object Nil extends List[Nothing] implicit class IntListOps(l: List[Int]) { def sum: Int = l.foldRight(0, (x, y) => x + y) }
しかし、コードのまとまりとしての分かりやすさ、余計な記述量が減っている点でextension
を使った書き方の方が良いですね。あと、どうしてもIntListOps
のような、定義だけしてどこにも出てこない名称があるのが微妙でした。
また、型が違う時のエラーメッセージも不親切です。
-- [E008] Not Found Error: /xxx/ListExtension2.scala:20:15 ----- 20 | println(list.sum) | ^^^^^^^^ | value sum is not a member of example.Cons[String] 1 error found Error: Errors encountered during compilation
特定の型の時だけに適用されるメソッドを定義する方法としては、extension
を使ったやり方の方がスマートですね。言われてみればそうか、という使い方ですが、公式ドキュメントにはそのものずばりの書き方がされていなかったので、取り上げてみました。
メソッドをどこに配置するか、その汎用性や、限定性によって、必ずしもすべて一つのクラスの中に書かなくても済む点がScalaの良いところです。ただ、それで記述量が増えてしまうのは嬉しくないので、シンプルにまとまって書けるScala3のextension
はいいですね!
opaque typeについて
opaque typeはScala3から導入されたtype aliasの新しい仕組みです。
導入された経緯などは、公式ドキュメントを読んでください。
Opaque Type Aliases | Scala 3 Language Reference | Scala Documentation
先ほどと同じように第2版向けのリポジトリで、この機能が使われています。
第1版では、状態を示すState
の定義は、以下のようになっていました。
case class State[S,+A](run: S => (A, S))
しかし、実際には単にS => (A, S)
をラップしたいだけなので、できればcase class
は作りたくありません。一方で単なるtype aliasでは独自のメソッドは定義できません。
そこで、opaque typeを使います。
object State: extension [S, A](underlying: State[S, A]) def run(s: S): (A, S) = underlying(s) def map[B](f: A => B): State[S, B] = ???
これでState
に独自のメソッドであるmap
を保たせられるようになりました。余計なクラス階層も無くなり、オーバーヘッドも無くなりました。
型のエイリアスを作りたくなるのは、ラッパークラスを作ることで階層構造が複雑化したり、余計なオブジェクト生成に関するオーバーヘッドを避けたい時、あとは型定義が複雑でシンプルに見せたい時(FP in Scalaの例はまさにその例ですね)だと思いますが、最後の例が公式ドキュメントでは触れられていないので、リポジトリのコードを読むことで理解できました。