Ciris 〜 純粋関数型な Configuration as Code

コンフィギュレーションが Scala で書ける型安全＋純粋関数型な *Configuration as Code* ライブラリ Ciris について。

## Ciris の動機
そもそも**設定ファイル**をプログラムとは別の言語で書いて別扱いする理由とは何だったか？

「再コンパイルせずにすむ」というのが、よくある答えだと思う。特に、コンパイル済みの実行可能ファイルが配布され、エンドユーザ環境でインストールされるようなプロダクトだと、再コンパイルせずにアプリケーションの挙動を変える需要は確かにある。

ただし設定ファイルがプログラムと同じコードベースでバージョン管理され、レビューされ、デプロイされるような使われ方の場合、事実上ソースコードと変わらないライフサイクルになって「再コンパイル不要」は大きなメリットではなくなる。となると、例えば JSON や HOCON で resources フォルダ下の application.conf などに書いたりしなくても、普通の Scala コードでも良いことにもなる。

Typelevel の [Ciris](https://cir.is/) は、この観点で **configuration file** と **configuration as code**[^configuration_as_code] を対比して、後者を Scala で実装したライブラリになる。

また環境変数や外部サービス[^sensitive_data]などから実行時に値を取得したり、ローカル／テスト／プロダクトなど環境ごとに設定値を切り替えたりする仕組みは依然として必要となるが、これも Ciris で提供される。

さらに Scala であることから、書きっぷりの自由度が高く柔軟に記述でき、既存のライブラリを活用したコンパイル時の型安全も手厚く提供される。

以下、コードを書きながらざっと紹介してみる（※ [ライブラリのバージョンなどはここ](https://gist.github.com/yasuabe/1f3b0546f83149b2a351e67cd8ee4e60)）

## 基本的な使い方
古式にならって Hello World を書いてみる。もしも環境変数 `NAME` に値が設定されていればその名前への挨拶、なければエラーメッセージを標準出力するという仕様にしてみる。

まず従来の設定ファイルに相当するデータを、以下のような `Config` 型で表現する[^excuse1]。この`Config#name` に環境変数 `NAME` を反映させたい。

```scala
case class Config(name: String)
```
以下のような値で、環境変数 `NAME` へのアクセスが表現できる。

```scala
val entry: ConfigEntry[Id, String, String, String] = env[String]("NAME")
```
`entry` の型をあえて日本語にすると、「文脈`Id` で、String型のキーを使って、String型の環境変数を得て、String型にデコードした設定項目」といったものになる。

`env` は環境変数にアクセスしているが、プログラム引数用の `arg`、Java のシステムプロパティ用の `prop` などもある。また `Id` の代りに抽象化した `F[_]` が指定できるピュア版の `envF`、`argF`、`propF` もある（[後述](https://qiita.com/yasuabe2613/items/020d3b3e6030b7e619d0#cats-effect--monix-task-%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA)）。

ちなみに、型注釈を `env[Option[String]]` として `Option#getOrElse` と併用してデフォルト値を指定したり、`env[T]("FOO_KEY").orElse(prop["foo.key"]).orNone` のように[第二候補以降を指定するコード](https://cir.is/docs/basics#configuration-values)も自然な Scala コードで書ける。


また秘匿情報へのアクセス用途で、Kubernetes secret 用に [ciris-kubernetes](https://github.com/ovotech/ciris-kubernetes)、credstash 用の [ciris-credstash](https://github.com/ovotech/ciris-credstash)、AWS Systems Manager パラメータストア用 の [ciris-aws-ssm](https://github.com/ovotech/ciris-aws-ssm) などが、サードパーティから提供されている。

この `entry` を使って、以下のようなコードで `Config` を組み立てる

```scala
val result: ConfigResult[Id, Config] = loadConfig(entry) { name =>
  Config(name)
}
```
`loadConfig` の最初のパラメータリストで `ConfigEntry` を並べて（ここでは簡単のため一個だけ）、次のパラメータで、得られた設定値を使ってコンフィギュレーション全体を組み立てる関数を与える。また (A1, A2,..., Ai) => Z の形の関数ではなく、(A1, A2,..., Ai) => ConfigResult[F, Z] となる関数を渡せる `withValues` メソッドもあり、環境ごとに設定を変える場合などに使われる（[後述](https://qiita.com/yasuabe2613/items/020d3b3e6030b7e619d0#%E7%92%B0%E5%A2%83%E3%81%AE%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88)）。

これを使って最終的に設定情報を扱うコードは以下のように書ける。

```scala
val config: Id[Either[ConfigErrors, Config]] = result.result
val greeting = config.fold(_.message, c => s"Hello, ${c.name}!")
println(greeting)
```

実行すると、もし環境変数 `NAME` に設定値が存在し、例えばそれが "World" なら "Hello, World!"、もし設定されていなければ "- Missing environment variable [NAME]." といったエラーメッセージが標準出力される。

[ソースGist](https://gist.github.com/yasuabe/7b200f90e6a4e8205f36b7af28fcc6fd)

### 環境の切り替え
環境ごとの設定値切り替えも元々の設定ファイルの目的の一つだった。

Ciris では、環境変数 APP_ENV などに実行環境を設定して切り分けるような方針になる。上の Hello World を改変して、APP_ENV=PRODUCTION なら環境変数 NAME の値、そうでなければ固定値 "local" を用いるコンフィギュレーションを書いてみる。

```scala
case class Config(name: String)

val config: ConfigResult[Id, Config] =
  withValues(env[Option[String]]("APP_ENV")) {
    case Some("PRODUCTION") => loadConfig(env[String]("NAME"))(Config)
    case _                  => loadConfig(Config("local"))
  }
def main(args: Array[String]): Unit = {
  val greeting = config.result.fold(_.message, c => s"Hello, ${c.name}!")
  println(greeting)
}
```
ここで上で少し触れた `withValues` を使っている。ただの Scala コードなので如何様にも書ける自由度がある。ちなみに簡単のため、ここでは APP_ENV の型を文字列としたが、[Ciris のドキュメント](https://cir.is/docs/environments#multiple-environments)では列挙値を使うことが推薦されている。

[ソースGist](https://gist.github.com/yasuabe/6481307d649af92972cfa53cfc8a424a)

## Cats Effect / Monix Task との連携
Ciris では *zero dependency* のため、`Apply` や `Sync` など Cats にすでにある型クラスが、自前で実装されているが、ciris-cats, ciris-cats-effect モジュールを組み込むと、Cats版のいくつかの型クラスが Ciris 版の同名型クラスと同等に扱えるようになる。

従って、Cats Effect `Sync` のインスタンスがある型、例えば Cats Effect `IO` や Monix `Task` などは、Ciris の `Sync` としても使えるようになる。

たとえば、上の Hello World の Monix Task 版は下のように書ける。

```scala
object Main extends TaskApp {
  def config[F[_]: CApplicative] = loadConfig(envF[F, String]("NAME"))(Config)
  def program[F[_]: Sync](c: Config) = Sync[F].delay { println(s"Hello, ${c.name}!") }

  def run(args: List[String]): Task[ExitCode] = for {
    r <- config[Task].result
    x <- r.fold(
      e => Task(println(e.message)) as Error,
      c => program[Task](c)         as Success
    )
  } yield x
}
```
当然、`IO` でも同様に書ける。

[ソースGist](https://gist.github.com/yasuabe/e7963184a539f10e84323e04758dcb31)

##  Refined, Squants, Spire, Shapeless との連携
型安全を求めてやまない Scala プログラマとしては、もしもコンフィギュレーションが成功裏に得られたならば、その時点ではもう不正な値が存在し得ないことが、できれば型として表現され保証されていてほしい。Ciris ではそうした型安全を実現するライブラリとの連携モジュールが提供されている。

以降、Scala Worksheet でサンプルを書いてみる。
[ソースGist](https://gist.github.com/yasuabe/41c08c4ea5bb2f6b7be1c6925af739a3)

### Refined
[Refined](https://github.com/fthomas/refined) は型に制約をつけて洗練させるライブラリで、以下のように Ciris と連携できる。

```Scala
type NonSystemPortNumber = Int Refined Interval.Closed[W.`1024`.T, W.`65535`.T]
val rType = ConfigKeyType[String]("refined key")

ConfigSource.fromEntries(rType)("key" -> "123")
  .read("key").decodeValue[NonSystemPortNumber].result
// Left(ConfigErrors(WrongType(key, ConfigKeyType(refined key), Right(123), 123, Refined[Int, Closed[Int(1024),Int(65535)]], Left predicate of (!(123 < 1024) && !(123 > 65535)) failed: Predicate (123 < 1024) did not fail.)))

ConfigSource.fromEntries(rType)("key" -> "8080")
  .read("key").decodeValue[NonSystemPortNumber].result
// Right(8080)
```
例として[このスライド](https://vlovgr.github.io/refined-types/#16)のポート番号の型を参考にした。"123" では範囲外なので Left値になるが、"8080"なら正しく値が得られる。

### Squants
[Squants](https://github.com/typelevel/squants) は、各種の**量と単位**を提供するライブラリで、以下のように Ciris と連携できる。

```scala
val qType = ConfigKeyType[String]("squants key")

ConfigSource.fromEntries(qType)("key" -> "3°C")
  .read("key").decodeValue[Temperature].result
// Right(3.0°C)

ConfigSource.fromEntries(qType)("key" -> "3J")
  .read("key").decodeValue[Temperature].result
// Left(ConfigErrors(WrongType(key, ConfigKeyType(squants key), Right(3J), 3J, Temperature, QuantityParseException: Unable to parse Temperature:3J)))

ConfigSource.fromEntries(qType)("key" -> "3J")
  .read("key").decodeValue[Energy].result
// Right(3.0 J)
```
"3°C" は温度の正しい設定値として解釈されるが、"3J" は温度設定値としては解釈できない。ただしエネルギーとしては "3J"は正しい設定値となる。

### Spire
[Spire](https://github.com/typelevel/spire) は数値型のライブラリで、以下のように Ciris と連携できる。

```scala
val sType = ConfigKeyType[String]("spire key")

ConfigSource.fromEntries(sType)("key" -> "123/456")
  .read("key").decodeValue[Rational].result
// Right(41/152)

ConfigSource.fromEntries(sType)("key" -> "123/0")
  .read("key").decodeValue[Rational].result
// Left(ConfigErrors(WrongType(key, ConfigKeyType(spire key), Right(123/0), 123/0, Rational, IllegalArgumentException: 0 denominator)))
```
"123/456" は有理数の正しい設定値と解釈されるが、"123/0" は不正であると判別される。

### Shapeless
[Shapeless](https://github.com/milessabin/shapeless) と Ciris の連携では以下のように Coproduct が使える。

```scala
val gType = ConfigKeyType[String]("generic key")

ConfigSource.fromEntries(gType)("key" -> "5.0")
  .read("key").decodeValue[Float :+: String :+: CNil].result
// Right(Inl(5.0))

ConfigSource.fromEntries(gType)("key" -> "abc")
  .read("key").decodeValue[Float :+: String :+: CNil].result
// Right(Inr(Inl(abc)))

ConfigSource.fromEntries(gType)("key" -> "123")
  .read("key").decodeValue[String :+: Float :+: CNil].result
// Right(Inl(abc))
```
左から順に見て、最初にマッチした型が使われるらしい。

## 所感・補足
- センシティブな秘匿設定情報が生で**ログ出力**されないようにする仕組みなども提供されている。
- 公式サイトでは、[独自のコンフィギュレーションソース](https://cir.is/docs/supporting-new-sources)や、[独自のデコーディング](https://cir.is/docs/supporting-new-types)の書き方なども解説されている。
- どうしても「設定ファイル」でなければならない縛りが特になければ、この Ciris などを使った configuration as (Scala) code で良いのではないかと思う。
- それでもやはり、JSON や HOCON といった従来の設定ファイルを使うべき事情があるとしたら、Ciris の作者自身の[ブログ記事](https://typelevel.org/blog/2017/06/21/ciris.html)で紹介されている PureConfig + Refined などの手法が良いかもしれない

[^configuration_as_code]: configuration as code の語義を単にバージョンコントロールシステムで管理してデプロイパイプラインに載せる事とする解釈もあるが、ここでは [Ciris 公式](https://cir.is/docs/basics)と同じように 設定ファイルとの対立概念として扱った。
[^sensitive_data]: 接続情報や各種シークレットなど、セキュリティ的にセンシティブな情報の保管先サービス
[^excuse1]: 最小のサンプルだからフィールドが一つしかないが、もちろん実用上は各種設定情報が入る大きなデータ構造になりうる。