ゼロから作るCatsライブラリ ~ implicit編 ~


はじめに

はじめまして。やがです。普段はScala言語を用いて保育園の業務支援SaaSの開発をしています。
Zennは初投稿になります💪

筆者は関数型プログラミングに入門したばかりであるため、なるべく誤りがないよう試行錯誤しながら記事を書いていますが、もし誤っている点等があればコメントをいただけるととても嬉しいです。

Catsライブラリの概要

CatsライブラリはScala言語で使用することができる関数型プログラミングライブラリです。

名前の由来は圏(category)の省略系であると公式ドキュメントに書かれています。関数型プログラミングに大きな影響を与えている圏論との繋がりを感じますね。

Catsライブラリには関数型プログラミングのツールのようなものが多く用意されています。そしてこれらのツールの大部分は型クラスと呼ばれるプログラミングパターンで実装されています。

本記事では型クラスに関する説明は省略します。

本記事の内容

本記事は型クラスをScala言語で実装するための、implicitの機能と実装パターンについてサンプルコードを交えて紹介をしていきます。

implicitは「暗黙的な」というような意味を持つ単語になります。

本記事で紹介する、Scala言語のimplicitの機能と実装パターンのは以下の3つです。

  • implicit parameter
  • implicit conversion
  • Enrich my libraryパターン

以下、順に紹介していきます。

implicit parameter

implicit parameterは、メソッドやクラスの引数を暗黙的に渡す事ができる機能です。

implicit parameterは暗黙的に渡したい引数グループにimplicit修飾子をつける事で使用します。

そしてimplicit修飾子による引数に値を渡すためには、メソッドやクラスを呼び出す処理のスコープ内に、引数の型に対応した暗黙的な値(以下、implicit value)がただ一つ定義されている必要があります。

サンプルコード

例として、implicit修飾子を用いた引数を持つメソッドは以下のように定義します。

scala> def showGreet(name: String)(implicit greet: String): String = s"$greet $name"
// def showGreet(name: String)(implicit greet: String): String

挨拶の文字列を表すgreet引数は暗黙的に渡すため、showGreet("hoge")の形で、name引数のみでメソッドを呼び出すことができます。

実際にメソッドを呼んでみましょう。

scala> showGreet("yaga")
/*              ^
       error: could not find implicit value for parameter greet: String
 */

greet引数のためのimplicit valueが見つからないというコンパイルエラーが出ました。

implicit valueは通常の値定義にimplicit修飾子をつける事で定義します。

scala> implicit val hello: String = "Hello"
// val hello: String = Hello

implicit parameterは引数のに対応するimplicit valueを必要とするため、値の名前が異なっていても問題はありません。

再度、showGreetメソッドを呼んでみます。

scala> showGreet("yaga")
// val res0: String = Hello yaga

今度は正常にメソッドを呼び出す事ができました。

以上がimplicit parameterの機能の概要となります。

implicit修飾子による引数の型に対応するimplicit valueが複数定義されている場合、以下のようなコンパイルエラーが出ます。

scala> implicit val hello: String = "Hello"
// val hello: String = Hello

scala> implicit val goodMorning: String = "Good Morning"
// val goodMorning: String = Good Morning

scala> showGreet("yaga")
/*              ^
       error: ambiguous implicit values:
        both value hello of type String
        and value goodMorning of type String
        match expected type String
 */

その他補足

implicit valuevalだけではなくdefを用いることも可能です。

implicit def foo: String = ...

defを用いると、型パラメータを使用できるようになります。

implicit def foo[T]: Option[T] = ...

implicit valueimplicit parameterを持つことも可能です。

implicit def foo[T](implicit a: T): Option[T] = ...

その他context bound等、説明できていない部分もあるとは思いますが、本記事では省略します。

implicit conversion

implicit conversionimplicit修飾子を持つメソッドの引数の型から返り値の型へと暗黙的な型変換を行う機能です。

サンプルコード

例として、Int型からString型への暗黙的な型変換を行うメソッドを定義します。

scala> implicit def intToString(a: Int): String = a.toString
// def intToString(a: Int): String

そして受け取ったString型の値をそのまま返すメソッドがあるとします。

scala> def receiveString(a: String): String = a
// def receiveString(a: String): String

receiveStringメソッドにInt型の値を渡してみます。通常は引数に与える型がString型ではないため、type mismatchエラーが発生しますが、implicit conversionによりコンパイルが通ります。

scala> receiveString(3)
// val res0: String = 3

implicit conversionは一見便利な機能に見えます。しかし、上記のようにコンパイルエラーを簡単にすり抜ける等の理由から、次に紹介するEnrich my libraryパターン以外での使用は、ずいぶん前から非推奨とされているようです。

Enrich my libraryパターン

Enrich my libraryパターンとはimplicit conversionの機能を用いて、既存クラスの拡張を場当たり的(アドホック) に行う事ができる実装パターンです。

日本語では拡張メソッドという名前で呼ばれています。

サンプルコード

例として、既存のInt型の値にshowメソッドを追加する実装を行っていきます。まず最初にshowメソッドを定義しているShowIntOpsクラスを定義します。

scala> class ShowIntOps(a: Int) {
     |   def show: String = a.toString
     | }
// class ShowIntOps

次にimplciit conversionを用いて、Int型の値をShowIntOpsクラスのインスタンスに型変換するメソッドを定義します。

scala> implicit def syntaxShowInt(a: Int): ShowIntOps = new ShowIntOps(a)
// def syntaxShowInt(a: Int): ShowIntOps

ではshowメソッドを呼んでみましょう。

scala> 3.show
// val res0: String = 3

以上のようにして、Emrich my libraryパターンによる既存クラスの拡張を行う事ができました。

implicit class

scala 2.10以降ではクラス定義にimplicit修飾子をつける事で、上記のサンプルコード実装をさらに簡潔に記述する事ができます。

scala> implicit class ShowIntOps(a: Int) {
     |   def show: String = a.toString
     | }
// class ShowIntOps

scala> 3.show
// val res0: String = 3

Enrich my libraryパターンを実装する場合は、基本的にimplicit classで良いかとは思いますが、implicit classを用いていないライブラリもあるようです(Catsライブラリのcats.syntaxパッケージ内はimplicit classを使用していなさそう)。

そのためどちらの実装方法も読めるようにしておくと良いのかなと思いました。

おわりに

今回はCatsライブラリの基礎となる型クラスを実装するための、Scala言語のimplicitの機能についての説明をしました。

繰り返しになりますが、誤っている点等あればコメントをいただけると嬉しいです🙇‍♂️

モチベーション次第ですが、できれば今後もシリーズとして書いていきたいです。シリーズでやるのであれば次回はSemigroup,Monoid型クラスについての内容にしようかなと考えています。

最後まで読んでいただきありがとうございました!

参考資料

参考資料を以下にまとめます。

https://github.com/typelevel/cats
https://www.scalawithcats.com/dist/scala-with-cats.html
https://www2.slideshare.net/AoiroAoino/scala-79575940
https://scala-text.github.io/scala_text/implicit.html
https://gakuzzzz.github.io/slides/implicit_reintroduction/#1
https://kmizu.hatenablog.com/entry/2017/05/19/074149
https://www2.slideshare.net/MakotoFukuhara/scala-56310825
http://chopl.in/post/2012/11/06/introduction-to-typeclass-with-scala/
http://blog.takeda-soft.jp/blog/show/396.html