この投稿の内容および参照先のコードは、Kotlin 1.3.20です。
よく使うあの関数、自動生成されたコードで記述されている
Kotlinの各種コレクションの拡張関数であるmapやfilterなどはstdlib内に定義されています。
さてこの拡張関数が定義されているコードの先頭部分には次のようなことが記載されています。
//
// NOTE: THIS FILE IS AUTO-GENERATED by the GenerateStandardLib.kt
// See: https://github.com/JetBrains/kotlin/tree/master/libraries/stdlib
//
どうやらmapやfilterが定義されているコードは、自動生成されているようです。
上記に記述のある通り、libraries/stdlibを参照してみましょう。ここのReadMeは次のようになっています。
やはり自動生成が行われているようです。こちらの一文に注目してください。
This then runs the script which generates a significant part of stdlib sources from the templates authored with a special kotlin based DSL.
自動生成のコードはKotlinのDSLとなっています。この投稿の残りでは、そのDSLを覗いてみましょう。
stdlibの自動生成のDSLをのぞいてみよう
stdlibのコードの自動生成を行なうDSLは、以下の場所にあります。
エントリーポイントは?
まずはbuild.gradleをみてみましょう。
ここの内容から、エントリーポイントはgenerators.GenerateStandardLibKtであることがわかりました。
次はこのgenerators.GenerateStandardLibKtをみてみましょう。
templatesパッケージには、次のようなTemplateGroupのサブクラスが定義されており、それを列挙しています。
val templateGroups = sequenceOf<TemplateGroup>(
Elements,
Filtering,
Ordering,
ArrayOps,
Snapshots,
Mapping,
SetOps,
Aggregates,
Guards,
Generators,
StringJoinOps,
SequenceOps,
RangeOps,
Numeric,
ComparableOps
)
このFilteringやMapping、SnapshotsなどはDSLを活用して記述されています。
下記のコードで、プラットフォーム別の目的のディレクトリにアウトプットします。
templateGroups.groupByFileAndWrite { (platform, source) ->
// File("build/out/$platform/$source.kt")
when (platform) {
Platform.Common -> commonDir.resolve("_${source.name.capitalize()}.kt")
Platform.JVM -> jvmDir.resolve("_${source.name.capitalize()}Jvm.kt")
Platform.JS -> jsDir.resolve("_${source.name.capitalize()}Js.kt")
Platform.Native -> error("Native is unsupported yet")
}
}
DSLで記述された部分は?
先ほども出てきた次のlibraries/tools/kotlin-stdlib-gen/src/template下にあるクラスは、DSLを用いて記述されています。
- Elements
- Filtering
- Ordering
- ArrayOps
- Snapshots
- Mapping
- SetOps
- Aggregates
- Guards
- Generators
- StringJoinOps
- SequenceOps
- RangeOps
- Numeric
- ComparableOps
それぞれの内容は、関数の系統ごとクラスごとにまとめられています。
たとえば、
- Mappingは、mapやmapNotNullなどの射影とそれに近い処理を行う関数
- Filteringは、filterやtakeWhileなどの選択とそれに近い処理を行う関数
- SetOps(ファイル名はSets、クラス名はSetOps)は、unionやintersectなどの集合演算を行う関数
がまとめられています。
各種コレクション型のfilter関数を生成する部分をみてみましょう。
val f_filter = fn("filter(predicate: (T) -> Boolean)") {
includeDefault()
include(CharSequences, Strings)
} builder {
inline()
doc { "Returns a ${f.mapResult} containing only ${f.element.pluralize()} matching the given [predicate]." }
returns("List<T>")
body {
"""
return filterTo(ArrayList<T>(), predicate)
"""
}
specialFor(Strings, CharSequences) {
returns("SELF")
doc { "Returns a ${f.collection} containing only those characters from the original ${f.collection} that match the given [predicate]." }
body { """return filterTo(StringBuilder(), predicate)${toResult(f)}""" }
}
specialFor(Sequences) {
inline(Inline.No)
returns("Sequence<T>")
body { """return FilteringSequence(this, true, predicate)""" }
}
}
このようにKotlinのDSLを活用して記述されています。
上記のコード中のdocはコメント部を、bodyはメソッドの実装の生成を担っています。
次のコメントを担っている部分に注目してください。Kotlinの言語機能を活用していますね。
doc { "Returns a ${f.mapResult} containing only ${f.element.pluralize()} matching the given [predicate]." }
また、Sequenceに対してfilterメソッドは、inline化を行わないなどの特殊化をする必要があります。そこで上記のコードのようにspecialFor
を使って特殊化をしています。
specialFor(Sequences) {
inline(Inline.No)
returns("Sequence<T>")
body { """return FilteringSequence(this, true, predicate)""" }
}
これらのDSLで記述したコードは、どのような効果があるか簡単に想像できるのではないでしょうか?
DSLの定義は?
KotlinのDSLについては以下の公式ドキュメントを参照してください。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/index.html
https://kotlinlang.org/docs/reference/type-safe-builders.html
https://kotlinlang.org/docs/reference/type-safe-builders.html#scope-control-dslmarker-since-11
特に、Kotlin 1.1から利用可能な@DslMaker
に注目してください。
stdlibのコードジェネレーターDSLの実装は、libraries/tools/kotlin-stdlib-gen/src/templates/dslにあるコード群です。
@DskMaker
を活用しているのが、TemplateDslアノテーションです。
それを活用しているのが次のクラス群です。
まとめ
Kotlinでコーディングをしているときによく使うmapやfilterという関数のコードは、実は自動生成されています。
そしてその自動生成のコードはKotlinの言語機能を活用したDSLで実装されています。
もしあなたが、KotlinでDSLを作ることになったときにもしかしたら参考になるかもしれません。ぜひ覗いてみてください。