はじめに
ここ半年ほどKotlin Multiplatformなコードを書く中で、使って便利だった小技がいくつかあったため共有します。
ログ出力時にそのログの出力元のクラス名を利用する
現在のプロジェクトではロガーについてシンプルなインタフェースを作り、実装をDIで注入するという方式を取っています。
ログの各行にはそれを出力したクラス名くらいは載っていてほしいものですが、ログ出力のたびに出力関数の引数に this::class.simpleName
のようなボイラープレートを渡すのも少々面倒です。
そのため、以下のようなちょっとした工夫をおこない、ログ出力コードの簡便さとログの情報量を両立させています。
まず、ロガーのインタフェースのなかに以下の Attatched
のような、ロガーをプロパティとして持っていることを表すインタフェースを入れ子で用意しておきます。
internal interface Logger {
enum class Severity {
TRACE, DEBUG, INFO, WARN, ERROR, FATAL
}
fun log(severity: Severity, content: () -> String)
interface Attached {
val logger: Logger
}
}
また、適当な場所に以下のようなユーティリティを生やしておきます。
private inline fun log(
logger: Logger,
severity: Logger.Severity,
className: String?,
crossinline content: () -> String
) {
logger.log(severity){"<${className?:"unknown"}>: ${content()}"}
}
internal fun Logger.Attached.putTrace(content: () -> String) {
log(logger, Logger.Severity.TRACE, this::class.simpleName, content)
}
internal fun Logger.Attached.putDebug(content: () -> String) {
log(logger, Logger.Severity.DEBUG, this::class.simpleName, content)
}
// :
// :
こうすることで、以下のように、Logger.Attatched
を実装したクラスの中では、 putDebug {(出力する内容)}
のような記述をするだけで、ログの内容にクラス名を自動的に含められるようになります。
internal class Klass(di: DI): Logger.Attatched {
override val logger: Logger by di.instance()
fun f() {
putDebug { "Hello World!" }
}
}
Kotlin/JVMのリフレクションなどならもう少しスマートな方法があるかもしれませんが、とりあえずこのような小手先のテクニックでも十分役に立っています。
気になる点としては、logger
がクラス外に漏れてしまっている点が挙げられます。 internal
なクラスで使う分には、そのクラスの外側であえてそのクラスの文脈であることを明示したいときに 、Klass().logger.putDebug{}
のように使えて便利な場面もありますが、ライブラリ等の一部としてモジュール外に公開するクラスには使用しない方がいいかもしれません。
Result
周りを少し書きやすくする
kotlinx.coroutineではコルーチン間を跨いだ例外を扱うのが面倒な場合が多いため、自然と(Rustなどでよくやるように)例外も関数の返値として陽に扱うケースが出てくるかと思います。その際に、正常系の値や例外をラップする型として、Kotlin標準のResult
1 や ArrowのEither
を選択する場合が多いかと思います。
現在のプロジェクトでは、一部を除いて Result
を使用することを決めたため、値や例外を Result
に変換するために、Result.success(v)
や Result.failure(e)
というコードが頻出するようになりました。
これは長くて冗長であるため、適当な場所に
@Suppress("NOTHING_TO_INLINE")
inline fun <T> T.success(): Result<T> = Result.success(this)
@Suppress("NOTHING_TO_INLINE")
inline fun <T, F> F.failure(): Result<T> where F: Throwable = Result.failure(this)
のようなユーティリティ関数を書いておき、v.success()
や e.failure()
と書けるようにしました。
今のところこの程度の機能で十分ではありますが、 Result
をよりガッツリ扱いたい場合はkotlin-resultなどのライブラリを使ってみるのも手かもしれません。
Deffered
な値にお手軽にログを仕込む
kotlin.coroutineを使った非同期処理を行う場合、async
などの返値としてDeferred
型の値を扱うことがあるかと思います。
またデバッグのために、こうした値のキャンセル時や例外送出時にログを吐かせたいケースが多々あるかと思います2。
そのような場合、下記のようなコードを適当な場所に置いておき、
internal fun <T> Deferred<T>.attachTracer(
onComplete: () -> Unit,
onCancelled: () -> Unit,
onThrown: (Throwable) -> Unit
): Deferred<T> = apply {
invokeOnCompletion {
when (it) {
null -> onComplete()
is CancellationException -> onCancelled()
else -> onThrown(it)
}
}
}
val v = async { f() }.attachTracer(
onComplete = { putTrace { "f() completed" } },
onThrown = { putError { "error on f(): $it" } },
onCancelled = { putTrace { "f() cancelled" } }
)
上記コードのように attachTracer
をつけ足してやると、コードをあまり変更せずに簡単にログを仕込めて便利です。