PONY λ M2 Modula-2
for Kotlin programmers

You already know Kotlin.Now explore other languages.

Side-by-side, interactive cheatsheets for Kotlin programmers
comparing Kotlin to other languages. Every example runs live in your browser — no setup, no installation.

▶ Start with Go Browse comparisons ↓

Choose your own path by reordering languages

Go Pre-Alpha

Radical simplicity, and the concurrency your coroutines were modeled on. Go deletes most of Kotlin's expressiveness on purpose — no exceptions, no null safety, no inheritance, no default arguments, if is a statement — and offers in exchange a language you can hold in your head, plus the goroutines and channels that Kotlin's launch, Channel, and select pay homage to.

  • Goroutines vs coroutines is the marquee comparison: go func() is launch { } unstructured — no scope owns a goroutine, and sync.WaitGroup does by hand what structured concurrency does automatically
  • Kotlin's Channel and select came from Go — send, receive, close, range-until-closed translate line for line
  • No exceptions: fallible functions return (value, error) and every caller writes if err != nil
  • No T?: nil is back and unchecked — the comma-ok idiom (value, ok := …) is the closest thing to ?.
  • No inheritance and no implements-declarations: struct embedding replaces subclassing, and interfaces are satisfied structurally
  • No map/filter chains — without light lambda syntax the for loop is idiomatic, and unused variables are compile errors
Python Beta ⚡ Works Offline ⚡ Offline

The compiler is gone. Python trades away everything Kotlin's type system buys you — null safety, exhaustive when, a typo caught at build time — for a language that says what it means in a third of the lines. Type hints look just like your annotations and are enforced by nobody; the safety net exists, but you install it yourself.

  • Duck typing replaces interfaces — nothing declares conformance, and a missing method is an AttributeError at runtime, not a compile error
  • Type hints are inert documentation: def double(number: int) happily accepts a string; mypy is an external, opt-in checker
  • No T?, no ?., no ?:, no smart casts — None is unguarded null, and every safe-call chain gets written out by hand
  • Comprehensions replace map/filter chains; range() is half-open where Kotlin's .. is inclusive
  • @dataclass is data class, match/case is when (a statement, non-exhaustive), decorators are the thing Kotlin has no answer for
  • asyncio is coroutines minus the threads — one event loop, a GIL, and no structured concurrency until TaskGroup
Dart Pre-Alpha

The Android-to-Flutter move, and the friendliest target Kotlin has. Null safety with ?, !, ?? and ?.; $ string interpolation; named arguments; expression bodies — the first day is genuinely easy. What is hard is structural: no data classes, no it, no varargs, and a concurrency model with no shared memory at all.

  • Null safety is nearly the same syntax and it is sound — no platform types, because there is no Java interop to punch a hole in it
  • No data class: ==, hashCode, toString and copyWith are hand-written, which is why every real codebase runs the freezed code generator
  • Sealed classes and exhaustive switch match when — and object, list, and map patterns destructure while they match, which Kotlin cannot do
  • Collection-if and collection-for build lists declaratively inside the literal, which is how every Flutter children: list is written
  • One event loop, no threads: async/await never runs in parallel, and CPU work goes to an isolate that shares no memory and copies its arguments
  • Compose's @Composable function becomes a Widget class with a build method, and remember { mutableStateOf } becomes a State object plus an explicit setState
Rust Pre-Alpha

What the JVM was hiding, made explicit. Rust's type system will feel like Kotlin's taken seriously — val/var is let/let mut, T? is Option, sealed classes are enums, when is match — but the garbage collector is gone, replaced by ownership rules the compiler enforces at build time.

  • Ownership & borrowing replace the GC: assignment MOVES a value, & lends it, and the aliased mutation Kotlin allows freely is a compile error
  • T? becomes Option<T>?. is .map(), ?: is .unwrap_or(), !! is .unwrap(), and ?: return is let-else
  • Sealed classes are enums with payloads, matched exhaustively — data class is struct plus an explicit #[derive(…)] list
  • Exceptions become Result<T, E> with the ? operator; panic! exists but is for bugs, not control flow
  • Iterator chains look like Kotlin's but are LAZY by default (Kotlin needs asSequence()) — and ranges flip: Kotlin's 1..5 is Rust's 1..=5
  • Data races are compile errors: Send/Sync encode thread safety in the types, and shared mutable state requires Arc<Mutex<…>> to even compile
Swift Pre-Alpha

Your twin on the other phone. Kotlin and Swift converged on the same modern answers — val/var is let/var, T? optionals on both sides, sealed classes are enums with associated values, when is switch, trailing lambdas everywhere — so this page is mostly about the two real differences: structs (and arrays!) copy on assignment, and ARC replaces the garbage collector with deterministic deinit.

  • Structs are VALUES: assignment copies, mutation needs mutating, and a let struct is deeply frozen — the inverse of data-class reference semantics
  • Arrays and dictionaries are structs too — copy-on-write kills the aliasing bugs (and the defensive copies) of the JVM collection world
  • Optionals are near-twins: ?. and ?? for ?. and ?:, if let for smart casts, and guard let as a first-class ?: return
  • Errors are marked at every call site: throws in the signature, try at the call, try? to fold failures into optionals
  • Argument labels are required by default — API design in Swift is label design, where Kotlin's named arguments are optional sugar
  • suspend is async with visible await; coroutineScope is withTaskGroup; and deinit fires deterministically where the JVM offers only use { }
TypeScript Alpha ⚡ Works Offline ⚡ Offline

Strong inference on both sides — but the types are erased, structural, and gradual. TypeScript will feel familiar (?. and ?? work like home, chains are chains, async/await maps to coroutines) until the type system inverts your JVM instincts: shape is identity, nothing exists at runtime, and any is a legal lie Kotlin never offers.

  • Structural typing: anything with the right shape IS the type — no declared conformance, and two accidentally same-shaped types are interchangeable
  • Total erasure: interfaces have no runtime existence, JSON.parse returns unverified any, and zod-style validators guard the boundaries
  • The null-safety twins: ?. and ?? as at home, smart casts as "narrowing" — but there are TWO absences, null and undefined
  • Union types replace sealed hierarchies: number | string inline, discriminated unions with a never-based exhaustiveness check for when
  • async/await is suspend on ONE thread — no Dispatchers, no blocking, no parallelism without workers; Promise.all plays awaitAll minus cancellation
  • Mapped types (Partial, keyof, Pick) compute types from types — the corner where TypeScript out-expresses Kotlin's generics entirely
Clojure Pre-Alpha ⚡ Works Offline ⚡ Offline

Same JVM, maximum paradigm distance. No types, no classes, no methods, no statements, and no mutation. What is left is functions and data — and the bet Clojure makes is that it is enough, that generic operations over plain maps beat a type per shape, and that a language you can extend yourself beats one you wait on.

  • Maps replace most of your data classes — value equality, assoc (which is copy()), and every library function already knows how to work with them
  • Everything is persistent and immutable, so there is no synchronized, no lock, no defensive copy — the data-race category simply does not arise
  • defmacro means ->, when, cond and defn are library code; a feature Kotlin needs a compiler release for, you can write this afternoon
  • Multimethods dispatch on an arbitrary function of the arguments and stay open to extension from any namespace — the opposite trade to a sealed when
  • nil is safe almost everywhere ((:name nil) is nil, (count nil) is 0), so the ?. operator is mostly unnecessary — until you touch Java
  • No type checker at all: a misspelled function is an error when that line runs, and the REPL, not the compiler, is where you develop
C# Pre-Alpha

The sibling language — which is exactly what makes it dangerous. C# has null-safe types, properties, records, extension methods, and async/await, so almost everything looks like home. The false friends are where it bites: var means inference (not mutability), sealed means final (not a closed hierarchy), and T? is a warning the runtime never enforces.

  • Nullability is opt-in and advisory: violating string? is a warning, ! emits no runtime check at all, and null still reaches your non-nullable fields
  • var is the type-inference keyword — every local is mutable, and there is no val for locals anywhere in the language
  • Generics are reified: typeof(T) and is T just work with no inline/reified, and List<int> does not box
  • struct gives you real user-defined value types, copied on assignment — far beyond @JvmInline value class
  • LINQ renames the whole stdlib (mapSelect, filterWhere, foldAggregate) and is lazy by default
  • No structured concurrency: a Task is hot, no coroutineScope owns it, and CancellationToken is threaded by hand
Java Pre-Alpha

The language Kotlin was built to replace — and to live beside. Same bytecode, same libraries, same garbage collector, and none of the syntax you learned Kotlin for. Everything Kotlin removed comes back: getters, constructors, overloads, throws clauses, and a class wrapped around every function. What returns with it is a language that has quietly caught up in three places, and stands still everywhere else.

  • Null safety is gone from the type system — Optional is a library band-aid for return values, and everything else is a NullPointerException waiting
  • == means reference identity again, and the Integer cache makes that silently correct up to 127 and wrong at 128
  • Records, sealed types, and pattern-matching switch are data classes and exhaustive when, feature for feature — the pages that read almost like Kotlin
  • Virtual threads (Java 21) make blocking cheap again, so the coroutine style survives the trip — but nothing in a signature tells you a method suspends
  • Extension functions and scope functions have no Java analog at all, not even as a library — apply becomes a local variable and a run of assignments
  • Checked exceptions are back and load-bearing: every caller catches or re-declares, which is why Java lambdas fight the throws clause
Haskell Pre-Alpha

Functional-first purity where Kotlin blends pragmatically. Haskell takes the ideas Kotlin borrowed — sealed types, nullability tracking, lambdas, inference — to their logical conclusion: effects live only in IO and the signature says so, everything is lazy, nothing mutates, and interfaces become type classes the compiler resolves by inference.

  • A function typed Int -> Int provably cannot println, mutate, or launch anything — effects appear in the type (IO Int) or not at all
  • Lazy by default: Kotlin's opt-in Sequence is Haskell's everything, and infinite lists like [0, 2 ..] are ordinary values
  • T? is Maybe, exceptions are Either, and ?.let chains are do blocks that stop at the first Nothing
  • Every function curries: add 10 IS the partially applied function, and operator sections like (* 2) replace { it * 2 }
  • data class unbundles into deriving (Show, Eq, Ord), sealed classes into one data declaration, value class into newtype
  • Type classes out-dispatch interfaces: instances declared apart from types, resolved by inference — fmap is one map for lists, Maybe, and IO alike
Scala Pre-Alpha

The other serious JVM language, and the sharpest contrast available on the same runtime. Kotlin borrowed val/var, data classes, companion objects, and declaration-site variance from here — then stopped. What it left behind is the half that makes Scala a different language: implicits, higher-kinded types, and one for-comprehension that works over every container you have.

  • Implicits are the feature with no Kotlin answer: one mechanism gives you context parameters, extension methods, and real type classes — behavior added to a type you do not own, resolved by the compiler
  • Higher-kinded types (F[_]) let you abstract over List, Option and Future themselves; Kotlin cannot even express the signature
  • for … yield is sugar for flatMap, so the same syntax chains fallible steps over Option, Either, Try, List and Future
  • No null and no ?. — absence is a value (Option[T]) that composes, not a compiler check that is erased
  • Pattern matching is extensible: any object with an unapply can be a pattern, so you match on parsed integers and JSON shapes, not just types and constants
  • Any method can be an operator and any single-argument method can be infix — which is what makes Cats and Akka read the way they do, for better and for worse
Elixir Pre-Alpha

The concurrency contrast Go did not fully cover. A coroutine is a cheap unit of scheduling over shared memory; a BEAM process is a cheap unit of isolation — its own heap, its own garbage collector, reachable only by message. The data race is not mitigated here, it is structurally impossible. And when something breaks, nobody catches it: a supervisor restarts it.

  • Processes share nothing, so there is no lock, no @Volatile, no Mutex — a message send copies the value, and that copy is what makes the isolation real
  • Scheduling is preemptive: a tight CPU loop cannot starve the scheduler, which is why there is no Dispatchers.IO and no yield() to remember
  • Structured concurrency becomes supervision: coroutineScope makes one child's failure cancel its siblings; a supervisor restarts the dead child from a known-good state, with no rescue anywhere
  • = is pattern matching, not assignment — and that one idea reshapes function heads, case, with, and receive alike
  • No objects and no mutation: modules hold functions, data is passed through them, and |> replaces the dot chain
  • {:ok, value} / {:error, reason} replaces both nullable types and exceptions — a failure is a value that carries why
Ruby ⚡ Works Offline ⚡ Offline

Every guarantee traded for expressiveness. No compiler, no types, no null safety, no exhaustive when — and in return, a language where classes are open, blocks are everywhere, and methods can be written at runtime. Kotlin protects you from yourself; Ruby hands you the keys and trusts you.

  • Duck typing replaces interfaces, and a misspelled method is a NoMethodError when that line runs — the test suite is the type checker
  • Everything is an object: nil has methods, classes are objects, and 3.times { } is a method on the integer
  • Open classes — reopen String or Integer and redefine anything, globally; extension functions with none of the guardrails
  • Blocks and yield are the whole language, and Enumerable is the stdlib you know (filterselect, foldreduce, &:upcase)
  • Mixins replace both interfaces and extension functions: define <=>, include Comparable, get every operator free
  • Metaprogramming is idiomatic, not a hack — define_method and method_missing are how attr_accessor and Rails work
Drag cards to reorder · your order is saved locally