はじめに
こんにちは。日経 ID 認証認可基盤チームの奥田・淵脇です。この記事は Nikkei Advent Calendar 2022 の記念すべき 1 日目の記事です。
日経 ID チームで開発している認証認可基盤ではバックエンドの技術スタックとして Kotlin + SpringBoot を用いています。最近この基盤に対し Kotlin の関数型プログラミング用のライブラリである Arrow-kt を導入しました。関数型プログラミングには様々な要素がありますが、既存のコードに適用しやすいところから始めようということで、Either
を用いた例外処理を中心に導入してみました。
そこで本記事ではサーバサイド Kotlin における関数型プログラミングの導入事例として、日経 ID チームにおける Either
の使われ方や実際のプロダクションコードでの規約などを紹介いたします。
Either 入門
まずは軽く Either
そのものについて説明したいと思います。
基本
Either<A, B>
は A
または B
のどちらかの型の値を保持する型で、Arrow-kt においては下記のように sealed class
で表現されています。これは一般的には失敗しうる処理の戻り値の型として使われ、 Right
には成功した結果の値を、 Left
には失敗の原因等を格納します。
sealed class Either<out A, out B> {
data class Left<A>(val value: A): Either<A, Nothing>
data class Right<B>(val value: B): Either<Nothing, B>
}
簡単な例として、整数の割り算を考えてみます。ここで divide
は割り算に成功した場合はその結果の Int
を格納し、失敗した場合にはエラーメッセージとして String
を格納します。divide
の結果を受け取った main
関数は、結果が Right
か Left
かで出力を変化させています。
import arrow.core.Either
fun main() {
val a: Int = readLine()?.toInt()!!
val b: Int = readLine()?.toInt()!!
val result = divide(a, b)
when (result) {
is Either.Right -> {
println("割り算に成功しました: ${result.value}")
}
is Either.Left -> {
println("割り算に失敗しました。 message = ${result.value}")
}
}
}
fun divide(a: Int, b: Int): Either<String, Int> {
return if (b != 0) {
Either.Right(a / b)
} else {
Either.Left("divided by zero")
}
}
fun input(): Int {
return readLine()?.toInt()!!
}
基本的な処理
map
Either
の値が Right
だった場合には引数のラムダ式による変換を行い、 Left
だった場合には何もしません。リストの各要素を別の値に変換していく map
の親戚のような関数です。
// 割り算に成功した場合、その結果を 2 倍にする。
fun main() {
val result1 = divide(10, 5).map { it * 2 }
println(result1) // Either.Right(4)
val result2 = divide(10, 0).map { it * 2 }
println(result2) // Either.Left(divided by zero)
}
mapLeft
map
とは逆に、Either
の値が Left
だった場合にのみ引数のラムダ式による変換を行います。
// 割り算の結果が失敗だった場合、エラーメッセージを編集する
fun main() {
val result1 = divide(10, 5).mapLeft { "Calculation Failed. message = $it" }
println(result1) // Either.Right(2)
val result2 = divide(10, 0).mapLeft { "Calculation Failed. message = $it" }
println(result2) // Either.Left(Calculation Failed. message = divided by zero)
}
flatMap, bind
大体の場合は処理に成功していた場合は、その値を用いて次の処理をしたいはずです。そして、次に行う処理もまた失敗しうる場合があります。こんなとき、単なる map
を使うと次のようになります。

つまり、 Either
で包まれた値に対して Either
を返す関数を適用するので、結果は Either
が入れ子になってしまうのです。これでは演算を繰り返すたびに Either
の入れ子がどんどん深くなってしまいます。
そんなときのために以下の動作をする flatMap
があります。
- 最初の処理が失敗していれば、そのエラーを
Left
として返す - 最初の処理が成功していれば、その値を用いて続きの失敗しうる処理
f
を行う。f
が成功すればその値をRight
として返す。f
が失敗すれば、そのエラーをLeft
として返す。
この動作は定義を見たほうがわかりやすいかもしれません。
inline fun <A, B, C> Either<A, B>.flatMap(f: (B) -> Either<A, C>): Either<A, C> =
when (this) {
is Right -> f(this.value)
is Left -> this
}
イメージで書くとこんな感じで入れ子が解消されています。

これなら常に Either
1 個だけの文脈で計算を続けることができシンプルになりました。例として、まず a/b
を計算し、成功した場合はさらに c
で割る、という計算をしてみましょう。
fun main() {
val a: Int = input()
val b: Int = input()
val c: Int = input()
val result: Either<String, Int> = divide(a, b).flatMap { v ->
divide(v, c)
}
println(result)
// (a, b, c) = (54, 2, 3) => Either.Right(9)
// (a, b, c) = (54, 0, 3) => Either.Left(divided by zero), ここでは最初の divide で失敗している。
// (a, b, c) = (54, 2, 0) => Either.Left(divided by zero), ここでは flatMap の中で行った divide で失敗している。
}
このように flatMap
の便利さがわかるかなと思います。しかしまだ少し問題があります。それは Either
の値が複数あるときに、それらを組み合わせるとネストがどんどん深くなっていくというものです。
例えば a/b
, c/d
, e/f
, g/h
をそれぞれ計算し、全て成功した場合にそれらの和を取りたい場合、下記のようなコードになります。
fun main() {
// a~h の宣言は省略
val result: Either<String, Int> = divide(a, b).flatMap { r1 ->
divide(c, d).flatMap { r2 ->
divide(e, f).flatMap { r3 ->
divide(g, h).map { r4 ->
r1 + r2 + r3 + r4
}
}
}
}
}
そこで、より簡潔な記述が出来る糖衣構文が用意されています。
either.eager
で開始できる EagerEffectScope
の中では Either
に対して bind()
を行う事ができ、その際に下記の動作をします:
Left
だった場合はそのエラーをそのままLeft
としてeither.eager
全体の戻り値として返却します。Right
だった場合は、成功した値を取り出して処理を続行します。
こうすると、Either
ではない普通の値を返却する関数を使って処理を書いていくのと同じ感覚で Either
の値を処理する事ができます。
fun main() {
// a~h の宣言は省略
val result: Either<String, Int> = either.eager {
val r1: Int = divide(a, b).bind()
val r2: Int = divide(c, d).bind()
val r3: Int = divide(e, f).bind()
val r4: Int = divide(g, h).bind()
r1 + r2 + r3 + r4
}
}
Result を使わない理由
Kotlin では Either
と似たような機能をもつ Result
が標準で用意されていますが、主に 2 つの理由から Either
のほうが使い勝手が良いと考えています。
失敗の場合の型を指定できず Throwable で固定される
例えば下記の関数で失敗時に返却される例外は実際には ArithmeticException
に限られますが、 Result
は失敗時の型を Throwable
として持つため、ArithmeticException
以外が格納されている場合の処理を書かざるを得ません。
fun divideResult(a: Int, b: Int): Result<Int> {
return if (b != 0) {
Result.success(a / b)
} else {
Result.failure(ArithmeticException())
}
}
Either の bind のような機構がない
Result
には Either
の bind
にあたるものが用意されていません。よって複数の Result
の値を組み合わせたい場合には flatMap
をネストさせることになります。
実際のコーディング規約
Either
を実際に日経 ID の開発で適用するためにコーディング規約を整備しました。その内容をご紹介したいと思います。
- 例外が発生しうる関数の戻り値の型として
Either
を用いる。ここでEither
を返す関数ごとにLeft
として返す例外を定義し、それらはException
を継承させる。 - ただし、予期せぬ例外 (主にクライアントにはステータスコード 500 を返す場合) は
throw
し、Either
を用いた処理は行わない。 - また (ドメイン駆動設計における) アプリケーションサービス層において DB をロールバックしたい場合は例外を
throw
する。
1 は複数の関数で同じ例外を共有しないことを求めています。また Left
の型は Either
の仕様上は任意の型を指定できますが、我々は Exception
を継承した例外クラスを指定しています。これはログ出力用にスタックトレースを取得したいケースがしばしばあるからです。
2 は例えば SQL が不正な場合の例外や、DB との接続に失敗した場合などの例外を全て Either
で処理しているとボイラープレート的なコードが増えるため、ビジネスロジックとは無関係な予期せぬ例外は throw
することでコードをシンプルにすることを狙いにしています。
3 は SpringBoot の @Transactional
と Either
の相性の悪さから来るものです。DB をロールバックしたいときには @Transactional
の仕様により例外を throw
する必要があるため、ここでは Either
は使えません。それでも例外を throw
して良い箇所は限定したいことと、日経 ID ではアプリケーションサービス層でトランザクションを開始していることからこのような規約になりました。主にビジネスロジック上の例外によりクライアントに 400 番台のステータスコードを返したい場合が該当します。
チームに Either や関数型プログラミングを浸透させる
関数型プログラミングは単に Either
を使うだけではなく、乱数や時刻、I/O といった副作用に関する参照透過性などいろいろなエッセンスがあります。日経 ID 認証認可基盤では既存のコードベースも大きく、チームも関数型に慣れてないメンバーが多かったため、新規のコードからチームのスキルアップを兼ねて徐々に浸透させていきました。
チュートリアルの用意
ドメイン駆動設計でよく用いられるコントローラー層やドメイン層といったレイヤードアーキテクチャのスタブを用意し、そこからサンプルシステムを開発するためのチュートリアルを作成しチームメンバーでトライしてみました。その際にドメイン層を関数型プログラミング的に作ってみることで Either
の使い方や参照透過性などの基礎をチームで勉強しました。

必要なところから、徐々に
関数型プログラミングにはたくさんの利点がありますが、既存のコード資産も大量にあるため一気に導入するのは非常にコストがかかります。プロダクト開発においてはすべてが綺麗に設計されているが稼働していないコードよりも、レガシーな積み重ねだが顧客に価値を提供し続けているコードのほうが何倍も重要です。そのため、関数型プログラミングも一気に導入するのではなく必要なところから徐々に導入していくという手法を取りました。そのために行った施策をいくつか紹介いたします。
パッケージの分離
新しいパラダイムを取り入れるべきパッケージの分離です。日経 ID はサーバサイド Kotlin で開発を行っているため、パッケージ構造は Java ほどではありませんが、ある程度ディレクトリ構造に一致しています。そこで、新たに関数型のパラダイムを導入するパッケージを切り、新規開発できる部分についてはその中にコードを書いていくようにしました。
具体的には今まで開発していたディレクトリの中に刷新したコードを書くための <開発コード名> というパッケージを切りその中では関数型のパラダイムを用いるというものです。
src/main
├── kotlin
│ └── com
│ └── nikkei
│ └── id
│ ├── iam
│ │ ├── app
│ │ ├── config
│ │ ├── domain
│ │ ├── ...
│ │ ├── <開発コード名>
│ │ │ ├── app
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── resources
その他のパッケージにあるコードについては無理に導入せずに既存との互換性を保つようにしています。
取り入れるレイヤーの選択
日経 ID ではドメイン駆動設計を用いた開発をしています。ドメイン駆動設計ではアプリケーションの中で関心や責務の分離を疎結合にするためにレイヤードアーキテクチャを取ることが多いです。レイヤードアーキテクチャはパッケージを用いて表現されることが多く、日経 ID でもそのスタイルを取っています。レイヤードアーキテクチャは外界と中でビジネスロジックを表すドメインモデルの円で描かれることが多く、例えば下記のような図はどこかで見たことあるかもしれません。

日経 ID ではこの中でドメインモデルについてのみ関数型プログラミングを導入することにしました。これにはもちろん諸説あり、IO モナドなどを用いてインフラもすべて関数型で開発するべきなどの意見もありましたが、チームで議論した結果、導入しても一番違和感がなく、既存の非関数型パラダイムのコードベースと互換性が保てる点からドメイン層への導入が妥当という結論になりました。またドメイン層は純粋なロジックで構成することが容易であり、日時やネットワーク、DB といった副作用はユースケース層の DI により排除がやりやすかったという点も決め手になりました。
導入する Arrow-kt の機能の選択
Arrow-kt には Either
以外にも Optional
や Validated
、更にモノイドや Lens
圏といった機能やコルーチンやリソース管理の機能も存在します。しかし、 Kotlin はオプショナルチェーンが導入されていたり、イミュータブルな変数が使いやすいなど同じ JVM 言語である Java などに比べて関数型プログラミングに近いことはもとから可能でした。そこで一番の課題となっていたエラー処理に重点をおいて関数型パラダイムを導入するのが効果的という結論になりました。そのため、まずは Either
を中心的に導入し、それ以外は Kotlin の Vanilla な書き方で参照透過性の確保や副作用の排除をすすめることになりました。今後はより複雑なデータ構造やエラー処理などの要件も増えてくると思われるため、 Validated
や Lens
の導入も予定しています。
実際に関数型プログラミングの要素を導入してみてどうだったか
以上の施策はかれこれ 1 年ほど続けてきましたが、下記の効果を実感しています。
- 開発メンバーが Arrow-kt や関数型の考えに親しんできた。
- 時刻の入力や乱数の状態が固定されるなど参照等価なテストを書くようになってきた。
- 単体テストの品質も上がってきた。
- ドメインで起こりうるエラーの処理の漏れがなくなった。
元々はミュータブルな状態を用いた導入例が多かった DDD のアーキテクチャに対してどのように純粋関数的なスタイルを入れていくかなど悩ましい点はいくつかありましたが、全体的には見通しの良いコードになったなと感じており引き続き来年も進めていきたいと考えています。
終わりに
本記事は NIKKEI Advent Calendar 2022 1 日目の記事として、日経 ID における関数型プログラミングの導入についてお話しました。
テクノロジーメディアを目指す日本経済新聞社では、最新のセキュリティ技術を日々キャッチアップし、セキュアなサービスを目指して日々開発しております。 セキュリティの技術を生かしてメディアの未来を作る仕事に興味のある方は、ぜひお気軽にご連絡ください。
明日は 2 日目、齊藤さんによる「OpenWebAdvocacy メンバーとしての活動が官邸 Web サイトに掲載された話」です。お楽しみに!