Reach One|株式会社ビズリーチ(BizReach)【企業公式ブログ】

株式会社ビズリーチの公式ブログ「Reach One(リーチワン)」です。 Reach Oneでは、ビズリーチのメンバーやプロジェクトの紹介、社内外のイベントレポートなどを通じて、「ビズリーチのイマとこれから」をお伝えします。

REACH ONE

ビズリーチのイマとこれから

世界最大のScalaイベント「Scala Days@CPH」に行ってきた(後編)

f:id:bizreach:20170706143948j:plain

こんにちは。
HRMOS事業部のScalaエンジニア、岩松です。社内ではZ戦士とも呼ばれています。

ただいま5月30日から6月2日にかけて開催された「Scala Days Copenhagen」の参加レポートをお送りしています。今回の記事も前編に引き続き、特に印象に残ったセッションをお伝えしていきます!

前編の記事はこちらreachone.bizreach.co.jp

Class up your config

f:id:bizreach:20170706144205j:plain

Scalaにおける設定ファイルの取り扱いを楽にするライブラリ「case classy」の紹介です。

サービスを開発するにあたってセンシティブな情報をプログラム上で取り扱いたい(コードには直接書きたくない)、実行環境毎にサービスの挙動を変えたいといった事はよくあります。設定ファイルや環境変数、JVM環境のシステムプロパティを用いる事でこれらが実現できますが、設定ファイルに関してScalaでは特にHOCON書式を読み取るTypesafe Configがよく使われています。

このTypesafe Configがどんな使い方をするのか見てみましょう。まず、以下のような設定ファイルを用意します。

foo {
  bar = 1
  baz = "baz"
}

この設定ファイルを読み込むコードが以下のようになります。

val config = ConfigFactory.load()
object FooConfig {
  val bar = config.getInt("foo.bar")
  val baz = config.getString("foo.baz")
}
println(FooConfig.bar)

一見普通のコードに見えますがScalaらしいコードを書きたい場合、いくつか困った事があります。

  • 指定したパラメータがなかった、間違った設定ファイルを当ててしまったときにRuntimeExceptionになってしまう

  • プリミティブ(相当)型だけでなく、任意の型にもマッピングできるようにしたい 

  • 分かりやすいエラーにしたい

  • 関数型らしい実装がしたい

ここで登場するのがcase classyというライブラリです。case classyでは先程のコードを以下のように扱う事ができます。

case class Foo(bar: Int, baz: String)
implicit val decoder = deriveDecoder[Config, Foo]
ConfigDecoder[Foo].atPath("foo").load() match {
  case Right(foo) => println(s"success! => $foo")
  case Left(e) => println(s"error! => $e")
}

これを実行すると…

success! => Foo(1,baz)

というように、Eitherで包んでくれる上、特に設定をしなくてもケースクラスにマッピングしてくれました!もし設定ファイルに不備があったとしても

error! => AtPath(foo,AtPath(bar,Missing))

このようにEitherのLeftかつパラメータ情報を持ったまま扱う事ができるようになりました。項目がない、型が違うといったエラー情報も細かく出してくれるので、だいぶ扱いやすくなっていますね。Typesafe Configを使った場合に比べてかなりScalaらしい書き方ができるようなりました。

Monad transformers down to earth

f:id:bizreach:20170706145228j:plain

現地では「Monad Transformers down to earth」を「モナドトランスフォーマーが地球に落ちる」というちょっと危ない意味で捉えていましたが、後々確認したところ「down to earth」は「しっかりと、ちゃんと」という意味だったため「モナドトランスフォーマーをちゃんと学ぼう」といった意味のタイトルだったことが分かりました。😅

ScalazやCatsを使っていると「OptionT」や「EitherT」といった「~T」という型に出くわす事がよくありますが、これがどういうものなのか分からずに使っている方、もしくは分からないので使わないという方も多いのではと思います。このセッションではなぜモナドトランスフォーマーが必要になるのか丁寧に説明してくれました。

例えばFuture[Option[Int]]といった型の値を変換したい場合mapを使っていくことになりますが、素直に書くと

val future: Future[Option[Int]] = Future.successful(Option(1))
future.map { option =>
  option.map(_ + 1)
}

と2つのmapが必要となってきます。これだけだとあまり気にならないかもしれませんが、以下のようなケースだと冗長ぶりが伝わるかと思います。

val future1: Future[Option[Int]] = Future.successful(Option(1))
val future2: Future[Option[Int]] = Future.successful(Option(2))
val future3: Future[Option[Int]] = Future.successful(Option(3))
// Optionの値を足し合わせたい
future1.flatMap { option1 =>
  future2.flatMap { option2 =>
    future3.map { option3 =>
      option1.flatMap { num1 =>
        option2.flatMap { num2 =>
          option3.map { num3 =>
            num1 + num2 + num3
          }
        }
      }
    }
  }
}
// こういった書き方もできます
for {
  option1 <- future1
  option2 <- future2
  option3 <- future3
} yield for {
  num1 <- option1
  num2 <- option2
  num3 <- option3
} yield num1 + num2 + num3

ネストが深くなってしまいますし、for式に書き直した所で2重の構造になってしまい、見栄えもあまり良くありません。。。しかし、これから紹介するOptionTを使うことで

for {
  num1 <- OptionT(future1)
  num2 <- OptionT(future2)
  num3 <- OptionT(future3)
} yield num1 + num2 + num3

と書けるようになります。だいぶシンプルになりました!
そして、どうやってOptionTが作られているかというところで「Functor」と「Monad」が登場してきます。😨

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

なんだが難しそうに感じますが、それぞれのメソッドはシンプルです。例えば、FutureのFunctorを実装するとこうなります。

new Functor[Future] {
  def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
}

Futureのmapを呼び出すだけなので、そんなに難しいことはありませんね!Monadも同じように作る事ができます。

new Monad[Future] {
  def pure[A](a: A): Future[A] = Future.successful(a)
  def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
  def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f)
}

これらを使ってOptionTを作るとこのような形になります。

case class OptionT[F[_], A](run: F[Option[A]]) {
 def map[B](f: A => B)(implicit functor: Functor[F]): OptionT[F, B] = {
   OptionT(functor.map(run) { option =>
     option.map(f)
   })
 }
 def flatMap[B](f: A => OptionT[F, B])(implicit monad: Monad[F]): OptionT[F, B] = {
   OptionT(monad.flatMap(run) {
     case Some(a) => f(a).run
     case None => {
       val none: Option[B] = None
       monad.pure(None)
     }
   })
 }
}

型パラメータのFはListやSeq、Optionでも良いのですが、OptionTに渡ってきたFがmapやflatMapを持っているかどうかは分かりません。OptionTとしてmapやflatMapを実行するときにFunctorやMonadを渡す事で、これを経由してFのmapやflatMapを呼び出すことができるようになります。これにより、最初に書いたような簡単なfor式で記述する事ができるようになりました!

サンプルコードはこちら

手前味噌となりますが、なぜ~Tが登場するのか、必要となるのかについては同じような発表をこちらでもやっていました。HRMOSスタンバイといったビズリーチのScalaプロダクトでもEitherTはよく使われていますが、実際に自分で作ってみることでより理解が進んだなと感じています!

Building a Company on Scala

f:id:bizreach:20170706150525j:plain

Tapadという最近3億6000万ドル(約400億円!)で売却された企業では2010年ごろからScalaを採用していました。こちらのセッションはここまでにご紹介したものとは少し毛色が異なり、TapadなぜScalaを採用していたのか、何が良かったのか、何に気をつけるべきかを紹介しています。

途中「“FP”(Functional Programming) Mastery is not Required to Master Scala (Scalaをマスターするために関数型プログラミングをマスターする必要はない)」という言葉がありましたが、これはその通りだなと感じました。Scalaは関数型プログラミングとして使わないといけないかのような風潮がありますが、そうではありません。「〜できる」=「〜しなければいけない」ではありませんし、Better Javaでも十分Scalaの特性を活かせるという事ですね。(前項で散々モナドの話をしましたが…🙄)

また、Scalaを使う際に気を付けたい事としてJVMの資産を使える事を忘れないようにしようという事です。「It’s fun to implement RFCs in Scala, but is it productive?」(ScalaでRFCを実装するのは楽しいけど、それって本当に生産的?)とありますが、いわゆる車輪の再発明をするなということですね。鉄腕エンジニア部という車輪の再発明を積極的に行っていく部活動をしているので耳が痛いです😇 (勉強目的なので、普段の開発ではもちろん車輪の再発明は避けています!)

まとめ

名だたるScalaエンジニア、企業が集まるカンファレンスなだけに内容についていけるか不安な気持ちもありましたが、ちゃんと内容を追っていけば自分たちのプロダクトでも使われている技術の話であったり、同じような課題感の共有があったりと内容の理解には意外と苦労しませんでした。

今回の経験をしっかりとプロダクトにも活かし、よりScalaを活用していきたいと思います!

ありがとうございました!

ビズリーチではScalaエンジニアを募集しています

hrmos.co

f:id:bizreach:20160823213758j:plain

この記事を書いたメンバー

岩松 竜也 / Tatsuya Iwamatsu


2015年新卒入社のScalaエンジニア。戦略人事クラウド「HRMOS(ハーモス)」のサーバーアプリケーション開発を担当。AtomでScalaを書くようになり早1年半、"目"型推論が得意になってきた。