JetBrains-made modern programming language
Gaining momentum since Google adopted is as official Android language
Clearly inspired by a mixture of Java, C#, Scala, and Groovy
Born in industry, for the industry
Acts as the “super language” supporting the “write once, build anywhere” approach
Reference: https://kotlinlang.org/docs/reference/mpp-dsl-reference.html
JVM
11
JavaScript (JS)
Native
Reference: https://kotlinlang.org/docs/reference/mpp-dsl-reference.html
Win by MinGW, Linux
iOS, macOS
Android
Building a multi-platform Kotlin project is only supported via Gradle
Gradle is a build automation + dependency management system
Gradle simultaneously enables & constrains the multi-platform workflow
Kotlin enforces strong segregation of the platform-agnostic and platform-specific parts
common main code: platform-agnostic code which only depends on:
common test code: platform-agnostic test code which only depends on:
kotlin.test
or Kotest)T
(e.g. jvm
, js
, etc.)
T
-specific main code: main Kotlin code targeting the T
platform, depending on:
T
-specific standard libraryT
-specific third-party librariesT
-specific test code: test Kotlin code targeting the T
platform, depending on:
T
-specific main codeT
-specific test libraries<root>/
│
├── build.gradle.kts # multi-platform build description
├── settings.gradle.kts # root project settings
│
└── src/
├── commonMain/
│ └── kotlin/ # platform-agnostic Kotlin code here font-weight: normal;
├── commonTest/
│ └── kotlin/ # platform-agnostic test code written in Kotlin here
│
├── jvmMain/
│ ├── java/ # JVM-specific Java code here
│ ├── kotlin/ # JVM-specific Kotlin code here
│ └── resources/ # JVM-specific resources here
├── jvmTest
│ ├── java/ # JVM-specific test code written in Java here
│ ├── kotlin/ # JVM-specific test code written in Kotlin here
│ └── resources/ # JVM-specific test resources here
│
├── jsMain/
│ └── kotlin/ # JS-specific Kotlin code here
├── jsTest/
│ └── kotlin/ # JS-specific Kotlin code here
│
├── <TARGET>Main/
│ └── kotlin/ # <TARGET>-specific Kotlin code here
└── <TARGET>Test/
└── kotlin/ # <TARGET>-specific test code written in Kotlin here
Defines several aspects of the project:
which version of the Kotlin compiler to adopt
plugins {
kotlin("multiplatform") version "1.9.10" // defines plugin and compiler version
}
which repositories should Gradle use when looking for dependencies
repositories {
mavenCentral() // use MCR for downloading dependencies (recommneded)
// other custom repositories here (discouraged)
}
Defines several aspects of the project:
which platforms to target (reference here)
kotlin {
// declares JVM as target
jvm {
withJava() // jvm-specific targets may include java sources
}
// declares JavaScript as target
js {
useCommonJs() // use CommonJS for JS depenencies management
// or useEsModules()
binaries.executable() // enables tasks for Node packages generation
// the target will consist of a Node project (with NodeJS's stdlib)
nodejs {
runTask { /* configure project running in Node here */ }
testRuns { /* configure Node's testing frameworks */ }
}
// alternatively, or additionally to nodejs:
browser { /* ... */ }
}
// other targets here
}
other admissible targets:
android
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(kotlin("stdlib-common"))
implementation("group.of", "multiplatform-library", "X.Y.Z") // or api
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
}
}
val jvmMain by getting {
dependencies {
api(kotlin("stdlib-jdk8"))
implementation("group.of", "jvm-library", "X.Y.Z") // or api
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
val jsMain by getting {
dependencies {
api(kotlin("stdlib-js"))
implementation(npm("npm-module", "X.Y.Z")) // lookup on https://www.npmjs.com
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
}
}
configure Kotlin compiler options
kotlin {
sourceSets.all {
languageSettings.apply {
// provides source compatibility with the specified version of Kotlin.
languageVersion = "1.8" // possible values: "1.4", "1.5", "1.6", "1.7", "1.8", "1.9"
// allows using declarations only from the specified version of Kotlin bundled libraries.
apiVersion = "1.8" // possible values: "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9"
// enables the specified language feature
enableLanguageFeature("InlineClasses") // language feature name
// allow using the specified opt-in
optIn("kotlin.ExperimentalUnsignedTypes") // annotation FQ-name
// enables/disable progressive mode
progressiveMode = true // false by default
}
}
}
details about:
plugins {
kotlin("multiplatform") version "1.9.10"
}
repositories {
mavenCentral()
}
kotlin {
jvm {
withJava()
}
js {
nodejs {
runTask { /* ... */ }
testRuns { /* ... */ }
}
// alternatively, or additionally to nodejs:
browser { /* ... */ }
}
sourceSets {
val commonMain by getting {
dependencies {
api(kotlin("stdlib-common"))
implementation("group.of", "multiplatform-library", "X.Y.Z") // or api
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
}
}
val jvmMain by getting {
dependencies {
api(kotlin("stdlib-jdk8"))
implementation("group.of", "jvm-library", "X.Y.Z") // or api
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
val jsMain by getting {
dependencies {
api(kotlin("stdlib-js"))
implementation(npm("npm-module", "X.Y.Z")) // lookup on https://www.npmjs.com
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
all {
languageVersion = "1.8"
apiVersion = "1.8"
enableLanguageFeature("InlineClasses")
optIn("kotlin.ExperimentalUnsignedTypes")
progressiveMode = true // false by default
}
}
}
Let T
denote the target name (e.g. jvm
, js
, etc.)
<T>MainClasses
compiles the main code for platform T
jvmMainClasses
, jsMainClasses
<T>TestClasses
compiles the test code for platform T
jvmTestClasses
, jsTestClasses
<T>Jar
compiles the main code for platform T
and produces a JAR out of it
jvmJar
, jsJar
<T>MainClasses
, but NOT <T>TestClasses
<T>Test
executes tests for platform T
jvmTest
, jsTest
<T>MainClasses
, AND on <T>TestClasses
compileProductionExecutableKotlinJs
compiles the JS main code into a Node project
js
target to be enabledbinaries.executable()
configuration to be enabledassemble
creates all JARs (hence compiling for main code for all platforms)
test
executes tests for all platforms
check
like test
but it may also include other checks (e.g. style) if configured
build
$\approx$ check
+ assemble
Ordinary Kotlin sources
When in common
:
Common
)expect
keyword@JvmStatic
, @JsName
, etc.Let T
denote some target platform
When in T
-specific source sets
T
-specific std-libT
-specific Kotlin librariesT
-specific librariesactual
keywordEach platform T
may allow for specific keywords
external
modifierexpect
/actual
mechanism for specialising common APIDeclaring an expect
ed function / type declaration in common code…
… enforces the existence of a corresponding actual
definition for all platforms
Draw a platform-agnostic design for your domain entities
expect
keyword to declare platform-agnostic factoriesAssess the abstraction gap, for each target platform
For each target platform:
actual
keyword to implement platform-specific factoriesCSV (comma-separated values) files, e.g.:
# HEADER_1, HEADER_2, HEADER_3, ...
# character '#' denotes the beginning of a single-line comment
# first line conventionally denotes columns names (headers)
field1, filed2, field3, ...
# character ',' acts as field separator
"field with spaces", "another field, with comma", "yet another field", ...
# character '"' acts as field delimiter
# other characters may be used to denote comments, separators, or delimiters
JVM and JS std-lib do not provide direct support for CSV
Requirements:
We’ll follow a domain-driven approach:
Domain entities (can be realised as common code):
Table
: in-memory representation of a CSV fileRow
: generic row in a CSV fileHeader
: special row containing the names of the columnsRecord
: special row containing the values of a line in a CSV fileMain functionalities (require platform specific code):
Table
programmaticallyTable
Table
Table
into a CSV fileRow
interface// Represents a single row in a CSV file.
interface Row : Iterable<String> {
// Gets the value of the field at the given index.
operator fun get(index: Int): String
// Gets the number of fields in this row.
val size: Int
}
Header
interface// Represents the header of a CSV file.
// A header is a special row containing the names of the columns.
interface Header : Row {
// Gets the names of the columns.
val columns: List<String>
// Checks whether the given column name is present in this header.
operator fun contains(column: String): Boolean
// Gets the index of the given column name.
fun indexOf(column: String): Int
}
Record
interface// Represents a record in a CSV file.
interface Record : Row {
// Gets the header of this record (useful to know column names).
val header: Header
// Gets the values of the fields in this record.
val values: List<String>
// Checks whether the given value is present in this record.
operator fun contains(value: String): Boolean
// Gets the value of the field in the given column.
operator fun get(column: String): String
}
Table
interface// Represents a table (i.e. an in-memory representation of a CSV file).
interface Table : Iterable<Row> {
// Gets the header of this table (useful to know column names).
val header: Header
// Gets the records in this table.
val records: List<Record>
// Gets the row at the given index.
operator fun get(row: Int): Row
// Gets the number of rows in this table.
val size: Int
}
AbstractRow
class// Base implementation for the Row interface.
internal abstract class AbstractRow(protected open val values: List<String>) : Row {
override val size: Int
get() = values.size
override fun get(index: Int): String = values[index]
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
return values == (other as AbstractRow).values
}
override fun hashCode(): Int = values.hashCode()
// Returns a string representation of this row as "<type>(field1, field2, ...)".
protected fun toString(type: String?): String {
var prefix = ""
var suffix = ""
if (type != null) {
prefix = "$type("
suffix = ")"
}
return values.joinToString(", ", prefix, suffix) { "\"$it\"" }
}
// Returns a string representation of this row as "Row(field1, field2, ...)".
override fun toString(): String = toString("Row")
// Makes it possible to iterate over the fields of this row, via for-each loops.
override fun iterator(): Iterator<String> = values.iterator()
}
DefaultHeader
class// Default implementation for the Header interface.
internal class DefaultHeader(columns: Iterable<String>) : Header, AbstractRow(columns.toList()) {
// Cache of column indexes, for faster lookup.
private val indexesByName = columns.mapIndexed { index, name -> name to index }.toMap()
override val columns: List<String>
get() = values
override fun contains(column: String): Boolean = column in indexesByName.keys
override fun indexOf(column: String): Int = indexesByName[column] ?: -1
override fun iterator(): Iterator<String> = values.iterator()
override fun toString(): String = toString("Header")
}
DefaultRecord
classinternal class DefaultRecord(override val header: Header, values: Iterable<String>) : Record, AbstractRow(values.toList()) {
init {
require(header.size == super.values.size) {
"Inconsistent amount of values (${super.values.size}) w.r.t. to header size (${header.size})"
}
}
override fun contains(value: String): Boolean = values.contains(value)
override val values: List<String>
get() = super.values
override fun get(column: String): String =
header.indexOf(column).takeIf { it in 0..< size }?.let { values[it] }
?: throw NoSuchElementException("No such column: $column")
override fun toString(): String = toString("Record")
}
DefaultTable
classinternal class DefaultTable(override val header: Header, records: Iterable<Record>) : Table {
// Lazy, defensive copy of the records.
override val records: List<Record> by lazy { records.toList() }
override fun get(row: Int): Row = if (row == 0) header else records[row - 1]
override val size: Int
get() = records.size + 1
override fun iterator(): Iterator<Row> = (sequenceOf(header) + records.asSequence()).iterator()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is DefaultTable) return false
if (header != other.header) return false
if (records != other.records) return false
return true
}
override fun hashCode(): Int {
var result = header.hashCode()
result = 31 * result + records.hashCode()
return result
}
override fun toString(): String = this.joinToString(", ", "Table(", ")")
}
To enforce separation among API and implementation code, it’s better:
Convention in Kotlin is to create factory methods as package-level fun
ctions
io/github/gciatto/csv/Csv.kt
fileFactory methods may be named after the concept they create: <concept>Of(args...)
headerOf(columns...)
, recordOf(header, values...)
, etc.Class diagram:
Csv.kt
file// Headers creation from columns names
fun headerOf(columns: Iterable<String>): Header = DefaultHeader(columns)
fun headerOf(vararg columns: String): Header = headerOf(columns.asIterable())
// Creates anonymous headers, with columns named after their index
fun anonymousHeader(size: Int): Header = headerOf((0 ..< size).map { it.toString() })
// Records creation from header and values
fun recordOf(header: Header, columns: Iterable<String>): Record = DefaultRecord(header, columns)
fun recordOf(header: Header, vararg columns: String): Record = recordOf(header, columns.asIterable())
// Tables creation from header and records
fun tableOf(header: Header, records: Iterable<Record>): Table = DefaultTable(header, records)
fun tableOf(header: Header, vararg records: Record): Table = tableOf(header, records.asIterable())
// Tables creation from rows (anonymous header if none is provided)
fun tableOf(rows: Iterable<Row>): Table {
val records = mutableListOf<Record>()
var header: Header? = null
for (row in rows) {
when (row) {
is Header -> header = row
is Record -> records.add(row)
}
}
require(header != null || records.isNotEmpty())
return tableOf(header ?: anonymousHeader(records[0].size), records)
}
Iterable
and vararg
argumentsDummy instances object:
object Tables {
val irisShortHeader = headerOf("sepal_length", "sepal_width", "petal_length", "petal_width", "class")
val irisLongHeader = headerOf("sepal length in cm", "sepal width, in cm", "petal length", "petal width", "class")
fun iris(header: Header): Table = tableOf(
header,
recordOf(header, "5.1", "3.5", "1.4", "0.2", "Iris-setosa"),
recordOf(header, "4.9", "3.0", "1.4", "0.2", "Iris-setosa"),
recordOf(header, "4.7", "3.2", "1.3", "0.2", "Iris-setosa")
)
}
Some basic tests in file TestCSV.kt
, e.g. (bad way of writing test methods, don’t do this at home)
@Test
fun recordBasics() {
val record = Tables.iris(Tables.irisShortHeader).records[0]
assertEquals("5.1", record[0])
assertEquals("5.1", record["sepal_length"])
assertEquals("0.2", record[3])
assertEquals("0.2", record["petal_width"])
assertEquals("Iris-setosa", record[4])
assertEquals("Iris-setosa", record["class"])
assertFailsWith<IndexOutOfBoundsException> { record[5] }
assertFailsWith<NoSuchElementException> { record["missing"] }
}
Run tests via Gradle task test
(also try to run tests for specific platforms, e.g. jvmTest
or jsTest
)
We designed a set of domain entities
Row
, Header
, Record
, Table
We implemented the domain entities as common code
We wrote some unit tests for the domain entities
Let’s focus now on more complex functionalities
Table
Table
into a CSV fileI/O is platform-specific, hence we need platform-specific code
I/O functionalities are supported by fairly different API in JVM and JS
java.io
package vs. JS’ fs
moduleDo we need to rewrite the same business logic twice? (once for JVM, once for JS)
Let’s try to decompose the problem as follows:
Table
: file $\rightarrow$ string(s) $\rightarrow$ Table
Table
as CSV file: Table
$\rightarrow$ string(s) $\rightarrow$ fileRemarks:
Table
” part is platform-agnostic
Configuration
: set of characters to be used to parse / represent CSV files
Formatter
: converts Rows
into strings, according to some Configuration
Parser
: converts some source into a Table
, according to some Configuration
Configuration
classdata class Configuration(val separator: Char, val delimiter: Char, val comment: Char) {
// Checks whether the given string is a comment (i.e. starts with the comment character).
fun isComment(string: String): Boolean = // ...
// Gets the content of the comment line (i.e. removes the initial comment character).
fun getComment(string: String): String? = // ...
// Checks whether the given string is a record (i.e. it contains the delimiter character)
fun isRecord(string: String): Boolean = // ...
// Retrieves the fields in the given record (i.e. splits the string at the separator character)
fun getFields(string: String): List<String> = // ...
// Checks whether the given string is a header (i.e. simultaneously a record and a comment)
fun isHeader(string: String): Boolean = // ...
// Retrieves the column names in the given header
fun getColumnNames(string: String): List<String> = // ...
}
Formatter
interfaceinterface Formatter {
// The source of this formatter (i.e. the rows to be formatted).
val source: Iterable<Row>
// The configuration of this formatter (i.e. the characters to be used).
val configuration: Configuration
// Formats the source of this formatter into a sequence of strings (one per each row in the source)
fun format(): Iterable<String>
}
Parser
interfaceinterface Parser {
// The source to be parsed by this parser (must be interpretable as string)
val source: Any
// The configuration of this parser (i.e. the characters to be used).
val configuration: Configuration
// Parses the source of this parser into a sequence of rows (one per each row in the source)
fun parse(): Iterable<Row>
}
DefaultFormatter
classclass DefaultFormatter(override val source: Iterable<Row>, override val configuration: Configuration) : Formatter {
// Lazily converts each row from the source into a string, according to the configuration.
override fun format(): Iterable<String> = source.asSequence().map(this::formatRow).asIterable()
// Converts the given row into a string, according to the configuration.
private fun formatRow(row: Row): String = when (row) {
is Header -> formatAsHeader(row)
else -> formatAsRecord(row)
}
// Formats the given row as a header (putting the comment character at the beginning).
private fun formatAsHeader(row: Row): String = "${configuration.comment} ${formatAsRecord(row)}"
// Formats the given row as a record (using the separator and delimiter characters accordingly).
private fun formatAsRecord(row: Row): String =
row.joinToString("${configuration.separator} ") {
val delimiter = configuration.delimiter
"$delimiter$it$delimiter"
}
}
AbstractParser
classclass AbstractParser(override val source: Any, override val configuration: Configuration) : Parser {
// Empty methods to be overridden by sub-classes to initialize/finalise parsing.
protected open fun beforeParsing() { /* does nothing by default */ }
protected open fun afterParsing() { /* does nothing by default */ }
// Template method that parses the source into a sequence of strings (one per line).
protected abstract fun sourceAsLines(): Sequence<String>
// Parses the source into a sequence of rows (skipping comments, looking for at most 1 header).
override fun parse(): Iterable<Row> = sequence {
beforeParsing()
var header: Header? = null
var i = 0
for (line in sourceAsLines()) {
if (line.isBlank()) {
continue
} else if (configuration.isHeader(line)) {
if (header == null) {
header = headerOf(configuration.getColumnNames(line))
yield(header)
}
} else if (configuration.isComment(line)) {
continue
} else if (configuration.isRecord(line)) {
val fields = configuration.getFields(line)
if (header == null) {
header = anonymousHeader(fields.size)
yield(header)
}
try {
yield(recordOf(header, fields))
} catch (e: IllegalArgumentException) {
throw IllegalStateException("Invalid CSV at line $i: $line", e)
}
} else {
error("Invalid CSV line at $i: $line")
}
i++
}
afterParsing()
}.asIterable()
}
StringParser
classclass StringParser(override val source: String, configuration: Configuration)
: AbstractParser(source, configuration) {
// Splits the source string into lines.
override fun sourceAsLines(): Sequence<String> = source.lineSequence()
}
Additions to the Csv.kt
file:
const val DEFAULT_SEPARATOR = ','
const val DEFAULT_DELIMITER = '"'
const val DEFAULT_COMMENT = '#'
// Converts the current container of rows into a CSV string, using the given characters.
fun Iterable<Row>.formatAsCSV(
separator: Char = DEFAULT_SEPARATOR,
delimiter: Char = DEFAULT_DELIMITER,
comment: Char = DEFAULT_COMMENT
): String = DefaultFormatter(this, Configuration(separator, delimiter, comment)).format().joinToString("\n")
// Parses the current CSV string into a table, using the given characters.
fun String.parseAsCSV(
separator: Char = DEFAULT_SEPARATOR,
delimiter: Char = DEFAULT_DELIMITER,
comment: Char = DEFAULT_COMMENT
): Table = StringParser(this, Configuration(separator, delimiter, comment)).parse().let(::tableOf)
object CsvStrings {
val iris: String = """
|# sepal_length, sepal_width, petal_length, petal_width, class
|5.1,3.5,1.4,0.2,Iris-setosa
|4.9,3.0,1.4,0.2,Iris-setosa
|4.7,3.2,1.3,0.2,Iris-setosa
""".trimMargin()
val irisWellFormatted: String = """
|# "sepal_length", "sepal_width", "petal_length", "petal_width", "class"
|"5.1", "3.5", "1.4", "0.2", "Iris-setosa"
|"4.9", "3.0", "1.4", "0.2", "Iris-setosa"
|"4.7", "3.2", "1.3", "0.2", "Iris-setosa"
""".trimMargin()
// other dummy constants here
}
Tests involving parsing be like:
@Test
fun parsingFromCleanString() {
val parsed: Table = CsvStrings.iris.parseAsCSV()
assertEquals(
expected = Tables.iris(Tables.irisShortHeader),
actual = parsed
)
}
Tests involving formatting be like:
@Test
fun formattingToString() {
val iris: Table = Tables.iris(Tables.irisShortHeader)
assertEquals(
expected = CsvStrings.irisWellFormatted,
actual = iris.formatAsCSV()
)
}
Further additions to the Csv.kt
file:
// Reads and parses a CSV file from the given path, using the given characters.
expect fun parseCsvFile(
path: String,
separator: Char = DEFAULT_SEPARATOR,
delimiter: Char = DEFAULT_DELIMITER,
comment: Char = DEFAULT_COMMENT
): Table
expect
keyword, and the the lack of function bodyString
as a platform-agnostic representation of paths
I/O (over textual files) is mainly supported by means of the following classes:
Buffered readers support reading a file line-by-line
On Kotlin/JVM, Java’s std-lib is available as Kotlin’s std-lib
We may simply create a new sub-type of AbstractParser
File
s as sourcesBufferedReader
behind the scenes to read files line-by-lineIn the jvmMain
source set
let’s define the following JVM-specific class:
class FileParser(
override val source: File, // source is now forced to be a File
configuration: Configuration
) : AbstractParser(source, configuration) {
// Lately initialised reader, corresponding to source
private lateinit var reader: BufferedReader
// Opens the source file, hence initialising the reader, before each parsing
override fun beforeParsing() { reader = source.bufferedReader() }
// Closes the reader, after each parsing
override fun afterParsing() { reader.close() }
// Lazily reads the source file line-by-line
override fun sourceAsLines(): Sequence<String> = reader.lines().asSequence()
}
let’s create the Csv.jvm.kt
file containing:
actual fun parseCsvFile(
path: String,
separator: Char,
delimiter: Char,
comment: Char
): Table = FileParser(File(path), Configuration(separator, delimiter, comment)).parse().let(::tableOf)
parseCsvFile
is implemented on the JVMactual
keyword, and the presence of a function bodyFileParser
in the function body
FileParser
is internal class for filling the abstraction gap on the JVMI/O (over textual files) is mainly supported by means of the following things:
These function supports reading / writing a file in one shot
On Kotlin/JS, Node’s std-lib is not directly available
external
declarations)To-do list for Kotlin/JS:
external
declarations for the Node’s std-lib functions to be usedStringParser
(after reading the whole file)In the jsMain
source set
let’s create the NodeFs.kt
file (containing external
declarations for the fs
module):
@file:JsModule("fs")
@file:JsNonModule
package io.github.gciatto.csv
// Kotlin mapping for: https://nodejs.org/api/fs.html#fsreadfilesyncpath-options
external fun readFileSync(path: String, options: dynamic = definedExternally): String
// Kotlin mapping for: https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options
external fun writeFileSync(file: String, data: String, options: dynamic = definedExternally)
@JsModule
annotation instructs the compiler about where to look up for the fs
moduleexternal
declarations are Kotlin signatures of JS functiondynamic
is a special type that can be used to represent any JS object
external
stuffdefinedExternally
is stating that a parameter is optional (default value is defined in JS)let’s create the Csv.js.kt
file containing:
actual fun parseCsvFile(
path: String,
separator: Char,
delimiter: Char,
comment: Char
): Table = readFileSync(path, js("{encoding: 'utf8'}")).parseAsCSV(separator, delimiter, comment)
parseCsvFile
is implemented on JSactual
keyword, and the presence of a function bodyreadFileSync
to read a file as a string in one shotTable
(namely, parseAsCSV
)js("...")
magic function
readFileSync
We may test our CSV library in common-code
The test suite may:
The hard part is step 1: two platform-specific steps
file Utils.kt
in commonTest
:
// Creates a temporary file with the given name, extension, and content, and returns its path.
expect fun createTempFile(name: String, extension: String, content: String): String
notice the expect
ed function
file Utils.jvm.kt
in jvmTest
:
import java.io.File
actual fun createTempFile(name: String, extension: String, content: String): String
val file = File.createTempFile(name, extension)
file.writeText(content)
return file.absolutePath
}
File.createTempFile
file Utils.js.kt
in jsTest
:
private val Math: dynamic by lazy { js("Math") }
@JsModule("os") @JsNonModule
external fun tmpdir(): String
actual fun createTempFile(name: String, extension: String, content: String): String {
val tag = Math.random().toString().replace(".", "")
val path = "$tmpDirectory/$name-$tag.$extension"
writeFileSync(path, content)
return path
}
file CsvFiles.kt
in commonMain
:
object CsvFiles {
// Path of the temporary file containing the string CsvStrings.iris
// (file lazily created upon first usage).
val iris: String by lazy { createTempFile("iris.csv", CsvStrings.iris) }
// Path of the temporary file containing the string CsvStrings.irisWellFormatted
// (file lazily created upon first usage).
val irisWellFormatted: String by lazy {
createTempFile("irisWellFormatted.csv", CsvStrings.irisWellFormatted)
}
// other paths here, corresponding to other constants in CsvStrings
}
tests for parsing be like:
@Test
fun testParseIris() {
val parsedFromString = CsvStrings.iris.parseAsCSV()
val readFromFile = parseCsvFile(CsvFiles.iris)
assertEquals(parsedFromString, readFromFile)
}
Kotlin multi-platform projects can be assembled as JARs
The jvmMain
source set is compiled into a JVM-compliant JARs
*Jar
or assemble
tasksThe jsMain
source set is compiled into either
.klib
), enabling imporing the project tas dependency in Kotlin/JS projects
*Jar
or assemble
taskscompileProductionExecutableKotlinJs
taskTask for JVM-only compilation of re-usable packages: jvmJar
Effect:
$PROJ_DIR/build/libs/$PROJ_NAME-jvm-$PROJ_VERSION
The JAR does not contain dependencies
Ad-hoc Gradle plugins/code is needed for creating fat Jar
Task for JS-only compilation of re-usable packages: compileProductionExecutableKotlinJs
Effect:
$ROOT_PROJ_DIR/build/js/packages/$ROOT_PROJ_NAME-$PROJ_NAME
:
package.json
fileSupport for packing as NPM package via the npm-publish
Gradle plugin
Kotlin code can be called from the target platforms’ main languages
Understading the mapping among Kotlin and other languages is key
How are Kotlin’s syntactical categories mapped to other platforms/languages?
class MyClass {}
interface MyType {}
public class MyClass {}
public interface MyType {}
Int
$\leftrightarrow$ int
/ Integer
, Short
$\leftrightarrow$ short
/ Short
, etc.val x: Int = 0
val y: Int? = null
int x = 0;
Integer y = null;
Any
$\rightarrow$ Object
, kotlin.collections.List
$\rightarrow$ java.util.List
, etc.val x: Any = "ciao"
val y: kotlin.collections.List<Int> = listOf(1, 2, 3)
val z: kotlin.collections.MutableList<String> = mutableListOf("a", "b", "c")
Object x = "ciao";
java.util.List<Integer> y = java.util.Arrays.asList(1, 2, 3);
java.util.List<String> z = java.util.List.of("a", "b", "c");
interface MyType {
val x: Int
var y: String
}
public interface MyType {
int getX();
String getY(); void setY(String y);
}
JvmField
annotation is adoptedimport kotlin.jvm.JvmField
class MyType {
@JvmField
val x: Int = 0
@JvmField
var y: String = ""
}
public class MyType {
public final int x = 0;
public String y = "";
}
X.kt
are mapped to static methods of class XKt
// file MyFile.kt
fun f() {}
public static class MyFileKt {
public static void f() {}
}
JvmName
annotation is exploited@file:JvmName("MyModule")
import kotlin.jvm.JvmName
fun f() {}
public class MyModule {
public static void f() {}
}
object X
is mapped to Java class X
with
INSTANCE
to accessobject MySingleton {}
public static class MySingleton {
private MySingleton() {}
public static final MySingleton INSTANCE = new MySingleton();
}
X
’s companion object is mapped to public static final field named Companion
on class X
class MyType {
private constructor()
companion object {
fun of(): MyType = MyType()
}
}
// usage:
val x: MyType = MyType.of()
public class MyType {
private MyType() {}
public static final class Companion {
public MyType of() { return new MyType(); }
}
public static final Companion Companion = new Companion();
}
// usage:
MyType x = MyType.Companion.of();
X
’s companion object’s member M
tagged with @JvmStatic
is mapped to static member M
on class X
import kotlin.jvm.JvmStatic
class MyType {
private constructor()
companion object {
@JvmStatic
fun of(): MyType = MyType()
}
}
// usage:
val x: MyType = MyType.of()
public class MyType {
private MyType() {}
public static MyType of() { return new MyType(); }
}
// usage:
MyType x = MyType.of();
fun f(vararg xs: Int) {
val ys: Array<Int> = xs
}
void f(int... xs) {
Integer[] ys = xs;
}
class MyType { }
fun MyType.myMethod() {}
// usage:
val x = MyType()
x.myMethod() // postfix
public class MyType {}
public void myMethod(MyType self) {}
// usage:
MyType x = new MyType();
myMethod(x); // prefix
import kotlin.sequences.*
val x = (0 ..< 10).asSequence() // 0, 1, 2, ..., 9
.filter { it % 2 == 0 } // 0, 2, 4, ..., 8
.map { it + 1 } // 1, 3, 5, ..., 9
.sum() // 25
import static kotlin.sequences.SequencesKt.*;
int x = sumOfInt(
map(
filter(
asSequence(
new IntRange(0, 9).iterator()
),
it -> it % 2 == 0
),
it -> it + 1
)
);
fun f(a: Int = 1, b: Int = 2, c: Int = 3) = // ...
// usage:
f(b = 5)
void f(int a, int b, int c) { /* ... */ }
// usage:
f(1, 5, 3);
@JvmOverloads
to generate overloaded methods for Java
import kotlin.jvm.JvmOverloads
@JvmOverloads
fun f(a: Int = 1, b: Int = 2, c: Int = 3) = // ...
void f(int a, int b, int c) { /* ... */ }
void f(int a, int b) { f(a, b, 3); }
void f(int a) { f(a, 2, 3); }
void f() { f(1, 2, 3); }
// missing overloads:
// void f(int a, int c) { f(a, 2, c); }
// void f(int b, int c) { f(1, b, c); }
// void f(int c) { f(1, 2, c); }
// void f(int b) { f(1, b, 3); }
Compile our CSV lib and import it as a dependency in a novel Java library
Alternatively, add Java sources to the jvmTest
source set, and use the library
Listen to the teacher presenting key points
Disclaimer: the generated JS code is not really meant to be read by humans
@JsExport
annotation
kotlin.js.ExperimentalJsExport
opt-in@file:Suppress("NON_EXPORTABLE_TYPE")
package my.package
import kotlin.js.JsExport
import kotlin.js.JsName
@JsExport
class MyType {
val nonExportableType: Long
}
@JsExport
fun myFunction() = // ...
var module = require("project-name");
var MyType = module.my.package.MyType;
var myFunction = module.my.package.myFunction;
@JsName
annotation can be used to control the name of a member
package my.package
@JsName("sumNumbers")
fun sum(vararg numbers: Int): Int = // ...
@JsName("sumIterableOfNumbers")
fun sum(numbers: Iterable<Int>): Int = // ...
@JsName("sumSequenceOfNumbers")
fun sum(numbers: Sequence<Int>): Int = // ...
var module = require("project-name");
var sumNumbers = module.my.package.sumNumbers;
var sumIterableOfNumbers = module.my.package.sumIterableOfNumbers;
var sumSequenceOfNumbers = module.my.package.sumSequenceOfNumbers;
class MyClass(private val argument: String) {
@JsName("method")
fun method(): String = argument + "!"
}
function MyClass(argument) {
this.randomName_1 = argument
}
MyClass.prototype.method = function () {
return this.randomName_1 + "!"
}
Kotlin numeric types, except for kotlin.Long
, are mapped to JavaScript Number
kotlin.Char
is mapped to JS Number
representing character code.
Kotlin preserves overflow semantics for kotlin.Int
, kotlin.Byte
, kotlin.Short
, kotlin.Char
and kotlin.Long
kotlin.Long
is not mapped to any JS object, as there is no 64-bit integer number type in JS. It is emulated by a Kotlin class
kotlin.String
is mapped to JS String
kotlin.Any
is mapped to JS Object
(new Object()
, {}
, and so on)
kotlin.Array
is mapped to JS Array
Kotlin collections (List
, Set
, Map
, and so on) are not mapped to any specific JS type
kotlin.Throwable
is mapped to JS Error
practical consequence: no way to distinguish numbers by type
val x: Int = 23
val y: Any = x
println(y as Float) // fails on JVM, works on JS
Kotlin’s dynamic
overrides the type system, and it is translated “1-to-1”
val string1: String = "some string"
string1.missingMethod() // compilation error
val string2: dynamic = "some string"
string2.missingMethod() // compilation ok, runtime error
kotlin
Companion objects are treated similarly to Kotlin/JVM
Extension methods are treated similarly to Kotlin/JVM
Variadic functions are compiled to JS functions accepting an array
fun f(vararg xs: String) = // ...
// usage:
f("a")
f("a", "b")
f("a", "b", "c")
// usage:
f(["a"])
f(["a", "b"])
f(["a", "b", "c"])
Conceptual workflow:
O
(e.g. Win, Mac, Linux):
T
:
V
of the platform T
(e.g. LTS releases + latest)
<T>MainClasses
)<T>TestClasses
)<T>Test
)master
branch)
T
$\cup$ kotlin-multiplatform
:
R
of platform T
Even more complicated, because release on Maven Central is complex
Setting up a CI/CD pipeline of this kind requires a lot of work
Recommendations:
Takeaway: each platform has some preferred main repository where users expect to find packages onto
JVM
$\rightarrow$ Maven Central Repository (MCR)
Kotlin / Multiplatform $\rightarrow$ MCR
JS
$\rightarrow$ NPM
Android
$\rightarrow$ Google Play
Mac
/ iOS
$\rightarrow$ App Store
Windows
$\rightarrow$ Microsoft Store
Linux
$\rightarrow$ depends on the distro
Python
$\rightarrow$ PyPI
.Net
$\rightarrow$ NuGet
giovanni.ciatto@unibo.it
Compiled on: 2024-02-20