Ruby.CodeCompared.To/Kotlin

An interactive executable cheatsheet for Rubyists learning Kotlin

Ruby 4.0 Kotlin 2.3
Syntax Basics
Hello World
puts "Hello, Kotlin!"
fun main() { println("Hello, Kotlin!") }

Every Kotlin program starts with a top-level fun main() function β€” no class wrapper is required (unlike Java). The println() function appends a newline, mirroring Ruby's puts. Kotlin files use the .kt extension and the compiler infers the entry-point class name from the filename.

Comments
# Single-line comment count = 42 # inline comment =begin Multi-line comment block (rarely used in practice) =end puts count
fun main() { // Single-line comment val count = 42 // inline comment /* Multi-line comment block */ /** * KDoc comment β€” appears in generated API docs. * Use on classes, functions, and properties. */ println(count) }

Kotlin uses // for single-line comments and /* ... */ for block comments. Block comments can be nested, which is unusual for C-family languages. The /** ... */ KDoc format is the idiomatic documentation comment β€” the dokka tool uses it to generate API documentation. Ruby's rarely-used =begin / =end serves the same block-comment purpose.

Semicolons
# Ruby: no semicolons needed greeting = "Hello" name = "World" puts "#{greeting}, #{name}!" # Semicolons allow multiple statements per line (rare) x = 1; y = 2; puts x + y
fun main() { // Kotlin: semicolons are optional β€” newlines end statements val greeting = "Hello" val name = "World" println("$greeting, $name!") // Multiple statements on one line require semicolons val x = 1; val y = 2; println(x + y) }

Kotlin infers statement endings from newlines, making semicolons optional in normal code β€” just like Ruby. Semicolons are only needed when multiple statements appear on the same line, which is uncommon in idiomatic Kotlin. This is a significant ergonomic improvement over Java, where semicolons are mandatory.

Variables
val vs var
# Ruby: all variables are mutable by default name = "Alice" name = "Bob" # reassignment is fine puts name # freeze makes an object immutable greeting = "Hello".freeze # greeting << " World" # raises FrozenError puts greeting
fun main() { val name = "Alice" // immutable β€” cannot be reassigned // name = "Bob" // compile error: val cannot be reassigned var score = 10 // mutable β€” can be reassigned score = 20 println(score) val greeting = "Hello" println(greeting) }

Kotlin distinguishes val (read-only, cannot be reassigned) from var (mutable). This is enforced at compile time, unlike Ruby's convention of using frozen objects. The Kotlin style guide strongly prefers val by default β€” only use var when reassignment is genuinely needed. Note that val prevents reassignment but does not deep-freeze the object itself.

Type inference
# Ruby: dynamically typed β€” no type annotations name = "Alice" age = 30 temperature = 98.6 active = true puts name.class # String puts age.class # Integer puts temperature.class # Float puts active.class # TrueClass
fun main() { // Type inferred from the assigned value val name = "Alice" // String val age = 30 // Int val temperature = 98.6 // Double val active = true // Boolean // Explicit type annotations (optional here, required sometimes) val city: String = "Portland" val count: Int = 0 println("$name, $age, $temperature, $active") println("$city, $count") }

Kotlin is statically typed but uses type inference to avoid redundant annotations. The compiler infers String, Int, Double, and Boolean from the literal values. Explicit type annotations are written after the variable name with a colon β€” they are required when the type cannot be inferred, such as when declaring a variable without an initial value.

Constants
PI = 3.14159 MAX_RETRIES = 3 APP_NAME = "MyApp" puts PI puts MAX_RETRIES puts APP_NAME
// const val is evaluated at compile time (top-level or in companion objects) const val PI = 3.14159 const val MAX_RETRIES = 3 const val APP_NAME = "MyApp" fun main() { println(PI) println(MAX_RETRIES) println(APP_NAME) }

Kotlin's const val declares a compile-time constant β€” the value must be a primitive type or String and must be known at compile time. This differs from a plain val, which is a runtime read-only property. Ruby uses capitalized names as constants (by convention and enforced with a warning on reassignment), but they are not truly immutable at the language level.

Types & Casting
Basic types
integer_value = 42 long_value = 9_000_000_000 float_value = 3.14 bool_value = true text = "hello" nothing = nil puts integer_value.class puts long_value.class puts float_value.class puts bool_value.class puts text.class puts nothing.class
fun main() { val intValue: Int = 42 val longValue: Long = 9_000_000_000L val doubleValue: Double = 3.14 val floatValue: Float = 3.14f val boolValue: Boolean = true val text: String = "hello" val nothing: Any? = null println(intValue::class.simpleName) println(longValue::class.simpleName) println(doubleValue::class.simpleName) println(boolValue::class.simpleName) println(text::class.simpleName) println(nothing) }

Kotlin's basic types are Int, Long, Double, Float, Boolean, String, and Any (the root of the type hierarchy, analogous to Ruby's Object). Unlike Java, Kotlin does not have primitive types as a separate concept β€” everything is an object, though the compiler uses JVM primitives under the hood for performance. Long literals require an L suffix; Float literals require an f suffix.

Type checking
value = "hello" puts value.is_a?(String) # true puts value.is_a?(Integer) # false puts value.kind_of?(String) # same as is_a? puts value.instance_of?(String) # exact class only case value when String then puts "It's a string" when Integer then puts "It's an integer" end
fun main() { val value: Any = "hello" println(value is String) // true println(value is Int) // false println(value !is Int) // true (negated check) when (value) { is String -> println("It's a string, length ${(value as String).length}") is Int -> println("It's an integer") else -> println("Something else") } }

Kotlin's is operator checks whether a value is an instance of a type, mirroring Ruby's is_a?. The !is operator is the negated form. Inside a when or if branch that checks is SomeType, Kotlin automatically smart-casts the variable to that type β€” no explicit cast is needed within that scope.

Smart casts & explicit casts
def describe(value) if value.is_a?(String) puts "String of length #{value.length}" elsif value.is_a?(Integer) puts "Integer doubled: #{value * 2}" end end describe("hello") describe(21)
fun describe(value: Any) { if (value is String) { // Smart cast: value is automatically treated as String here println("String of length ${value.length}") } else if (value is Int) { // Smart cast: value is automatically treated as Int here println("Integer doubled: ${value * 2}") } } fun main() { describe("hello") describe(21) // Unsafe cast β€” throws ClassCastException if wrong type val text: Any = "Kotlin" val asString = text as String println(asString.uppercase()) // Safe cast β€” returns null instead of throwing val asInt = text as? Int println(asInt) // null }

Kotlin's smart cast feature automatically narrows a variable's type after a successful is check β€” there is no need for a manual cast inside the guarded branch. The explicit cast operator as throws a ClassCastException if the cast fails; the safe cast as? returns null instead. Ruby has no equivalent compile-time narrowing, relying instead on duck typing.

Null Safety
Nullable types
# Ruby: any variable can be nil at any time name = "Alice" name = nil # perfectly valid # nil check before use if name puts name.upcase else puts "No name" end
fun main() { val name: String = "Alice" // name = null // compile error: String is non-nullable // Nullable types: declared with a ? suffix val nickname: String? = null // val can hold null when type is String? // Null check: Kotlin requires explicit handling before use if (nickname != null) { println(nickname.uppercase()) // smart cast to String inside the if } else { println("No nickname") } // Safe-call operator: short-circuits on null val city: String? = "Portland" println(city?.length) // 8 (safe call on non-null) println(nickname?.length) // null (safe call on null) }

Kotlin distinguishes non-nullable types (String) from nullable types (String?) at the type-system level. A non-nullable variable can never hold null β€” this is enforced at compile time and eliminates an entire class of null pointer errors. Ruby has no such distinction; any variable can be nil at any time, making nil-checks a runtime concern.

Safe call operator
name = nil # Ruby safe navigation operator &. length = name&.length puts length.inspect # nil name = "Alice" length = name&.length puts length # 5
fun main() { var name: String? = null // Safe call: returns null instead of throwing NullPointerException val length = name?.length println(length) // null name = "Alice" println(name?.length) // 5 // Chained safe calls val city: String? = null println(city?.uppercase()?.reversed()) // null }

Kotlin's safe call operator ?. accesses a property or calls a method only if the receiver is non-null; otherwise it short-circuits and returns null. This is directly equivalent to Ruby's safe navigation operator &., introduced in Ruby 2.3. Safe calls can be chained β€” the entire chain short-circuits to null as soon as any step yields null.

Elvis operator
name = nil # Ruby: || provides a default for nil (and false) display_name = name || "Guest" puts display_name name = "Alice" display_name = name || "Guest" puts display_name
fun main() { var name: String? = null // Elvis operator: use right side when left side is null val displayName = name ?: "Guest" println(displayName) // Guest name = "Alice" val greeting = name ?: "Guest" println(greeting) // Alice // Combined with safe call val length = name?.length ?: 0 println(length) // 5 }

Kotlin's Elvis operator ?: (named for the sideways smiley face) returns the right-hand value when the left-hand expression is null. It is similar to Ruby's || idiom, but more precise: Ruby's || also triggers for false, while Kotlin's ?: only triggers for null. The Elvis operator pairs naturally with safe calls to provide fallback values.

Not-null assertion & let
name = "Alice" # Ruby: use the value directly or guard with &. # No equivalent to !! β€” Ruby trusts you puts name.upcase # Block-style nil guard name&.then { |n| puts "Name is: #{n.upcase}" }
fun main() { var name: String? = "Alice" // !! asserts non-null β€” throws NullPointerException if null println(name!!.uppercase()) // let β€” executes a block only when the value is non-null name?.let { nonNullName -> println("Name is: ${nonNullName.uppercase()}") } // With implicit 'it' parameter name?.let { println("Length: ${it.length}") } val missing: String? = null missing?.let { println("This never prints") } println("Done") }

The not-null assertion operator !! converts a nullable type to a non-nullable one, throwing a NullPointerException if the value is actually null. It should be used sparingly β€” its presence is often a sign that the code should be restructured. The ?.let { } pattern is idiomatic Kotlin for executing a block only when a nullable value is non-null, similar to Ruby's &.then { }.

Strings
String templates
name = "Alice" age = 30 # Simple interpolation puts "Hello, #{name}!" # Expression interpolation puts "In 10 years: #{age + 10}" puts "Uppercase: #{name.upcase}"
fun main() { val name = "Alice" val age = 30 // Simple variable β€” use $ prefix println("Hello, $name!") // Expression β€” use ${ } println("In 10 years: ${age + 10}") println("Uppercase: ${name.uppercase()}") // $ with a property access println("Length: ${name.length}") }

Kotlin string templates use $name for simple variable references and ${expression} for arbitrary expressions β€” a close parallel to Ruby's #{expression} syntax. The $ prefix without braces works only for identifiers; any method call or operator requires the ${ } delimiters. Both approaches embed the result of the expression directly into the string without explicit conversion.

Multiline strings
message = <<~HEREDOC Dear Alice, Your order has shipped. Regards, The Team HEREDOC puts message
fun main() { val message = """ Dear Alice, Your order has shipped. Regards, The Team """.trimIndent() println(message) }

Kotlin triple-quoted strings ("""...""") span multiple lines without escape sequences. The trimIndent() method removes the common leading whitespace from all lines β€” equivalent to Ruby's squiggly heredoc <<~HEREDOC which strips leading whitespace automatically. String templates ($variable) work inside triple-quoted strings as well.

Common string methods
text = " Hello, World! " puts text.length # 18 puts text.strip # "Hello, World!" puts text.upcase puts text.downcase puts text.include?("World") puts text.start_with?(" Hello") puts text.split(", ") puts text.strip.gsub("World", "Kotlin") puts "abc" * 3
fun main() { val text = " Hello, World! " println(text.length) // 18 println(text.trim()) // "Hello, World!" println(text.uppercase()) println(text.lowercase()) println(text.contains("World")) // true println(text.startsWith(" Hello")) // true println(text.trim().split(", ")) println(text.trim().replace("World", "Kotlin")) println("abc".repeat(3)) // abcabcabc }

Kotlin's string API closely mirrors Ruby's: trim() corresponds to strip, uppercase()/lowercase() to upcase/downcase, contains() to include?, and repeat(n) to Ruby's * operator. The most notable difference is naming convention β€” Kotlin uses camelCase method names with parentheses, while Ruby uses snake_case.

Collections
Lists
fruits = ["apple", "banana", "cherry"] puts fruits[0] puts fruits.length puts fruits.include?("banana") fruits.push("date") puts fruits # Mutable vs frozen frozen_list = ["a", "b"].freeze # frozen_list << "c" # raises FrozenError
fun main() { // Immutable list β€” read-only, cannot add/remove elements val fruits = listOf("apple", "banana", "cherry") println(fruits[0]) println(fruits.size) println(fruits.contains("banana")) // Mutable list β€” elements can be added and removed val mutableFruits = mutableListOf("apple", "banana", "cherry") mutableFruits.add("date") println(mutableFruits) // Type-explicit form val numbers = listOf<Int>(1, 2, 3) println(numbers) }

Kotlin separates read-only collections from mutable ones at the type level. listOf() returns a List<T> that cannot be mutated; mutableListOf() returns a MutableList<T> that supports add(), remove(), and similar methods. This immutability-by-default mirrors the val/var philosophy. Ruby arrays are always mutable unless explicitly frozen.

Maps (hashes)
person = { name: "Alice", age: 30, city: "Portland" } puts person[:name] puts person[:age] puts person.key?(:city) puts person.size person[:email] = "alice@example.com" puts person
fun main() { // Immutable map val person = mapOf( "name" to "Alice", "age" to 30, "city" to "Portland" ) println(person["name"]) println(person["age"]) println(person.containsKey("city")) println(person.size) // Mutable map val contacts = mutableMapOf("Alice" to "555-1234") contacts["Bob"] = "555-5678" println(contacts) }

Kotlin maps are created with mapOf(key to value, ...), using the to infix function to create Pair objects. The mutableMapOf() variant allows adding and updating entries. Unlike Ruby hashes, Kotlin maps are type-safe: a Map<String, Int> cannot hold values of any other type. Access uses the same bracket syntax as Ruby, returning null for missing keys.

Sets
require "set" colors = Set.new(["red", "green", "blue", "red"]) puts colors.size # 3 β€” duplicates removed puts colors.include?("red") colors.add("yellow") puts colors.to_a
fun main() { // Immutable set β€” duplicates are silently ignored val colors = setOf("red", "green", "blue", "red") println(colors.size) // 3 println(colors.contains("red")) // Mutable set val mutableColors = mutableSetOf("red", "green", "blue") mutableColors.add("yellow") mutableColors.add("red") // no-op β€” already present println(mutableColors) // Set operations val primary = setOf("red", "blue", "yellow") val secondary = setOf("orange", "green", "purple") println(primary.union(secondary)) }

Kotlin sets follow the same read-only/mutable split as lists and maps. setOf() creates an immutable Set<T>; mutableSetOf() creates a MutableSet<T>. Duplicate entries are silently discarded, just as in Ruby's Set. Kotlin's standard library provides union, intersect, and subtract as extension functions on any collection.

Ranges
puts (1..5).to_a.inspect # [1,2,3,4,5] inclusive puts (1...5).to_a.inspect # [1,2,3,4] exclusive puts (1..10).include?(7) # true # Step (1..10).step(2) { |n| print "#{n} " } puts # Reverse 10.downto(1).first(3).inspect.then { |result| puts result }
fun main() { val inclusive = 1..5 // 1, 2, 3, 4, 5 val exclusive = 1 until 5 // 1, 2, 3, 4 println(inclusive.toList()) println(exclusive.toList()) println(7 in 1..10) // true // Step for (number in 1..10 step 2) print("$number ") println() // Downward range for (number in 10 downTo 1 step 3) print("$number ") println() }

Kotlin's 1..5 creates an inclusive range, matching Ruby's 1..5. The exclusive form uses until instead of Kotlin having a three-dot syntax β€” 1 until 5 maps to Ruby's 1...5. The downTo and step infix functions replace Ruby's downto and step methods. Ranges can be used with the in operator to test membership.

Control Flow
if as expression
score = 85 # Ruby if as expression grade = if score >= 90 then "A" elsif score >= 80 then "B" elsif score >= 70 then "C" else "F" end puts grade # Ternary shorthand label = score >= 60 ? "pass" : "fail" puts label
fun main() { val score = 85 // Kotlin if is an expression β€” it returns a value val grade = if (score >= 90) "A" else if (score >= 80) "B" else if (score >= 70) "C" else "F" println(grade) // Kotlin has no ternary operator β€” use if/else instead val label = if (score >= 60) "pass" else "fail" println(label) }

Like Ruby, Kotlin's if is an expression that returns a value. Kotlin deliberately omits a ternary operator (? :) because if (cond) a else b is already concise. Ruby also supports using if as an expression (the result of the last evaluated branch is returned), making this a comfortable similarity for Rubyists moving to Kotlin.

when expression
value = 3 result = case value when 1 then "one" when 2, 3 then "two or three" when 4..9 then "four to nine" else "something else" end puts result
fun main() { val value = 3 val result = when (value) { 1 -> "one" 2, 3 -> "two or three" in 4..9 -> "four to nine" else -> "something else" } println(result) // when without a subject (like a chain of if/else) val temperature = 22 val description = when { temperature < 0 -> "freezing" temperature < 15 -> "cold" temperature < 25 -> "comfortable" else -> "hot" } println(description) }

Kotlin's when expression is the counterpart to Ruby's case/when. It matches the subject against branch conditions using -> arrows, and can match multiple values with commas or ranges with in. Without a subject, when { } behaves like a chain of if/else if branches. Like Ruby's case, when is an expression that returns the matching branch's value.

Loops
for / each
fruits = ["apple", "banana", "cherry"] # each β€” idiomatic Ruby iteration fruits.each { |fruit| puts fruit } # each_with_index fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
fun main() { val fruits = listOf("apple", "banana", "cherry") // for-in loop β€” most common form for (fruit in fruits) { println(fruit) } // withIndex for index + value for ((index, fruit) in fruits.withIndex()) { println("$index: $fruit") } // forEach higher-order function fruits.forEach { fruit -> println(fruit) } }

Kotlin's for (item in collection) loop iterates over any Iterable, directly mirroring Ruby's collection.each. For index-and-value pairs, withIndex() returns a sequence of IndexedValue objects that can be destructured in the for header. Kotlin also supports the forEach { } higher-order function for a more functional style.

Range loops
(1..5).each { |number| print "#{number} " } puts 5.times { |index| print "#{index} " } puts 1.upto(5) { |number| print "#{number} " } puts
fun main() { for (number in 1..5) print("$number ") println() // repeat is the closest equivalent to n.times repeat(5) { index -> print("$index ") } println() // upTo expressed as an inclusive range for (number in 1..5) print("$number ") println() }

Kotlin's for (number in 1..5) directly replaces Ruby's (1..5).each. The repeat(n) { } function is the idiomatic equivalent of Ruby's n.times { } β€” it calls the lambda n times and passes the zero-based iteration index. Kotlin does not have separate upto and downto methods on integers; all range-based loops use the range/step syntax.

while & do-while
count = 0 while count < 3 puts count count += 1 end # begin/end while (always executes at least once) total = 0 begin total += 1 end while total < 3 puts total
fun main() { var count = 0 while (count < 3) { println(count) count++ } // do-while β€” always executes body at least once var total = 0 do { total++ } while (total < 3) println(total) }

Kotlin's while and do-while loops work identically to their C-family counterparts. The do-while loop guarantees the body executes at least once before the condition is evaluated, matching Ruby's begin/end while (or loop do / break unless) idiom. Kotlin requires parentheses around the condition but not a separate keyword like do for standard while.

Functions
Basic functions
def greet(name) "Hello, #{name}!" end def add(x, y) x + y end puts greet("Alice") puts add(3, 4)
fun greet(name: String): String { return "Hello, $name!" } fun add(firstNumber: Int, secondNumber: Int): Int { return firstNumber + secondNumber } fun main() { println(greet("Alice")) println(add(3, 4)) }

Kotlin functions require explicit type annotations for parameters and, when using a block body, for the return type as well. The fun keyword introduces a function, analogous to Ruby's def. Unlike Ruby, Kotlin does not implicitly return the last expression in a block body β€” return is required. Functions that return nothing have return type Unit (analogous to void), which can be omitted.

Single-expression functions
# Ruby 4.0: one-liner method syntax def square(x) = x * x def cube(x) = x ** 3 def greet(name) = "Hello, #{name}!" puts square(5) puts cube(3) puts greet("World")
// Single-expression functions β€” return type is inferred fun square(number: Int) = number * number fun cube(number: Int) = number * number * number fun greet(name: String) = "Hello, $name!" fun main() { println(square(5)) println(cube(3)) println(greet("World")) }

Kotlin's single-expression function syntax (fun name(params) = expression) omits the braces, return keyword, and return type annotation, inferring the type from the expression. This is nearly identical to Ruby 4.0's one-liner method syntax def name(params) = expression. Both languages use = to signal that the function is a single expression rather than a block body.

Default & named arguments
def greet(name:, greeting: "Hello") "#{greeting}, #{name}!" end puts greet(name: "Alice") puts greet(name: "Bob", greeting: "Hi") puts greet(greeting: "Hey", name: "Carol")
fun greet(name: String, greeting: String = "Hello"): String { return "$greeting, $name!" } fun main() { println(greet("Alice")) println(greet("Bob", "Hi")) // Named arguments β€” order doesn't matter println(greet(name = "Carol", greeting = "Hey")) println(greet(greeting = "Howdy", name = "Dave")) }

Kotlin supports default parameter values and named arguments in the same way Ruby does. Default values are specified at the parameter declaration site with =. Named arguments use the parameter name followed by = at the call site, and can appear in any order. Unlike Ruby where keyword arguments must always be passed by name if they are defined as keywords, Kotlin allows calling the same function both positionally and by name.

vararg (splat)
def sum(*numbers) numbers.sum end def tag(element, *classes) "<#{element} class='#{classes.join(" ")}'>" end puts sum(1, 2, 3, 4, 5) puts tag("div", "card", "highlighted", "large")
fun sum(vararg numbers: Int): Int = numbers.sum() fun tag(element: String, vararg classes: String): String { return "<$element class='${classes.joinToString(" ")}'>" } fun main() { println(sum(1, 2, 3, 4, 5)) println(tag("div", "card", "highlighted", "large")) // Spread operator * unpacks an array into vararg position val extraNumbers = intArrayOf(4, 5, 6) println(sum(1, 2, 3, *extraNumbers)) }

Kotlin's vararg modifier accepts a variable number of arguments, analogous to Ruby's splat operator *args. Inside the function, the vararg parameter behaves as an Array<T>. The spread operator * can unpack an existing array into a vararg call site β€” the same symbol Ruby uses for the inverse operation of collecting arguments into an array.

Lambdas & Higher-Order Functions
Lambda syntax
double = ->(number) { number * 2 } add = ->(x, y) { x + y } shout = proc { |text| text.upcase + "!" } puts double.call(5) puts add.call(3, 4) puts shout.call("hello")
fun main() { val double: (Int) -> Int = { number -> number * 2 } val add: (Int, Int) -> Int = { x, y -> x + y } val shout: (String) -> String = { text -> text.uppercase() + "!" } println(double(5)) println(add(3, 4)) println(shout("hello")) // Lambda with implicit single-parameter 'it' val triple = { it: Int -> it * 3 } println(triple(7)) }

Kotlin lambdas are written as { parameters -> body }. When a lambda has a single parameter, it can be omitted and replaced with the implicit name it β€” similar to how Ruby blocks use _1 as an implicit parameter in Ruby 2.7+. Lambda types are written as (ParamType) -> ReturnType. Kotlin lambdas are objects that can be stored in variables and passed as arguments.

Trailing lambdas & it
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } evens = numbers.filter { |n| n.even? } total = numbers.reduce(0) { |sum, n| sum + n } puts doubled.inspect puts evens.inspect puts total
fun main() { val numbers = listOf(1, 2, 3, 4, 5) // Trailing lambda: lambda is the last argument, moved outside () val doubled = numbers.map { it * 2 } val evens = numbers.filter { it % 2 == 0 } val total = numbers.fold(0) { accumulator, number -> accumulator + number } println(doubled) println(evens) println(total) }

When a function's last parameter is a lambda, Kotlin allows placing it outside the parentheses β€” this is the trailing lambda convention, and it produces syntax that feels like Ruby's block syntax. The implicit it parameter avoids the need to name single-parameter lambdas. fold (Kotlin) and reduce (Ruby) both accumulate a collection to a single value, with fold accepting an explicit initial value.

map, filter, groupBy, sortedBy
words = ["banana", "apple", "cherry", "apricot", "blueberry"] lengths = words.map(&:length) long_ones = words.filter { |word| word.length > 6 } by_first = words.group_by { |word| word[0] } sorted = words.sort_by(&:length) puts lengths.inspect puts long_ones.inspect puts by_first.transform_values { |values| values.inspect } puts sorted.inspect
fun main() { val words = listOf("banana", "apple", "cherry", "apricot", "blueberry") val lengths = words.map { it.length } val longOnes = words.filter { it.length > 6 } val byFirst = words.groupBy { it.first() } val sorted = words.sortedBy { it.length } println(lengths) println(longOnes) println(byFirst) println(sorted) }

Kotlin's collection extension functions mirror Ruby's Enumerable methods almost one-for-one: map, filter (Ruby: select), groupBy (Ruby: group_by), and sortedBy (Ruby: sort_by). These are extension functions on Iterable<T>, not methods on a mixin, but the calling syntax is identical. All return new immutable collections rather than mutating in place.

any, all, none & function references
numbers = [2, 4, 6, 8, 10] puts numbers.any?(&:odd?) puts numbers.all?(&:even?) puts numbers.none?(&:negative?) # Method reference with &method(:name) words = ["hello", "world"] puts words.map(&method(:puts)).inspect
fun main() { val numbers = listOf(2, 4, 6, 8, 10) println(numbers.any { it % 2 != 0 }) // false println(numbers.all { it % 2 == 0 }) // true println(numbers.none { it < 0 }) // true // Function references with :: val words = listOf("hello", "world") words.forEach(::println) // Method reference on a type val lengths = words.map(String::length) println(lengths) }

Kotlin's any, all, and none predicates match Ruby's any?, all?, and none?. Function references use the :: operator β€” ::println references the top-level function, and String::length references a property accessor on a type. This is analogous to Ruby's &method(:name) and &:symbol shorthand for method objects.

Classes
Primary constructor
class Person attr_accessor :name, :age def initialize(name, age) @name = name @age = age end def introduce "Hi, I'm #{@name} and I'm #{@age}." end end alice = Person.new("Alice", 30) puts alice.introduce puts alice.name
class Person(val name: String, var age: Int) { fun introduce() = "Hi, I'm $name and I'm $age." } fun main() { val alice = Person("Alice", 30) println(alice.introduce()) println(alice.name) alice.age = 31 println(alice.age) }

Kotlin's primary constructor lives in the class header β€” parameters marked with val or var automatically become properties. This eliminates the boilerplate of Ruby's attr_accessor plus initialize assignment. The constructor is called without new: Person("Alice", 30). A parameter declared val is read-only; var is read-write.

init block & custom accessors
class Temperature def initialize(celsius) raise ArgumentError, "Too cold!" if celsius < -273.15 @celsius = celsius end def fahrenheit @celsius * 9.0 / 5 + 32 end def celsius @celsius end end temp = Temperature.new(100) puts temp.celsius puts temp.fahrenheit
class Temperature(val celsius: Double) { init { require(celsius >= -273.15) { "Too cold!" } } // Computed property (custom getter) val fahrenheit: Double get() = celsius * 9.0 / 5 + 32 } fun main() { val temp = Temperature(100.0) println(temp.celsius) println(temp.fahrenheit) }

The init block in Kotlin runs as part of construction, after the primary constructor parameters are set β€” it is the place for validation or setup logic, equivalent to Ruby's initialize body. Custom getters are defined directly on a property with get() = ..., replacing the need for a separate reader method. The require function throws an IllegalArgumentException if its condition is false.

Companion object
class Counter @@total_created = 0 def initialize(name) @name = name @@total_created += 1 end def self.total_created @@total_created end end Counter.new("first") Counter.new("second") puts Counter.total_created
class Counter(val name: String) { companion object { private var totalCreated = 0 fun incrementCount() { totalCreated++ } fun getTotalCreated() = totalCreated } init { incrementCount() } } fun main() { Counter("first") Counter("second") println(Counter.getTotalCreated()) }

Kotlin has no static keyword. Instead, class-level members β€” shared state and factory methods β€” live in a companion object declared inside the class. Members of the companion object are accessed using the class name, just like Ruby's class methods (Counter.total_created). Kotlin's companion object is a real singleton object, which means it can implement interfaces.

Inheritance & override
class Animal def initialize(name) @name = name end def speak "..." end def to_s "#{self.class.name}(#{@name}) says: #{speak}" end end class Dog < Animal def speak = "Woof!" end class Cat < Animal def speak = "Meow!" end puts Dog.new("Rex") puts Cat.new("Whiskers")
// open allows the class to be subclassed open class Animal(val name: String) { open fun speak() = "..." override fun toString() = "${this::class.simpleName}($name) says: ${speak()}" } class Dog(name: String) : Animal(name) { override fun speak() = "Woof!" } class Cat(name: String) : Animal(name) { override fun speak() = "Meow!" } fun main() { println(Dog("Rex")) println(Cat("Whiskers")) }

Kotlin classes are final by default β€” they cannot be subclassed unless marked open. Similarly, methods must be marked open to allow overriding, and overriding methods must use the override: Animal(name) in the class header.

Data Classes
data class basics
# Ruby: Struct generates accessors, ==, to_s automatically Point = Struct.new(:x, :y) origin = Point.new(0, 0) corner = Point.new(3, 4) puts origin puts corner puts origin == Point.new(0, 0) # true β€” structural equality puts corner.x puts corner.y
data class Point(val x: Int, val y: Int) fun main() { val origin = Point(0, 0) val corner = Point(3, 4) println(origin) // Point(x=0, y=0) println(corner) // Point(x=3, y=4) println(origin == Point(0, 0)) // true β€” structural equality println(corner.x) println(corner.y) }

Kotlin's data class automatically generates toString(), equals(), hashCode(), and copy() based on the primary constructor properties. This is the direct equivalent of Ruby's Struct.new, which generates the same boilerplate. The auto-generated equals() performs structural (value-based) equality, so two Point(0, 0) instances are equal β€” unlike regular Kotlin classes, which use reference equality by default.

copy & destructuring
Person = Struct.new(:name, :age, :city) alice = Person.new("Alice", 30, "Portland") # Ruby Struct: no built-in copy-with-changes; use dup + update bob = alice.dup bob.name = "Bob" bob.age = 25 puts bob # Destructuring name, age, city = alice puts "#{name} is #{age} in #{city}"
data class Person(val name: String, val age: Int, val city: String) fun main() { val alice = Person("Alice", 30, "Portland") // copy β€” creates a new instance with selected properties changed val bob = alice.copy(name = "Bob", age = 25) println(bob) // Destructuring declarations val (name, age, city) = alice println("$name is $age in $city") }

The copy() method generated for data classes creates a new instance with any combination of properties changed while preserving the rest β€” this immutable update pattern has no direct Ruby equivalent (Ruby structs require dup and manual field assignment). Destructuring declarations (val (name, age) = person) unpack the properties in declaration order, similar to Ruby's parallel assignment from a Struct.

Extension Functions
Extending existing types
# Ruby: open classes β€” add methods to any existing class class String def shout upcase + "!" end def word_count split.length end end class Integer def even_double even? ? self * 2 : self end end puts "hello".shout puts "the quick brown fox".word_count puts 4.even_double puts 7.even_double
// Extension functions β€” defined outside the class, called like methods fun String.shout() = uppercase() + "!" fun String.wordCount() = trim().split("\\s+".toRegex()).size fun Int.evenDouble() = if (this % 2 == 0) this * 2 else this fun main() { println("hello".shout()) println("the quick brown fox".wordCount()) println(4.evenDouble()) println(7.evenDouble()) }

Kotlin extension functions add methods to existing types without modifying the class or using inheritance β€” they are resolved statically at compile time, which makes them safer than Ruby's open classes. Inside an extension function, this refers to the receiver object. Extensions are scoped to the file or package where they are defined, preventing the global monkey-patching risks of Ruby's open classes.

Extension properties
class Integer def even? self % 2 == 0 end def factorial return 1 if self <= 1 self * (self - 1).factorial end end puts 4.even? puts 5.even? puts 5.factorial
// Extension property β€” adds a property to an existing type val Int.isEven: Boolean get() = this % 2 == 0 fun Int.factorial(): Long { if (this <= 1) return 1L return this * (this - 1).factorial() } fun main() { println(4.isEven) // true println(5.isEven) // false println(5.factorial()) }

Extension properties add computed read-only (or read-write) properties to existing types, with a custom get() (and optionally set()). They cannot store state β€” there is no backing field β€” so they must delegate to existing properties or methods. This is analogous to adding a reader method to a class in Ruby, though Kotlin's version is file-scoped rather than globally visible.

Sealed Classes & Pattern Matching
Sealed classes
# Ruby pattern matching with case/in (Ruby 3+) Circle = Struct.new(:radius) Rect = Struct.new(:width, :height) Triangle = Struct.new(:base, :height) def area(shape) case shape in Circle[radius:] then Math::PI * radius ** 2 in Rect[width:, height:] then width * height in Triangle[base:, height:] then 0.5 * base * height end end puts area(Circle.new(5)).round(2) puts area(Rect.new(4, 6)) puts area(Triangle.new(3, 8))
import kotlin.math.PI sealed class Shape data class Circle(val radius: Double) : Shape() data class Rect(val width: Double, val height: Double) : Shape() data class Triangle(val base: Double, val height: Double) : Shape() fun area(shape: Shape): Double = when (shape) { is Circle -> PI * shape.radius * shape.radius is Rect -> shape.width * shape.height is Triangle -> 0.5 * shape.base * shape.height } fun main() { println("%.2f".format(area(Circle(5.0)))) println(area(Rect(4.0, 6.0))) println(area(Triangle(3.0, 8.0))) }

A sealed class restricts which classes can inherit from it β€” all subclasses must be defined in the same package. This makes when expressions on a sealed class exhaustive: the compiler can verify that every possible subclass is handled, eliminating the need for an else branch. This is the Kotlin equivalent of Ruby 3's case/in pattern matching on known Struct types, but with compile-time exhaustiveness checking.

Result-style sealed class
def safe_divide(numerator, denominator) return { error: "Division by zero" } if denominator == 0 { value: numerator.to_f / denominator } end result = safe_divide(10, 2) case result in { value: } then puts "Result: #{value}" in { error: } then puts "Error: #{error}" end result2 = safe_divide(5, 0) case result2 in { value: } then puts "Result: #{value}" in { error: } then puts "Error: #{error}" end
sealed class MathResult data class Success(val value: Double) : MathResult() data class Failure(val reason: String) : MathResult() fun safeDivide(numerator: Double, denominator: Double): MathResult = if (denominator == 0.0) Failure("Division by zero") else Success(numerator / denominator) fun main() { val result = safeDivide(10.0, 2.0) val result2 = safeDivide(5.0, 0.0) for (outcome in listOf(result, result2)) { when (outcome) { is Success -> println("Result: ${outcome.value}") is Failure -> println("Error: ${outcome.reason}") } } }

Sealed classes model a closed set of outcomes β€” a common pattern for error handling without exceptions. The when expression on a sealed class is exhaustive: the compiler ensures all variants are covered. This pattern is more explicit than Ruby's hash-based result returns, and more structured than raw exception handling, making the possible outcomes part of the function's type signature.

Error Handling
try / catch / finally
begin result = 10 / 0 rescue ZeroDivisionError => error puts "Caught: #{error.message}" rescue RuntimeError => error puts "Runtime error: #{error.message}" ensure puts "This always runs" end
fun main() { try { val result = 10 / 0 println(result) } catch (error: ArithmeticException) { println("Caught: ${error.message}") } catch (error: RuntimeException) { println("Runtime error: ${error.message}") } finally { println("This always runs") } }

Kotlin's try/catch/finally maps directly to Ruby's begin/rescue/ensure. The structural correspondence is nearly one-to-one: both support multiple rescue/catch clauses to handle different exception types, and both have a cleanup block that always runs. Unlike Java, Kotlin has no checked exceptions β€” all exceptions are unchecked, so there is no throws declaration required.

try as expression
def parse_integer(text) Integer(text) rescue ArgumentError nil end result = parse_integer("42") puts result.inspect # 42 result = parse_integer("not a number") puts result.inspect # nil
fun parseIntOrNull(text: String): Int? = try { text.toInt() } catch (error: NumberFormatException) { null } fun main() { val result = parseIntOrNull("42") println(result) // 42 val missing = parseIntOrNull("not a number") println(missing) // null }

Like Ruby's begin/rescue, Kotlin's try is an expression β€” it returns the value of the last expression in the try block on success, or the value of the catch block on failure. This enables a concise "parse or return null" pattern. The equivalent Ruby idiom uses rescue inline or wraps in a method that returns nil on rescue.

Custom exceptions
class ValidationError < StandardError def initialize(field, message) @field = field super("#{field}: #{message}") end end def validate_age(age) raise ValidationError.new("age", "must be positive") if age < 0 raise ValidationError.new("age", "must be under 150") if age > 150 age end begin validate_age(-5) rescue ValidationError => error puts "Validation failed: #{error.message}" end
class ValidationError(val field: String, reason: String) : Exception("$field: $reason") fun validateAge(age: Int): Int { if (age < 0) throw ValidationError("age", "must be positive") if (age > 150) throw ValidationError("age", "must be under 150") return age } fun main() { try { validateAge(-5) } catch (error: ValidationError) { println("Validation failed: ${error.message}") } }

Custom Kotlin exceptions extend Exception (or any of its subclasses) using the same primary-constructor and : inheritance syntax as regular classes. Passing arguments to the parent constructor uses : Exception(message) in the class header β€” analogous to Ruby's super("message") inside initialize. Kotlin uses throw (not raise) to signal an exception.

runCatching (functional style)
require "json" # Ruby has no built-in Result type; simulate with rescue def parse_json(text) { ok: true, value: JSON.parse(text) } rescue JSON::ParserError => error { ok: false, error: error.message } end result = parse_json('{"name":"Alice"}') puts result[:value]["name"] if result[:ok] bad = parse_json("not json") puts "Error: #{bad[:error]}" unless bad[:ok]
fun riskyParse(text: String): Int = text.toInt() fun main() { // runCatching returns Result<T> β€” wraps success or failure val outcome = runCatching { riskyParse("42") } println(outcome.isSuccess) // true println(outcome.getOrNull()) // 42 val badOutcome = runCatching { riskyParse("oops") } println(badOutcome.isFailure) // true println(badOutcome.getOrDefault(0)) // 0 // Map over success, handle failure val message = badOutcome .map { "Parsed: $it" } .getOrElse { error -> "Failed: ${error.message}" } println(message) }

Kotlin's runCatching { } executes a block and wraps the outcome in a Result<T> β€” either a success holding the value or a failure holding the exception. This provides a functional, exception-free error-handling style. The Result type supports map, getOrNull, getOrDefault, and getOrElse, allowing error handling to be chained like collection operations rather than structured with try/catch.