Migrating from Kotlin to Gossamer

Kotlin and Gossamer share several ideas: null safety maps to Option<T>, val/var maps to let/let mut, when expressions map to match, data classes map to structs, and coroutines map to goroutines. The main shift is from JVM-hosted OO with nullable types to a GC-managed systems language where every absent value is explicit and every blocking call is safe.

TL;DR

  • What transfers: val/varlet/let mut, whenmatch, data classes → structs, sealed classes → enums, lambdas → closures, interfaces → traits, T?Option<T>, coroutines → goroutines.
  • What changes: method-chaining collections vs. iter::* + |>, suspend vs. go expr, string templates vs. format!, companion objects vs. associated functions, exceptions vs. Result<T, E>.
  • What's absent: @ annotations as user-extensible metadata, reflection, JVM interop, inline/reified generics, Kotlin DSLs.

Syntax cheat-sheet

Kotlin Gossamer Notes
val x = 5 let x = 5 Immutable binding.
var x = 5 let mut x = 5 Mutable binding.
fun f(x: Int): Int = x + 1 fn f(x: i64) -> i64 { x + 1 }
{ x: Int -> x + 1 } \|x: i64\| x + 1 Lambda / closure.
if (cond) a else b if cond { a } else { b } No parens required.
when (x) { 1 -> … else -> … } match x { 1 => …, _ => … } Exhaustive.
data class Point(val x: Int, val y: Int) struct Point { x: i64, y: i64 }
sealed class Shape enum Shape { … }
println("$name is $age") println!("{name} is {age}")
"$name".length name.len()
listOf(1, 2, 3) [1, 2, 3] Vec<i64>.
mapOf("a" to 1) HashMap::from([("a", 1)])
x?.method() if let Some(v) = x { v.method() }
x ?: default x.unwrap_or(default)
x!! x.unwrap() Panics on None.
launch { … } go fn() { … }() Goroutine.
async { … }.await channel send/recv, or direct call
try { … } catch (e: …) { … } match f() { Ok(v) => …, Err(e) => … }
throw Exception("msg") return Err(errors::new("msg"))

Null safety → Option

Kotlin's T? has four operators: ?. (safe call), ?: (Elvis), !! (force unwrap), and smart casts after null checks. Gossamer uses Option<T> with explicit unwrapping:

Kotlin:

val len: Int? = name?.length
val display = name ?: "anonymous"
val forced = name!!.uppercase()

Gossamer:

let len: Option<i64> = name.as_ref().map(|s: &String| s.len() as i64)
let display = name.as_deref().unwrap_or("anonymous")
let forced = name.as_ref().unwrap().to_uppercase()

For conditional access, if let is idiomatic:

if let Some(n) = name {
    println!("hello, {n}")
}

Data classes → structs

Kotlin:

data class User(val name: String, val age: Int)
val u = User("Ada", 36)
val older = u.copy(age = 37)

Gossamer:

struct User { name: String, age: i64 }

let u = User { name: "Ada", age: 36 }
let older = User { age: 37, ..u }

Deriving Debug, Clone, and PartialEq is the Gossamer equivalent of data class's auto-generated toString / equals / hashCode:

#[derive(Debug, Clone, PartialEq)]
struct User { name: String, age: i64 }

Sealed classes → enums

Kotlin:

sealed class Result<out T>
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()

when (result) {
    is Success -> println(result.value)
    is Failure -> println(result.error.message)
}

Gossamer uses the built-in Result<T, E> or a user enum:

enum Outcome {
    Win(String),
    Loss(String),
    Draw,
}

match outcome {
    Outcome::Win(msg) => println!("{msg}"),
    Outcome::Loss(msg) => eprintln!("{msg}"),
    Outcome::Draw => println!("tied"),
}

Collections and iteration

Kotlin collections carry .map, .filter, .fold as methods. Gossamer separates these into free functions in std::iter (data last, piped with |>). Mutating helpers (push, sort, remove) stay as methods.

Kotlin:

val total = listOf(1, 2, 3, 4, 5)
    .filter { it % 2 == 0 }
    .sumOf { it * it }

Gossamer:

let total = [1, 2, 3, 4, 5]
    |> iter::filter(|n: i64| n % 2 == 0)
    |> iter::sum_by(|n: i64| n * n)

Kotlin:

val words = sentence.split(" ").map { it.trim() }.filter { it.isNotEmpty() }

Gossamer:

let words = strings::split(&sentence, " ")
    |> iter::map(|s: String| strings::trim(&s))
    |> iter::filter(|s: String| s.len() > 0)
    |> iter::collect()

String interpolation

Kotlin uses "$variable" and "${expression}". Gossamer uses format! / println! with {identifier} or positional {}:

Kotlin:

val msg = "Hello, $name! You are ${age + 1} next year."

Gossamer:

let msg = format!("Hello, {name}! You are {} next year.", age + 1)

Coroutines → goroutines

Kotlin coroutines are cooperative and tied to a CoroutineScope. Gossamer goroutines are stackful and scheduled by an M:N work-stealing runtime — blocking IO doesn't block the OS thread.

Kotlin:

fun main() = runBlocking {
    val result = async { fetchData(url) }
    println(result.await())
}

Gossamer:

fn main() {
    let (tx, rx) = channel()
    go fn() {
        tx.send(fetch_data(&url))
    }()
    println!("{}", rx.recv().unwrap())
}

For fan-out + fan-in, WaitGroup is the Go-shaped idiom:

let wg = sync::WaitGroup::new()
let (tx, rx) = channel()

for url in urls {
    wg.add(1)
    let tx = tx.clone()
    go fn() {
        defer wg.done()
        tx.send(fetch_data(&url))
    }()
}

go fn() {
    wg.wait()
    drop(tx)
}()

while let Some(result) = rx.recv() {
    process(result)
}

Extension functions

Kotlin extension functions add methods to existing types:

fun String.shout(): String = this.uppercase() + "!"

Gossamer uses impl blocks or standalone free functions:

fn shout(s: &String) -> String {
    strings::to_upper(s) + "!"
}

If the method belongs logically to a type you own, use impl:

impl MyType {
    pub fn shout(&self) -> String {
        strings::to_upper(&self.name) + "!"
    }
}

Companion objects → associated functions

Kotlin companion objects hold factory methods and constants:

class Connection private constructor(val host: String) {
    companion object {
        fun local() = Connection("localhost")
        const val DEFAULT_PORT = 5432
    }
}

Gossamer uses associated functions on impl:

struct Connection { host: String }

impl Connection {
    pub fn local() -> Connection {
        Connection { host: "localhost" }
    }
}

const DEFAULT_PORT: i64 = 5432

Exceptions → Result

Kotlin uses exceptions for errors. Gossamer uses Result<T, E> with ? for propagation:

Kotlin:

fun readConfig(path: String): Config {
    val text = File(path).readText()  // throws IOException
    return parseConfig(text)         // throws ParseException
}

Gossamer:

fn read_config(path: &String) -> Result<Config, errors::Error> {
    let text = fs::read_to_string(path)?
    parse_config(&text)
}

? unwraps Ok(v) or returns Err(e) from the enclosing function. Combine errors with errors::wrap:

fn read_config(path: &String) -> Result<Config, errors::Error> {
    let text = fs::read_to_string(path)
        .map_err(|e| errors::wrap(e, format!("reading {path}")))?
    parse_config(&text)
}

Standard library mapping (Kotlin / JVM → Gossamer)

Kotlin / JVM Gossamer
File(path).readText() fs::read_to_string(path)
File(path).writeText(s) fs::write(path, s)
File(path).delete() fs::remove_file(path)
File(path).mkdirs() fs::create_dir_all(path)
System.getenv("X") env::var("X")
System.exit(0) process::exit(0)
ProcessBuilder(cmd).start() process::Command::new(cmd).spawn()
println(x) println!("{x}")
System.currentTimeMillis() time::now().as_millis()
Thread.sleep(ms) time::sleep(ms)
Regex(pattern).matches(s) regex::compile(pattern)?.is_match(&s)
s.split(delim) strings::split(&s, delim)
s.trim() strings::trim(&s)
s.uppercase() strings::to_upper(&s)
s.toInt() strconv::parse_i64(&s)
n.toString() strconv::format_i64(n)
listOf(...) [...] (Vec<T>)
mutableListOf(...) let mut xs = [...]
mapOf(k to v) HashMap::from([(k, v)])
setOf(...) HashSet::from([...])
list.map { } list \|> iter::map(\|x\| …)
list.filter { } list \|> iter::filter(\|x\| …)
list.fold(init) { acc, x -> } list \|> iter::fold(init, \|acc, x\| …)
list.sortedBy { } xs.sort_by(\|a, b\| …)
OkHttp / Ktor HttpClient http::Client::new()
ktor server { } http::serve(addr, handler)
kotlinx.serialization encoding::json::encode(v)
Gson / Jackson encoding::json::decode::<T>(s)
kotlinx.coroutines.launch go fn() { … }()
Mutex() sync::Mutex::new()
CountDownLatch(n) sync::WaitGroup::new()

Cross-references