MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

マイクロアドの新卒エンジニアがCatsに入門してみた

目次

はじめに

こんにちは、19新卒バックエンドエンジニアの飛田です。

マイクロアドでは、プロダクトの一部にCatsという関数型プログラミングを行うためのライブラリを導入しています。 現状では、広告掲載料の算出やログの処理、エラー処理などを中心にCatsを使用していますが、今後、Catsをより多くの用途で使用していく予定です。

というわけで、今回、Catsの初歩的な内容を自分でまとめてみることにしました。

型クラス

型クラスは実装したい機能を表すインターフェースです。Catsにおいて、型クラスは少なくとも1つの型パラメータをとります。 以下のサンプルコード*1は、型クラスを使ったJSON形式のシリアライズを行う機能を表しています。

// JSONの抽象構文木を代数的データ型で表現したもの
sealed trait Json
final case class JsObject(get: Map[String, Json]) extends Json
final case class JsString(get: String) extends Json
final case class JsNumber(get: Double) extends Json
case object JsNull extends Json

// 型クラス
trait JsonWriter[A] {
  def write(value: A): Json
}

上記のJsonWriterを実装すると以下のようになります。型クラスを実装したものを「型クラスのインスタンス」と言い、以下のサンプルコード*2ではstringWriterおよびpersonWriterが「型クラスのインスタンス」に該当します。

final case class Person(name: String, email: String)

object JsonWriterInstances {
  // StringクラスをJsonに変換する
  implicit val stringWriter: JsonWriter[String] =
    new JsonWriter[String] {
        def write(value: String): Json = JsString(value)
    }

  // PersonクラスをJsonに変換する
  implicit val personWriter: JsonWriter[Person] =
    new JsonWriter[Person] {
      def write(value: Person): Json =
        JsObject(Map(
          "name" -> JsString(value.name),
          "email" -> JsString(value.email)
        ))
    }
}

シンタックス

Scalaでは拡張関数を使うことによって既存の型の機能を拡張することができます。 Catsではこの仕組みを使って、シンタックスというものを定義しています。 シンタックスを定義することによって、型クラスを便利に使用することができるようになります。

ここ*3ではJsonSyntaxがシンタックスに該当します。

// シンタックス
object JsonSyntax {
    implicit class JsonWriterOps[A](value: A) {
        def toJson(implicit w: JsonWriter[A]): Json = w.write(value)
    }
}

定義したJsonSyntaxJsonWriterInstancesを実際に使用するためには、両者をインポートして、toJsonを呼び出す必要があります。

import JsonWriterInstances._
import JsonSyntax._

Person("Dave", "dave@example.com").toJson
// 評価結果: JsObject(Map(name -> JsString(Dave), email -> JsString(dave@example.com)))

インタフェースオブジェクト

インタフェースオブジェクトを定義して、型クラスを使用することも可能です。 インタフェースオブジェクトとは、型クラスのインスタンスを引数にとるメソッドを、シングルトンオブジェクトに配置したものです。*4

// インタフェースオブジェクト
object Json {
    def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = 
        w.write(value)
}

このオブジェクトのメソッドは、型クラスのインスタンスをインポートすることによって、利用可能になります。

// 型クラスのインスタンスのインポート
import JsonWriterInstances._

// toJsonメソッドの呼び出し
Json.toJson(Person("Dave", "dave@example.com"))
// 評価結果: JsObject(Map(name -> JsString(Dave), email -> JsString(dave@example.com)))

// Json.toJson(Person("Dave", "dave@example.com"))(personWriter)と動作が等価

implicitlyメソッド

Scalaの標準ライブラリにimplicitlyというメソッドがあります。このメソッドの定義は以下のようになっています。

def implicitly[A](implicit value: A): A = value

一見、このメソッドを利用するメリットはなさそうですが、implicit関連の曖昧なエラーをデバッグする用途などで使えるみたいです。

import JsonWriterInstances._

// JsonWriter[String]がスコープ内に見つからない場合、コンパイルエラーが発生するはず
implicitly[JsonWriter[String]]

Catsの型クラスとインスタンス

以下のコードはCatsのインタフェースシンタックスの使用例です。

import cats.syntax.show._ // Printable type class をインポート
import cats.instances.int._ // Int インスタンスをインポート
println(123.show) // 123

Catsまとめ

Catsでは型クラスと型クラスのインスタンスを用意することによって、既存のオブジェクトに対して、機能を付け足すことができます。

型クラスは実装したい機能を表すインターフェースで、詳細な実装は型クラスのインスタンスで行います。 実際に型クラスの機能を呼び出す際には、シンタックス(またはインタフェースオブジェクト)と型クラスのインスタンスをインポートする必要があります。

考察および雑感

関数型プログラミング(FP)における型クラスと型クラスのインスタンスは、 オブジェクト指向プログラミング(OOP)でいうところのインタフェースと具象クラスとして考えられそうですね。

OOPとFPの比較
OOPとFPの比較

OOPのインタフェースは、実装の詳細を考慮しないAPI的なもので、具象クラスが詳細な実装をもちます。 この関係性は、FPの型クラスおよび型クラスのインスタンスにも当てはまると思いました。

型クラスのメリット

OOP的なやり方だと、オブジェクトがインタフェースの実装を持つため、オブジェクトに新たに機能を追加する際、オブジェクトに実装を加える必要があります。

一方、型クラスには、既存のオブジェクトを一切変更せずに新しく機能が追加できるというメリットがあるように思えました。上記123.showというサンプルコードを挙げましたが、この例ではIntクラスに対して一切の変更が加わっていません。 そのため、Catsの関数型ライクなやり方だと実装をインスタンスがもつため、オブジェクトに対して機能を追加/編集/削除したりすることが容易になりそうですね。

実装の責務のイメージ

OOPとFPの比較
OOPとFPの比較

上記の図は、私なりにOOPと(Catsによる)FPを比較した図です。図中の「オブジェクト」というのはドメインオブジェクトのようなものを想定しており、先ほどの例でいうところのPersonのようなものです。

OOPのオブジェクトとインタフェースはis-aの関係になっていますが、FPに関しても、オブジェクトと型クラスのインスタンスとの関係がis-aのように思えました。 (is-aの関係ではありませんが、implicit修飾子によって、obj.methodでメソッドを呼び出すことができ、上記のサンプルコードですとPersonとJsonWriterの関係性が「Person is a JsonWriter」の関係になっているので、is-aのように思えました。)

OOPではオブジェクトがインターフェースで定義した機能を実装する責務がありますが、FPでは型クラスのインスタンスが詳細な実装をもつ責務があります。そのため、FPの方が「バラバラ感」が強く感じられました。OOPではインタフェースの機能をオブジェクトが中央集権的に取り扱うのに対し、FPでは型クラスの機能を型クラスのインスタンスが自律分散的に持つようなイメージを持ちました。

終わりに

今後、Catsや圏論をしっかり理解して、関数型プログラミング言語をより高度に使いこなして、より良い広告配信システムを作っていきたいと思います。

マイクロアドでは 「広告配信システムをつくりたい!」 「関数型プログラミング言語をガンガン使っていきたい!」 「圏論を理解できるようになりたい!」というエンジニアを積極的に募集しています。 ご興味を持たれた方はこちらから是非ご応募下さい!

www.wantedly.com

参考文献: Scala with Cats