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.
# 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.
# 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.
# 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.
# 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.
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.
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.
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.
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.
# 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.
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.
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.
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 { }.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
(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.
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.
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.
# 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
# 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.
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.
# 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.
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.
# 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.
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.
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.
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.
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.
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.