Koka と DI
この記事は ITRC Advent Calendar 2021 の9日目の記事です。
Koka について
Koka は非決定性や例外、副作用といった計算エフェクトを言語機能に組み込んだプログラミング言語です。
執筆時点での最新バージョンは 2.3.6 となります。
Koka と DI
実装の抽象化と注入のために DI (Dependency injection) を利用することがあるかと思いますが、
Koka では、エフェクトに対応するハンドラーを定義することで、DI コンテナのように振る舞います。
DI コンテナと比較して、関数スコープ中の任意のタイミングでハンドラーの定義ができることや、
継続を利用して制御フローの変更が可能なことから、より柔軟なプログラムを記述することができます。
今回は、ロギングをエフェクトとして定義し、デバッグログ付きのJSON パーサーを実装します。
STEP 1
はじめに、ロギング用のデータ型とエフェクト型、関数を定義します。
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 ライクなパーサーコンビネーターを利用しています。
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
最後にメイン関数の実装を行います。
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)
がエフェクトハンドラーを定義している場所です。
今回はコンソールに出力していますが、下記のように変更することでファイル出力に変更できます。
with fun write-log(log-type, message)
write-log-file(path("./example.log"), log-type, message)
実行結果
./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 などの近似となる実装は存在するので、その時が楽しみですね。