Koka と DI

# Koka と DI 

この記事は [ITRC Advent Calendar 2021](https://adventar.org/calendars/6368) の9日目の記事です。  

## Koka について

Koka は非決定性や例外、副作用といった計算エフェクトを言語機能に組み込んだプログラミング言語です。
執筆時点での最新バージョンは 2.3.6 となります。

## Koka と DI

実装の抽象化と注入のために DI (Dependency injection) を利用することがあるかと思いますが、
Koka では、エフェクトに対応するハンドラーを定義することで、DI コンテナのように振る舞います。

DI コンテナと比較して、関数スコープ中の任意のタイミングでハンドラーの定義ができることや、
継続を利用して制御フローの変更が可能なことから、より柔軟なプログラムを記述することができます。

今回は、ロギングをエフェクトとして定義し、デバッグログ付きのJSON パーサーを実装します。

### STEP 1

はじめに、ロギング用のデータ型とエフェクト型、関数を定義します。

```sh
type log-type
  LogError
  LogWarning
  LogNotification
  LogInformation
  LogDebug

fun show(x : log-type) : string
  match x
    LogError -> "ERROR"
    LogWarning -> "WARNING"
    LogNotification -> "NOTIFICATION"
    LogInformation -> "INFORMATION"
    LogDebug -> "DEBUG"

effect fun write-log(log-type: log-type, message : string) : ()

fun write-log-file(path : path, log-type : log-type, message : string) : _e ()
  write-text-file(path, layout-log(log-type, message), False)

fun write-log-console(log-type : log-type, message : string) : _e ()
  run-system("echo " ++ show(layout-log(log-type, message)))
  ()

fun layout-log(log-type: log-type, message : string) : _e string
  run-system-read("echo -n $(date +%Y-%m-%dT%H:%M:%S)").throw() ++ " [" ++ show(log-type) ++ "] " ++ message
```

上記コードの中にある `_e string` は `エフェクト型 戻り値型` を表し、
`_e` と記述すると関数内で発生するエフェクトを推論してくれます。

### STEP 2

次に JSON パーサーを実装し、適当な位置でログを出力します。
(エスケープ文字や実数などの実装は省略しています)
パースには、標準ライブラリの Parsec ライクなパーサーコンビネーターを利用しています。

```haskell
type json
  JsonNull
  JsonArray(data: list<json>)
  JsonBool(data: bool)
  JsonNumber(data: int)
  JsonObject(data: list<(string, json)>)
  JsonString(data: string)

fun show-json(x : json) : _e string
  match x
    JsonNull ->
      "null"
    JsonArray(data) ->
      "[ " ++ data.map(fn (v) { show-json(v) }).join(", ") ++ " ]"
    JsonBool(data) -> match data
      True -> "true"
      False -> "false"
    JsonNumber(data) ->
      show(data)
    JsonObject(data) ->
      "{ " ++ data.map(fn (kv) { show(kv.fst()) ++ ": " ++ kv.snd().show-json() }).join(", ") ++ " }"
    JsonString(data) ->
      show(data)

fun parse-json() : _e json
  write-log(LogDebug, "parse-json")

  parse-json-value()

fun parse-json-value() : _e json
  parse-json-whitespace()

  choose([
    parse-json-null,
    parse-json-array,
    parse-json-bool,
    parse-json-number,
    parse-json-object,
    parse-json-string
  ])

fun parse-json-whitespace() : _e ()
  many { one-of(" \t\r\n") }

  ()

fun parse-json-null() : _e json
  pstring("null")

  write-log(LogDebug, "parse-json-null")

  JsonNull

fun parse-json-array() : _e json
  char('[')

  write-log(LogDebug, "parse-json-array: begin")

  val list = many
    parse-json-whitespace()

    val v = parse-json-value()

    parse-json-whitespace()

    optional(',') { char(',') }

    v

  char(']')

  write-log(LogDebug, "parse-json-array: end")

  JsonArray(list)

fun parse-json-bool() : _e json
  val v = choose([
    {
      pstring("true")
      
      JsonBool(True)
    },
    {
      pstring("false")
      JsonBool(False)
    }
  ])

  write-log(LogDebug, "parse-json-bool")

  v

fun parse-json-number() : _e json
  val v = pint()

  write-log(LogDebug, "parse-json-number")
  
  JsonNumber(v)

fun parse-json-string() : _e json
  char('"')

  write-log(LogDebug, "parse-json-string")

  val list = many { none-of("\"\r\n") }

  char('"')

  JsonString(string(list))

fun parse-json-object() : _e json
  char('{')

  write-log(LogDebug, "parse-json-object: begin")

  parse-json-whitespace()

  val list = many()
      parse-json-whitespace()

      char('"')

      val k = string(many { none-of("\"\r\n") })

      char('"')

      parse-json-whitespace()

      char(':')

      val v = parse-json-value()

      parse-json-whitespace()

      optional(',') { char(',') }

      (k, v)

  char('}')

  write-log(LogDebug, "parse-json-object: end")

  JsonObject(list)
```

### STEP 3

最後にメイン関数の実装を行います。

```sh
import std/os/env
import std/os/file
import std/os/path
import std/os/process
import std/text/parse

public fun main()
  val args = get-args()

  with fun write-log(log-type, message)
    write-log-console(log-type, message)

  val input = slice(head(args, "null"))

  val json = match parse(input, parse-json)
     ParseOk(x, _) -> x
     ParseError(e, _) -> throw(e)

  write-log(LogDebug, show-json(json))
```

`with fun write-log(log-type, message)` がエフェクトハンドラーを定義している場所です。
今回はコンソールに出力していますが、下記のように変更することでファイル出力に変更できます。

```sh
  with fun write-log(log-type, message)
    write-log-file(path("./example.log"), log-type, message)
```

### 実行結果

```sh
./sample '[ true, 1, "a", { "key": null }]' 

2021-12-09T04:56:46 [DEBUG] parse-json
2021-12-09T04:56:46 [DEBUG] parse-json-array: begin
2021-12-09T04:56:46 [DEBUG] parse-json-bool
2021-12-09T04:56:46 [DEBUG] parse-json-number
2021-12-09T04:56:46 [DEBUG] parse-json-string
2021-12-09T04:56:46 [DEBUG] parse-json-object: begin
2021-12-09T04:56:46 [DEBUG] parse-json-null
2021-12-09T04:56:46 [DEBUG] parse-json-object: end
2021-12-09T04:56:46 [DEBUG] parse-json-array: end
2021-12-09T04:56:46 [DEBUG] [ true, 1, "a", { "key": null } ]
```

## あとがき

一般的なオブジェクト間の依存性注入とは趣が異なりますが、
エフェクトとハンドラーで同様のことが実現できることが確認できました。

また、ハンドラーでは継続による制御フローの変更ができるため、
他の言語における `defer` や `yield` のような言語機能も、 エフェクトとして実装できます。

エフェクトを扱う言語やライブラリはまだまだ発展途上で、プロダクションに現れることは当分ないと思いますが、
React Hooks などの近似となる実装は存在するので、その時が楽しみですね。

## 参考

[The Koka Programming Language](https://koka-lang.github.io/koka/doc/)
[我々向けの Algebraic Effects 入門](https://overreacted.io/ja/algebraic-effects-for-the-rest-of-us/)