PONYΞ»M2Modula-2
CodeCompared
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 RubyBrowse comparisons ↓Explore the language map β†—

Choose your own path by reordering languages

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 (filter β†’ select, fold β†’ reduce, &: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
GoPre-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
JavaScriptAlpha⚑ Works Offline⚑ Offline

Structured concurrency meets a promise nobody is waiting for. Null safety evaporates and undefined is a second empty you never had; exhaustive when becomes a switch nothing checks; and a coroutineScope that cancels its children becomes a promise that keeps running when its siblings fail. The page for the Kotlin Multiplatform reader whose shared module has to land on this platform.

  • Two empty values, and no type distinguishes String from String? β€” though ?. and ?? read exactly like ?. and ?:
  • No structured concurrency: no Job tree, no scope, and no cancellation β€” AbortController is a flag you check by hand
  • suspend and async are the same coloring problem, but an async call starts immediately and an unhandled rejection kills a Node process
  • Sealed classes become a kind field and a switch with a throwing default; there is no exhaustiveness anywhere
  • A data class becomes an object literal: copy() survives as { ...first, y: 99 }, but equals, hashCode and toString do not
  • One number type β€” no Int, no Long β€” plus BigInt, which is why an exported Kotlin/JS Long is a boxed object rather than a number
  • Extension functions become plain functions, because the alternative is mutating a global prototype; it, infix calls and trailing lambdas are all gone
  • @JsExport, dynamic and expect/actual get a section of their own β€” the reason most readers are here
PythonBeta⚑ 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
DartPre-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
RustPre-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
SwiftPre-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 { }
TypeScriptAlpha⚑ 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
ClojurePre-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 (map β†’ Select, filter β†’ Where, fold β†’ Aggregate) and is lazy by default
  • No structured concurrency: a Task is hot, no coroutineScope owns it, and CancellationToken is threaded by hand
JavaPre-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
HaskellPre-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
ScalaPre-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
RocPre-Alpha

The closest language on this site, and then one axis further. You already have null safety, sealed classes with exhaustive when, data classes and val. Roc removes null entirely, drops the declaration a union needs, lets a union stay open, puts every effect in the type system the way suspend does for one, and replaces the collector.

  • There is no null to be safe about β€” no ? suffix, no !!, and no platform type arriving unchecked from Java. Absence is a tag, so a "nothing" can say which nothing: Missing, NotYetLoaded and Refused in one union.
  • A sealed hierarchy becomes two lines. No base class, no subclass per case, no is check β€” and unlike sealed, a Roc union can be left open ([Go, Stop, ..]) while the named cases stay exhaustively checked.
  • ! is suspend for every effect. The same coloring you already understand, propagating the same way up the call stack, enforced by the compiler β€” so a signature without ! is a guarantee rather than a convention.
  • val protects the binding; Roc protects the value. There is no MutableList for a val to point at, and no read-only view that something else can still mutate underneath you.
  • No JVM. No collector, no boxing β€” a List(I64) holds the integers themselves, not Integer objects β€” no class loading, and no startup. Memory is reference counted by the compiler.
  • Errors are in the signature. Try(ok, err) names exactly what can go wrong, where Result<T> says only "or some Throwable" and toInt() says nothing at all.
  • Be honest about the trade: no coroutines, no Flow, no extension functions, no default or named arguments, no scope functions, no it β€” and no 1.0, against a language that had one in 2016.
ElixirPre-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
Drag cards to reorder Β· your order is saved locally