PONY λ M2 Modula-2

Ruby.CodeCompared.To/Scala

An interactive executable cheatsheet for Rubyists learning Scala

Ruby 4.0 Scala 2.13
Output & Basics
Hello World
puts "Hello, Scala!"
object Main { def main(args: Array[String]): Unit = { println("Hello, Scala!") } }

Every runnable Scala program requires a main method on an object — Scala 3 introduced a simpler @main annotation, but the classic object Main wrapper is the universally compatible form. The println function appends a newline, mirroring Ruby's puts.

print vs println
print "Hello, " print "World!" puts "" # newline puts "Done"
object Main { def main(args: Array[String]): Unit = { print("Hello, ") print("World!") println() // newline only println("Done") } }

The print function writes without a trailing newline; println adds one. Both mirror Ruby's print and puts. A bare println() emits just a newline, equivalent to Ruby's puts "".

Comments
# Single-line comment greeting = "Hello" # inline comment =begin Multi-line comment block (rarely used in practice) =end puts greeting
object Main { def main(args: Array[String]): Unit = { // Single-line comment val greeting = "Hello" // inline comment /* Multi-line comment block */ /** Scaladoc comment — appears in generated API documentation. * Use on classes, objects, methods, and fields. */ println(greeting) } }

Scala uses // for single-line comments and /* ... */ for block comments. Block comments may be nested, which is unusual for C-family languages. The /** ... */ Scaladoc format is the idiomatic documentation comment — the scaladoc tool uses it to generate API documentation, similar to RDoc in Ruby.

String interpolation
name = "Alice" count = 42 puts "Hello, #{name}!" puts "There are #{count} items" puts "Next year she will be #{30 + 1}"
object Main { def main(args: Array[String]): Unit = { val name = "Alice" val count = 42 println(s"Hello, ${name}!") println(s"There are ${count} items") println(s"Next year she will be ${30 + 1}") } }

Scala's s"..." prefix enables string interpolation using ${expression} syntax. The s prefix must appear immediately before the opening quote. Ruby's #{} is always active inside double-quoted strings; Scala requires the explicit s prefix.

Formatted output
price = 9.99 temperature = 98.6 puts format("Price: %.2f", price) puts format("Temp: %.1f F", temperature) puts format("%-10s | %5d", "Alice", 42)
object Main { def main(args: Array[String]): Unit = { val price = 9.99 val temperature = 98.6 println(f"Price: ${price}%.2f") println(f"Temp: ${temperature}%.1f F") println(f"${"Alice"}%-10s | ${42}%5d") } }

Scala's f"..." interpolator applies printf-style format specifiers directly after each expression: ${value}%format. The format string is checked at compile time, so a type mismatch (e.g., using %d with a Double) is a compile error rather than a runtime surprise.

Variables & Values
val — immutable binding
# Ruby variables are always reassignable greeting = "Hello" puts greeting
object Main { def main(args: Array[String]): Unit = { val greeting = "Hello" // greeting = "Hi" // compile error: reassignment to val println(greeting) } }

The val keyword declares an immutable binding — once assigned, the name cannot point to a different value. Ruby has no equivalent; all Ruby local variables are reassignable. The val/var distinction is Scala's primary mechanism for expressing immutability intent, and idiomatic Scala strongly prefers val.

var — mutable variable
counter = 0 counter += 1 counter += 1 puts counter
object Main { def main(args: Array[String]): Unit = { var counter = 0 counter += 1 counter += 1 println(counter) } }

The var keyword declares a mutable variable that can be reassigned. In idiomatic Scala, var is used sparingly — most state is managed through immutable vals and functional transformations. When you reach for var, consider whether a foldLeft or recursive method would express the same intent more cleanly.

Type inference
# Ruby is dynamically typed — no annotations needed number = 42 text = "hello" ratio = 3.14 puts number.class puts text.class puts ratio.class
object Main { def main(args: Array[String]): Unit = { val number = 42 // inferred as Int val text = "hello" // inferred as String val ratio = 3.14 // inferred as Double val flag = true // inferred as Boolean println(number.getClass.getSimpleName) println(text.getClass.getSimpleName) println(ratio.getClass.getSimpleName) println(flag.getClass.getSimpleName) } }

Scala infers types automatically using Hindley-Milner-style inference, so most local bindings need no annotation. Unlike Ruby (which determines types at runtime), Scala resolves types at compile time — the Int, String, and Double assignments above are fully type-checked before the program ever runs.

Explicit type annotations
# Ruby doesn't have type annotations in standard Ruby number = 42 text = "hello" puts number
object Main { def main(args: Array[String]): Unit = { val number: Int = 42 val text: String = "hello" val ratio: Double = 3.14 val flag: Boolean = true val items: List[Int] = List(1, 2, 3) println(s"${number}, ${text}, ${ratio}, ${flag}") println(items) } }

Type annotations follow a colon after the variable name. They are optional when the type is inferable but required when the compiler cannot determine the type from the right-hand side. Explicit annotations serve as documentation and catch type mismatches at the declaration site rather than at first use.

Lazy val
# Ruby uses memoization for lazy evaluation def expensive_value @expensive_value ||= begin puts "computing..." 42 end end puts "before access" puts expensive_value puts expensive_value # uses cached result
object Main { def main(args: Array[String]): Unit = { lazy val result = { println("computing...") 42 } println("before access") println(result) println(result) // computed only once; value is cached } }

A lazy val is not evaluated until its first access, and after that the result is cached. Ruby achieves the same pattern with the ||= memoization idiom, but Scala's lazy val is built into the language, thread-safe by default, and carries no extra syntax at the call site.

Tuple destructuring
first, second, third = [10, 20, 30] puts "#{first}, #{second}, #{third}" pair = ["Alice", 30] name, age = pair puts "#{name} is #{age}"
object Main { def main(args: Array[String]): Unit = { val triple = (10, 20, 30) val (first, second, third) = triple println(s"${first}, ${second}, ${third}") val person = ("Alice", 30) val (name, age) = person println(s"${name} is ${age}") println(triple._1) // 1-based positional access } }

Scala tuples are destructured via pattern matching on the left-hand side of val. Elements are also accessible as ._1, ._2, etc. (1-based). Ruby uses parallel assignment for the same purpose; Scala's version is type-safe — the compiler checks that the tuple has the right arity and types at compile time.

Strings
s"" interpolation
name = "Alice" age = 30 puts "Hello, #{name}!" puts "#{name} is #{age} years old" puts "Next year she will be #{age + 1}"
object Main { def main(args: Array[String]): Unit = { val name = "Alice" val age = 30 println(s"Hello, ${name}!") println(s"${name} is ${age} years old") println(s"Next year she will be ${age + 1}") println(s"Uppercased: ${name.toUpperCase}") } }

The s"..." prefix activates string interpolation. Any expression can appear inside ${}, including method calls, arithmetic, and conditional expressions. Ruby's #{} is always active inside double-quoted strings; Scala requires the explicit s prefix to opt in.

f"" formatted interpolation
temperature = 98.6 score = 0.876 puts format("Temp: %.1f", temperature) puts format("Score: %.2f%%", score * 100)
object Main { def main(args: Array[String]): Unit = { val temperature = 98.6 val score = 0.876 println(f"Temp: ${temperature}%.1f") println(f"Score: ${score * 100}%.2f%%") println(f"${"Alice"}%-10s | ${42}%05d") } }

The f"..." prefix applies printf-style format specifiers. The format appears immediately after the expression: ${value}%format. Unlike Ruby's format or sprintf, Scala checks the format string at compile time — passing a String to %d is a compile error.

Multi-line and raw strings
poem = <<~HEREDOC Line one Line two Line three HEREDOC puts poem.strip pattern = 'd+.d+' # single-quote: no escape processing puts pattern
object Main { def main(args: Array[String]): Unit = { val poem = """Line one |Line two |Line three""".stripMargin println(poem) val pattern = raw"d+.d+" // raw"": no escape processing println(pattern) } }

Triple-quoted strings ("""...""") span multiple lines without escape sequences. The pipe character with .stripMargin removes leading whitespace up to the pipe on each line — the idiomatic way to align multi-line strings with surrounding code. The raw"..." prefix disables escape processing, equivalent to Ruby's single-quoted strings.

String methods
text = "hello, world" puts text.length puts text.upcase puts text.capitalize puts text.include?("world") puts text.split(", ").inspect puts text.gsub("world", "Scala")
object Main { def main(args: Array[String]): Unit = { val text = "hello, world" println(text.length) println(text.toUpperCase) println(text.capitalize) println(text.contains("world")) println(text.split(", ").mkString("[", ", ", "]")) println(text.replace("world", "Scala")) } }

Scala strings are Java strings, so the full Java String API is available. The naming follows Java conventions (toUpperCase instead of Ruby's upcase, contains instead of include?). The mkString method is Scala-specific and formats collections into a string with a delimiter, prefix, and suffix.

String operations
puts "hello" + " " + "world" puts "ha" * 3 puts " spaces ".strip puts "abc".reverse puts "hello world".start_with?("hello") puts "hello world".index("world")
object Main { def main(args: Array[String]): Unit = { println("hello" + " " + "world") println("ha" * 3) println(" spaces ".trim) println("abc".reverse) println("hello world".startsWith("hello")) println("hello world".indexOf("world")) } }

Scala strings support + concatenation and * repetition just like Ruby. The trim method removes leading and trailing whitespace (Ruby uses strip). The startsWith and indexOf names follow Java convention rather than Ruby's predicate-style start_with?.

Type conversion to/from String
puts 42.to_s puts "42".to_i puts 3.14.to_s puts "3.14".to_f puts true.to_s
object Main { def main(args: Array[String]): Unit = { println(42.toString) println("42".toInt) println(3.14.toString) println("3.14".toDouble) println(true.toString) println("true".toBoolean) println(42.toBinaryString) } }

Scala provides .toString on every type and .toInt, .toDouble, .toBoolean etc. on strings. These mirror Ruby's .to_i, .to_f, and .to_s. Unlike Ruby's to_i (which returns 0 for non-numeric strings), Scala's .toInt throws NumberFormatException on invalid input.

Collections
List (immutable)
numbers = [1, 2, 3, 4, 5] puts numbers.first puts numbers.last puts numbers.length puts ([0] + numbers).inspect
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers.head) println(numbers.last) println(numbers.length) val extended = 0 :: numbers // prepend with :: (cons) println(extended) println(numbers) // original is unchanged } }

Scala's List is an immutable singly-linked list. The :: (cons) operator prepends an element in O(1), returning a new list. Appending to the end is O(n) and discouraged — use Vector when you need efficient random access or frequent appending. Ruby's Array combines both roles.

Vector (immutable indexed)
items = [1, 2, 3, 4, 5] puts items[1] items_copy = items.dup items_copy[1] = 99 puts items_copy.inspect puts items.inspect # original unchanged
object Main { def main(args: Array[String]): Unit = { val numbers = Vector(1, 2, 3, 4, 5) println(numbers(1)) // O(log n) access val updated = numbers.updated(1, 99) // returns a new Vector println(updated) println(numbers) // original is unchanged val appended = numbers :+ 6 println(appended) } }

A Vector is an immutable indexed sequence with effectively O(1) random access and O(1) append. It is the default choice when you need an indexed, immutable collection. Unlike Scala's List, it supports efficient access at any position — similar to a Ruby array but persistent (every operation returns a new structure).

Array (mutable)
items = Array.new(3, 0) items[1] = 42 puts items.inspect numbers = [10, 20, 30] puts numbers.map { |n| n * 2 }.inspect
object Main { def main(args: Array[String]): Unit = { val items = Array(10, 20, 30) items(1) = 99 // mutable — in-place mutation println(items.mkString("[", ", ", "]")) val zeroes = Array.fill(3)(0) println(zeroes.mkString("[", ", ", "]")) } }

Scala's Array is a mutable, fixed-size sequence backed by a JVM primitive array. Elements are accessed and updated with parentheses: items(1) reads, items(1) = 99 writes. Scala's other collection types (List, Vector) are immutable by default — reserve Array for performance-critical interoperability with Java libraries.

Map (immutable)
scores = { "Alice" => 95, "Bob" => 87 } puts scores["Alice"] puts scores.key?("Charlie") puts scores.fetch("Charlie", 0) puts scores.merge({ "Charlie" => 92 }).inspect
object Main { def main(args: Array[String]): Unit = { val scores = Map("Alice" -> 95, "Bob" -> 87) println(scores("Alice")) println(scores.contains("Charlie")) println(scores.getOrElse("Charlie", 0)) val updated = scores + ("Charlie" -> 92) println(updated) } }

Scala's default Map is immutable. The -> arrow creates a key-value pair (a Tuple2). Adding an entry with + returns a new map — the original is unchanged. The getOrElse method is idiomatic for safe key lookup, equivalent to Ruby's Hash#fetch with a default.

Set (immutable)
require 'set' colors = Set["red", "green", "blue"] puts colors.include?("red") extended = colors | Set["yellow"] puts extended.to_a.sort.inspect puts (colors & Set["red", "orange"]).to_a.inspect
object Main { def main(args: Array[String]): Unit = { val colors = Set("red", "green", "blue") println(colors.contains("red")) val extended = colors + "yellow" println(extended.toList.sorted) println(colors.intersect(Set("red", "orange"))) println(colors.union(Set("yellow", "purple"))) } }

Scala's default Set is immutable. Adding an element with + returns a new set. The intersect and union methods correspond to Ruby's & and | operators on Set. Unlike Ruby (which requires require 'set'), Scala's Set is imported automatically.

Range
puts (1..5).to_a.inspect puts (1...5).to_a.inspect # exclusive end puts (1..10).step(2).to_a.inspect
object Main { def main(args: Array[String]): Unit = { val inclusive = 1 to 5 val exclusive = 1 until 5 val stepping = 1 to 10 by 2 println(inclusive.toList) println(exclusive.toList) println(stepping.toList) println((1 to 5).sum) } }

Scala's to creates an inclusive range; until creates an exclusive one — mirroring Ruby's .. and ... operators. The by method sets the step. Ranges are lazy sequences and support the full collection API (map, filter, sum, etc.) without materializing all elements.

Tuple
pair = [42, "hello"] triple = ["Alice", 30, true] puts pair[0] puts pair[1] name, age, flag = triple puts "#{name} is #{age}"
object Main { def main(args: Array[String]): Unit = { val pair: (Int, String) = (42, "hello") val triple: (String, Int, Boolean) = ("Alice", 30, true) println(pair._1) println(pair._2) val (name, age, _) = triple // destructuring; _ discards the Boolean println(s"${name} is ${age}") } }

Scala tuples are fixed-length, heterogeneous sequences. Elements are accessed positionally with ._1, ._2, etc. (1-based). Tuples support pattern-matching destructuring. Unlike Ruby arrays, Scala tuples are typed at each position — (String, Int, Boolean) is a distinct type from (Int, String).

Common collection operations
numbers = [1, 2, 3, 4, 5] puts numbers.map { |n| n * 2 }.inspect puts numbers.select { |n| n.odd? }.inspect puts numbers.sum puts numbers.take(3).inspect puts numbers.drop(2).inspect
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers.map(_ * 2)) println(numbers.filter(_ % 2 != 0)) println(numbers.sum) println(numbers.take(3)) println(numbers.drop(2)) println(numbers.mkString(", ")) } }

Scala's collection API mirrors Ruby's Enumerable: map, filter, sum, take, and drop all behave as expected. The underscore shorthand (_ * 2) is Scala's concise anonymous-function syntax, equivalent to Ruby's { |n| n * 2 }. These operations return new collections without mutating the original.

Control Flow
if/else as an expression
number = 10 label = number > 5 ? "big" : "small" puts label grade = if number >= 90 then "A" elsif number >= 80 then "B" elsif number >= 70 then "C" else "F" end puts grade
object Main { def main(args: Array[String]): Unit = { val number = 10 val label = if (number > 5) "big" else "small" println(label) val grade = if (number >= 90) "A" else if (number >= 80) "B" else if (number >= 70) "C" else "F" println(grade) } }

In Scala, if/else is an expression that returns a value — there is no separate ternary operator. Ruby has the same design (the if expression returns the last evaluated value), but also provides the ?: ternary for concise one-liners. Scala's if plays both roles.

while loop
count = 0 while count < 3 puts count count += 1 end
object Main { def main(args: Array[String]): Unit = { var count = 0 while (count < 3) { println(count) count += 1 } } }

Scala's while loop is a statement (returns Unit), not an expression. Idiomatic Scala prefers recursion or higher-order functions (foreach, map, foldLeft) over imperative loops. When a while loop appears in Scala code, it almost always accompanies a var — both are signals that the code could be rewritten functionally.

for loop (iteration)
(1..5).each { |index| puts index } fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit }
object Main { def main(args: Array[String]): Unit = { for (index <- 1 to 5) { println(index) } val fruits = List("apple", "banana", "cherry") for (fruit <- fruits) { println(fruit) } } }

Scala's for (element <- collection) iterates over any iterable. Without yield it is a statement (like Ruby's each); with yield it is an expression that collects results (like Ruby's map). The <- arrow is called a "generator" and is the core of Scala's for comprehensions.

match expression
day = "Monday" result = case day when "Monday" then "Start of the week" when "Friday" then "End of the week" else "Middle of the week" end puts result
object Main { def main(args: Array[String]): Unit = { val day = "Monday" val result = day match { case "Monday" => "Start of the week" case "Friday" => "End of the week" case _ => "Middle of the week" } println(result) } }

Scala's match is an expression that returns a value — equivalent to Ruby's case/when used as an expression. The underscore _ is the catch-all wildcard, like Ruby's bare else. Unlike Ruby's case/when, Scala's match is exhaustiveness-checked when matching on sealed types.

match with guards
score = 85 grade = case score when 90..100 then "A" when 80...90 then "B" when 70...80 then "C" else "F" end puts grade
object Main { def main(args: Array[String]): Unit = { val score = 85 val grade = score match { case s if s >= 90 => "A" case s if s >= 80 => "B" case s if s >= 70 => "C" case _ => "F" } println(grade) } }

A guard clause (if condition after the pattern) adds a condition that must be true for the case to match. The pattern variable (here s) is bound to the matched value and available in both the guard and the body. This is equivalent to Ruby's when value then with conditional logic.

Pattern Matching
Matching on types
def describe(value) case value when Integer then "integer: #{value}" when String then "string: #{value}" when Array then "array: #{value.length} elements" else "unknown" end end puts describe(42) puts describe("hello") puts describe([1, 2, 3])
object Main { def describe(value: Any): String = value match { case number: Int => s"integer: ${number}" case text: String => s"string: ${text}" case items: List[_]=> s"list: ${items.length} elements" case _ => "unknown" } def main(args: Array[String]): Unit = { println(describe(42)) println(describe("hello")) println(describe(List(1, 2, 3))) } }

Scala binds the matched value to a name (number: Int) for use in the case body. The Any type is Scala's top type — the equivalent of Ruby's Object. Type matching in Scala is checked at compile time where possible, whereas Ruby's case/when with classes calls === at runtime.

Matching on tuples
point = [0, 3] description = case point in [0, 0] then "origin" in [_, 0] then "x-axis" in [0, _] then "y-axis" else "other point" end puts description
object Main { def main(args: Array[String]): Unit = { val point = (0, 3) val description = point match { case (0, 0) => "origin" case (x, 0) => s"on x-axis at ${x}" case (0, y) => s"on y-axis at ${y}" case (x, y) => s"at (${x}, ${y})" } println(description) } }

Tuples decompose in match exactly as they do in destructuring assignment. Named variables (x, y) are bound to the tuple elements and available in the case body. Unlike Ruby's case/when (which uses ===), Scala's tuple matching works structurally at compile time.

Matching on lists
def describe(list) case list when [] then "empty" when [Integer] then "just #{list[0]}" else head, *tail = list "head=#{head}, tail=#{tail.inspect}" end end puts describe([]) puts describe([42]) puts describe([1, 2, 3])
object Main { def describe(items: List[Int]): String = items match { case Nil => "empty" case head :: Nil => s"just ${head}" case head :: tail => s"head=${head}, tail=${tail}" } def main(args: Array[String]): Unit = { println(describe(List())) println(describe(List(42))) println(describe(List(1, 2, 3))) } }

The Nil pattern matches an empty list; head :: tail deconstructs a non-empty list into its first element and the rest. This mirrors the Prolog-style [H|T] and is the idiomatic way to write recursive list-processing functions in Scala. Ruby achieves the same with first, *rest = array.

Sealed traits and exhaustive matching
class Shape; end class Circle < Shape def initialize(radius) = @radius = radius def area = Math::PI * @radius ** 2 end class Rectangle < Shape def initialize(width, height) @width, @height = width, height end def area = @width * @height end shapes = [Circle.new(5.0), Rectangle.new(4.0, 6.0)] shapes.each { |shape| puts shape.area.round(2) }
object Main { sealed trait Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape def area(shape: Shape): Double = shape match { case Circle(radius) => Math.PI * radius * radius case Rectangle(width, height) => width * height } def main(args: Array[String]): Unit = { val shapes = List(Circle(5.0), Rectangle(4.0, 6.0)) shapes.foreach(shape => println(f"${area(shape)}%.2f")) } }

A sealed trait can only be extended within the same source file. This lets the compiler verify that a match is exhaustive — if you forget to handle Rectangle, the compiler issues a warning. Ruby has no equivalent; missing case branches are a runtime surprise. The sealed + case class + match combination is one of Scala's most celebrated features.

Wildcards and binding in patterns
numbers = [10, -5, 0, 42] numbers.each do |number| result = if number > 0 then "positive: #{number}" elsif number == 0 then "zero" else "negative: #{number}" end puts result end
object Main { def main(args: Array[String]): Unit = { val numbers = List(10, -5, 0, 42) numbers.foreach { number => val label = number match { case 0 => "zero" case positive if positive > 0 => s"positive: ${positive}" case negative => s"negative: ${negative}" } println(label) } } }

A bare name in a pattern (like positive or negative) binds the matched value to that name — it acts as both a catch-all and a binding. Guards then refine the match further. An underscore _ is a discard wildcard — it matches anything without binding a name. This combination gives Scala patterns expressive power equivalent to a nested Ruby conditional chain, but in a single match expression.

Methods & Functions
Defining methods
def greet(name) "Hello, #{name}!" end def add(x, y) x + y end puts greet("Alice") puts add(3, 4)
object Main { def greet(name: String): String = s"Hello, ${name}!" def add(x: Int, y: Int): Int = x + y def main(args: Array[String]): Unit = { println(greet("Alice")) println(add(3, 4)) } }

Scala methods require type annotations for parameters. The return type can usually be inferred for simple single-expression methods, but annotating it is good practice. A method body that is a single expression can use = expression without braces — the same one-liner style Ruby 4 introduced with def foo(x) = x * 2.

Default parameter values
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
object Main { def greet(name: String, greeting: String = "Hello"): String = s"${greeting}, ${name}!" def main(args: Array[String]): Unit = { println(greet("Alice")) println(greet("Bob", "Hi")) println(greet("Charlie", greeting = "Hey")) } }

Default parameter values in Scala work identically to Ruby — trailing parameters with defaults can be omitted at the call site. Scala also allows named arguments (here greeting = "Hey") to skip positional arguments or clarify intent — the same feature as Ruby's keyword arguments.

Named arguments
def create_user(name:, age:, admin: false) "#{name}, #{age}, admin=#{admin}" end puts create_user(name: "Alice", age: 30) puts create_user(age: 25, name: "Bob", admin: true)
object Main { def createUser(name: String, age: Int, admin: Boolean = false): String = s"${name}, ${age}, admin=${admin}" def main(args: Array[String]): Unit = { println(createUser("Alice", 30)) println(createUser(name = "Bob", age = 25, admin = true)) println(createUser(age = 20, name = "Charlie")) } }

Scala supports named arguments at every call site — any argument can be passed by name. Named arguments can appear in any order and skip over optional parameters with defaults. Ruby restricts named parameters to keyword arguments (with the name: syntax); Scala makes naming available for all parameters.

Recursive methods
def factorial(n) return 1 if n <= 1 n * factorial(n - 1) end def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end puts factorial(5) puts fibonacci(10)
object Main { def factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1) def fibonacci(n: Int): Int = n match { case 0 | 1 => n case _ => fibonacci(n - 1) + fibonacci(n - 2) } def main(args: Array[String]): Unit = { println(factorial(5)) println(fibonacci(10)) } }

Scala recursive methods require an explicit return type annotation (the compiler cannot infer it for recursive definitions). The @tailrec annotation, when added, makes the compiler verify that a method is tail-recursive and can be optimized into a loop — Ruby has no equivalent guarantee.

Multiple parameter lists (currying)
multiply = ->(x) { ->(y) { x * y } } double = multiply.(2) puts double.(5) puts double.(10)
object Main { def multiply(x: Int)(y: Int): Int = x * y def main(args: Array[String]): Unit = { println(multiply(3)(4)) val double = multiply(2) _ // partial application println(double(5)) println(double(10)) } }

A method with multiple parameter lists can be partially applied by supplying only the first argument list and using _ as a placeholder. The result is a function waiting for the remaining arguments. This enables currying idioms identical to Ruby's curry method on Proc. Multiple parameter lists are also used to improve type inference and to separate type parameters from value parameters.

Generic methods
def first_and_last(collection) [collection.first, collection.last] end puts first_and_last([1, 2, 3]).inspect puts first_and_last(["a", "b", "c"]).inspect
object Main { def firstAndLast[Element](items: List[Element]): (Element, Element) = (items.head, items.last) def wrap[Container](value: Container): List[Container] = List(value) def main(args: Array[String]): Unit = { println(firstAndLast(List(1, 2, 3))) println(firstAndLast(List("a", "b", "c"))) println(wrap(42)) println(wrap("hello")) } }

Type parameters (written in square brackets, [Element]) make a method generic — the compiler substitutes the actual type at each call site. Ruby achieves the same result dynamically through duck typing; Scala's generics are checked at compile time. The convention is to use single uppercase letters (A, B) or descriptive names in brackets.

Higher-Order Functions
map
numbers = [1, 2, 3, 4, 5] puts numbers.map { |n| n * 2 }.inspect puts numbers.map { |n| n.to_s }.inspect puts numbers.map { |n| "item-#{n}" }.inspect
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers.map(_ * 2)) println(numbers.map(_.toString)) println(numbers.map(number => s"item-${number}")) } }

The _ * 2 syntax is a shorthand for a single-argument anonymous function. The underscore stands for the argument. For multi-argument functions or when the argument is used more than once, write the full lambda: number => number * number. Both forms correspond to Ruby's block syntax ({ |n| n * 2 }).

filter and partition
numbers = [1, 2, 3, 4, 5, 6] puts numbers.select(&:even?).inspect puts numbers.reject(&:even?).inspect evens, odds = numbers.partition(&:even?) puts "evens=#{evens.inspect}, odds=#{odds.inspect}"
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5, 6) println(numbers.filter(_ % 2 == 0)) println(numbers.filterNot(_ % 2 == 0)) val (evens, odds) = numbers.partition(_ % 2 == 0) println(s"evens=${evens}, odds=${odds}") } }

Scala's filter corresponds to Ruby's select; filterNot corresponds to reject. The partition method splits a collection into two — elements satisfying the predicate and elements not satisfying it — returning a tuple. Ruby's partition does the same and returns a two-element array.

foldLeft and reduce
numbers = [1, 2, 3, 4, 5] puts numbers.reduce(0) { |sum, n| sum + n } puts numbers.reduce(:+) puts numbers.inject(1, :*) words = ["hello", "world", "scala"] puts words.reduce { |acc, word| acc + word.capitalize }
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers.foldLeft(0)(_ + _)) println(numbers.reduce(_ + _)) println(numbers.product) val words = List("hello", "world", "scala") println(words.foldLeft("") { (accumulator, word) => accumulator + word.capitalize }) } }

foldLeft corresponds to Ruby's inject or reduce with an initial value. It takes the accumulator as the first argument and the current element as the second. reduce uses the first element as the initial accumulator. The product method is a shorthand for multiplying all elements, equivalent to inject(:*).

flatMap
sentences = ["hello world", "foo bar"] puts sentences.flat_map { |s| s.split(" ") }.inspect nested = [[1, 2], [3, 4], [5]] puts nested.flatten.inspect
object Main { def main(args: Array[String]): Unit = { val sentences = List("hello world", "foo bar") println(sentences.flatMap(_.split(" "))) val nested = List(List(1, 2), List(3, 4), List(5)) println(nested.flatten) println(nested.flatMap(identity)) // equivalent to flatten } }

flatMap applies a function that returns a collection to each element, then flattens one level — equivalent to Ruby's flat_map. It is the key mechanism behind Scala's for comprehensions: for { x <- xs; y <- ys } yield (x, y) desugars into nested flatMap calls.

Function values and lambdas
double = ->(n) { n * 2 } add = ->(x, y) { x + y } square = ->(n) { n * n } puts double.(5) puts add.(3, 4) puts [1, 2, 3].map(&double).inspect
object Main { def main(args: Array[String]): Unit = { val double: Int => Int = n => n * 2 val add: (Int, Int) => Int = (x, y) => x + y val square: Int => Int = n => n * n println(double(5)) println(add(3, 4)) println(square(7)) println(List(1, 2, 3).map(double)) } }

Function values in Scala use the => arrow syntax and are typed as A => B (single argument) or (A, B) => C (multiple). They correspond to Ruby's lambdas (->(x) { x * 2 }). A function value can be passed directly to a higher-order method without any special conversion — unlike Ruby, where & is needed to convert a Proc to a block.

Function composition
double = ->(n) { n * 2 } add_one = ->(n) { n + 1 } double_then_add = double >> add_one add_then_double = double << add_one puts double_then_add.(5) # (5*2)+1 = 11 puts add_then_double.(5) # (5+1)*2 = 12
object Main { def main(args: Array[String]): Unit = { val double: Int => Int = _ * 2 val addOne: Int => Int = _ + 1 val doubleThenAdd = double andThen addOne val addThenDouble = double compose addOne println(doubleThenAdd(5)) // (5*2)+1 = 11 println(addThenDouble(5)) // (5+1)*2 = 12 } }

andThen composes left-to-right: f andThen g applies f first, then g. compose composes right-to-left: f compose g applies g first, then f. Ruby 2.6 added >> (left-to-right) and << (right-to-left) to Proc — exactly the same semantics as Scala's andThen and compose.

Case Classes
Defining a case class
Person = Data.define(:name, :age) alice = Person.new(name: "Alice", age: 30) puts alice.name puts alice.age puts alice
object Main { case class Person(name: String, age: Int) def main(args: Array[String]): Unit = { val alice = Person("Alice", 30) println(alice.name) println(alice.age) println(alice) // toString: Person(Alice,30) } }

A case class automatically generates: a constructor, accessor methods for each field, toString, structural equals and hashCode, a copy method, and an unapply for pattern matching. Ruby's Data.define (Ruby 3.2+) is the closest equivalent, providing immutable value objects with similar auto-generated methods.

Structural equality
Point = Data.define(:x, :y) point1 = Point.new(x: 1, y: 2) point2 = Point.new(x: 1, y: 2) point3 = Point.new(x: 3, y: 4) puts point1 == point2 # true puts point1 == point3 # false puts point1.hash == point2.hash
object Main { case class Point(x: Int, y: Int) def main(args: Array[String]): Unit = { val point1 = Point(1, 2) val point2 = Point(1, 2) val point3 = Point(3, 4) println(point1 == point2) // true println(point1 == point3) // false println(point1.hashCode == point2.hashCode) // true } }

Case class equality compares fields structurally, not by object identity — two Point(1, 2) instances are ==. Regular Scala classes use reference equality by default (like Ruby objects compared with equal?). The consistent hashCode makes case class instances safe to use as Map keys or Set elements.

copy method
Person = Data.define(:name, :age, :admin) alice = Person.new(name: "Alice", age: 30, admin: false) bob = alice.with(name: "Bob") admin = alice.with(admin: true) puts alice puts bob puts admin
object Main { case class Person(name: String, age: Int, admin: Boolean = false) def main(args: Array[String]): Unit = { val alice = Person("Alice", 30) val bob = alice.copy(name = "Bob") val admin = alice.copy(admin = true) println(alice) println(bob) println(admin) } }

The copy method returns a new instance with specified fields changed and the rest unchanged. This enables the functional update pattern — modifying a value without mutation. Ruby's Data#with (Ruby 3.2+) provides the same capability. Both are the idiomatic way to "update" immutable value objects.

Case class pattern matching
Point = Data.define(:x, :y) points = [Point.new(x: 0, y: 0), Point.new(x: 3, y: 0), Point.new(x: 2, y: 5)] points.each do |point| puts case [point.x, point.y] in [0, 0] then "origin" in [_, 0] then "x-axis" in [0, _] then "y-axis" else "other" end end
object Main { case class Point(x: Int, y: Int) def describe(point: Point): String = point match { case Point(0, 0) => "origin" case Point(_, 0) => "on x-axis" case Point(0, _) => "on y-axis" case Point(x, y) => s"at (${x}, ${y})" } def main(args: Array[String]): Unit = { println(describe(Point(0, 0))) println(describe(Point(3, 0))) println(describe(Point(2, 5))) } }

Case classes are designed for pattern matching — the auto-generated unapply method makes the field values available as sub-patterns. Point(0, 0) in a match clause succeeds only when both x and y equal zero. This is far more readable than Ruby's equivalent using case with array decomposition.

Sealed traits with case classes (ADTs)
# Ruby pattern matching (3.2+) def handle(result) case result in { type: :success, value: Integer => n } "Got #{n}" in { type: :failure, message: String => msg } "Error: #{msg}" end end puts handle({ type: :success, value: 42 }) puts handle({ type: :failure, message: "not found" })
object Main { sealed trait Result case class Success(value: Int) extends Result case class Failure(message: String) extends Result def handle(result: Result): String = result match { case Success(value) => s"Got ${value}" case Failure(message) => s"Error: ${message}" } def main(args: Array[String]): Unit = { println(handle(Success(42))) println(handle(Failure("not found"))) } }

A sealed trait extended by case classes forms an algebraic data type (ADT) — a closed set of possible shapes. The compiler verifies that every match on the ADT handles all variants. This is Scala's idiomatic replacement for Ruby's symbol-and-hash conventions: the data structure enforces what a plain hash cannot.

Traits & OOP
Defining and using a trait
module Greetable def greet "Hello, I'm #{name}" end end class Person include Greetable attr_reader :name def initialize(name) = @name = name end puts Person.new("Alice").greet
object Main { trait Greetable { def name: String def greet: String = s"Hello, I'm ${name}" } class Person(val name: String) extends Greetable def main(args: Array[String]): Unit = { val alice = new Person("Alice") println(alice.greet) println(alice.name) } }

Scala traits correspond to Ruby modules — both provide mixin-based multiple inheritance. A trait can declare abstract members (no body) and provide default implementations. Classes extend traits with extends (first trait) and with (subsequent traits). Unlike Ruby modules, traits are also used as interfaces in Scala's type system.

Abstract trait (interface-style)
module Drawable def draw raise NotImplementedError, "#{self.class} must implement draw" end def erase puts "Erasing #{self.class.name}" end end
object Main { trait Drawable { def draw(): Unit // abstract — no body required def erase(): Unit = println(s"Erasing ${getClass.getSimpleName}") } class Circle(val radius: Double) extends Drawable { def draw(): Unit = println(s"Drawing Circle(radius=${radius})") } def main(args: Array[String]): Unit = { val circle = new Circle(5.0) circle.draw() circle.erase() } }

An abstract member in a trait has no body — any class extending the trait must provide an implementation or be declared abstract itself. The compiler enforces this, unlike Ruby where raise NotImplementedError is a runtime convention rather than a compile-time guarantee.

Mixing multiple traits
module Flyable def fly = puts "#{self.class} is flying" end module Swimmable def swim = puts "#{self.class} is swimming" end class Duck include Flyable include Swimmable end duck = Duck.new duck.fly duck.swim
object Main { trait Flyable { def fly(): Unit = println(s"${getClass.getSimpleName} is flying") } trait Swimmable{ def swim(): Unit = println(s"${getClass.getSimpleName} is swimming") } class Duck extends Flyable with Swimmable def main(args: Array[String]): Unit = { val duck = new Duck() duck.fly() duck.swim() println(duck.isInstanceOf[Flyable]) println(duck.isInstanceOf[Swimmable]) } }

A class mixes in multiple traits using extends FirstTrait with SecondTrait with ThirdTrait. Scala uses C3 linearization to determine method resolution order when multiple traits define the same method — the same algorithm Ruby uses. The first trait after extends can also be a class, while additional ones with with must be traits.

Abstract class
class Animal def initialize(name) = @name = name def speak = raise NotImplementedError def describe = puts "I am #{@name}" end class Dog < Animal def speak = puts "Woof!" end dog = Dog.new("Rex") dog.describe dog.speak
object Main { abstract class Animal(val name: String) { def speak(): Unit def describe(): Unit = println(s"I am ${name}") } class Dog(name: String) extends Animal(name) { def speak(): Unit = println("Woof!") } def main(args: Array[String]): Unit = { val dog = new Dog("Rex") dog.describe() dog.speak() } }

An abstract class may declare abstract members (no body) and provide concrete ones. Unlike traits, abstract classes can have constructor parameters — which makes them the right choice when the base type needs initialization logic. A class may extend only one abstract class but may mix in many traits; combine both approaches as needed.

Inheritance and override
class Vehicle def initialize(speed) = @speed = speed def describe = "Vehicle going #{@speed} mph" end class Car < Vehicle def describe = "Car going #{@speed} mph" end puts Car.new(60).describe puts Car.new(60).is_a?(Vehicle)
object Main { class Vehicle(val speed: Int) { def describe(): String = s"Vehicle going ${speed} mph" } class Car(speed: Int) extends Vehicle(speed) { override def describe(): String = s"Car going ${speed} mph" } def main(args: Array[String]): Unit = { val car = new Car(60) println(car.describe()) println(car.isInstanceOf[Vehicle]) // true println(new Vehicle(60).describe()) } }

The override keyword is mandatory when redefining a method from a parent class. This prevents accidental shadowing — if the parent method's signature changes, the compiler reports an error on the child's override. Ruby silently allows method redefinition without any keyword, so the same class of bugs goes undetected.

Option Type
Some and None
def find_user(name) users = { "Alice" => 30, "Bob" => 25 } users[name] end puts find_user("Alice").nil? ? "not found" : "found: #{find_user("Alice")}" puts find_user("Charlie").nil? ? "not found" : "found"
object Main { def findUser(name: String): Option[Int] = { val users = Map("Alice" -> 30, "Bob" -> 25) users.get(name) } def main(args: Array[String]): Unit = { println(findUser("Alice")) println(findUser("Charlie")) println(findUser("Alice").isDefined) println(findUser("Charlie").isEmpty) } }

Option[A] is a container that holds either Some(value) or None. It makes absence explicit in the type — a method returning Option[Int] forces the caller to handle the "not found" case at compile time. Ruby uses nil for the same purpose, but the compiler cannot enforce that callers check for it.

map on Option
users = { "Alice" => 30 } age = users["Alice"] message = age ? "In 10 years: #{age + 10}" : "not found" puts message age = users["Charlie"] message = age ? "In 10 years: #{age + 10}" : "not found" puts message
object Main { def main(args: Array[String]): Unit = { val users = Map("Alice" -> 30) val alice = users.get("Alice") val charlie = users.get("Charlie") println(alice.map(age => s"In 10 years: ${age + 10}")) println(charlie.map(age => s"In 10 years: ${age + 10}")) // map on None returns None — the function is never called } }

Option#map applies a function to the contained value if it is Some, returning Some(result); if it is None, the function is never called and None is returned unchanged. This mirrors Ruby's safe navigation operator (&.) — both propagate absence without explicit nil checks.

getOrElse and orElse
users = { "Alice" => 30 } age = users.fetch("Alice", 0) missing = users.fetch("Charlie", 0) backup = users["Charlie"] || 99 puts age puts missing puts backup
object Main { def main(args: Array[String]): Unit = { val users = Map("Alice" -> 30) val age = users.get("Alice").getOrElse(0) val missing = users.get("Charlie").getOrElse(0) val backup = users.get("Charlie").orElse(Some(99)) println(age) println(missing) println(backup) } }

getOrElse unwraps Some(value) or returns the provided default — equivalent to Ruby's Hash#fetch(key, default). orElse provides a fallback Option when the original is None, returning the first non-None option. Both avoid explicit null or nil checks.

Pattern matching on Option
users = { "Alice" => 30 } def describe_user(name, users) age = users[name] if age "#{name} is #{age} years old" else "#{name} not found" end end puts describe_user("Alice", { "Alice" => 30 }) puts describe_user("Charlie", { "Alice" => 30 })
object Main { def describeUser(name: String): String = { val users = Map("Alice" -> 30, "Bob" -> 25) users.get(name) match { case Some(age) => s"${name} is ${age} years old" case None => s"${name} not found" } } def main(args: Array[String]): Unit = { println(describeUser("Alice")) println(describeUser("Charlie")) } }

Pattern matching on Option is the most explicit way to handle both branches. The compiler ensures exhaustiveness — forgetting None produces a warning. This is idiomatically cleaner than a chain of isDefined checks, and forces the developer to think about the "not found" path.

flatMap on Option
def safe_divide(x, y) return nil if y == 0 x / y end result = safe_divide(10, 2)&.then { |q| q * 2 } puts result puts safe_divide(10, 0)&.then { |q| q * 2 }.inspect
object Main { def safeDivide(x: Int, y: Int): Option[Int] = if (y == 0) None else Some(x / y) def main(args: Array[String]): Unit = { val result = safeDivide(10, 2).flatMap(q => Some(q * 2)) println(result) println(safeDivide(10, 0).flatMap(q => Some(q * 2))) // for comprehension reads more naturally for chained Options: val chained = for { quotient <- safeDivide(10, 2) doubled <- Some(quotient * 2) } yield doubled println(chained) } }

flatMap on Option applies a function that itself returns an Option, then flattens one level — equivalent to Ruby's &. (safe navigation) chaining. When all steps succeed, the result is Some(value); if any step returns None, the whole chain short-circuits to None.

Error Handling
try/catch/finally
begin result = 10 / 0 rescue ZeroDivisionError => error puts "Error: #{error.message}" rescue => error puts "Unexpected: #{error.message}" ensure puts "always runs" end
object Main { def main(args: Array[String]): Unit = { try { val result = 10 / 0 println(result) } catch { case error: ArithmeticException => println(s"Error: ${error.getMessage}") case error: Exception => println(s"Unexpected: ${error.getMessage}") } finally { println("always runs") } } }

Scala's try/catch/finally uses pattern matching inside the catch block — each case matches an exception type. The order matters: more specific exceptions must appear before more general ones, exactly as in Ruby's rescue chain. The finally block always runs, even when an exception is raised.

Try — functional error handling
result = Integer("abc") rescue $! if result.is_a?(Integer) puts "Success: #{result}" else puts "Failure: #{result.message}" end good = Integer("42") rescue nil puts good || 0
import scala.util.{Try, Success, Failure} object Main { def main(args: Array[String]): Unit = { val result = Try("abc".toInt) result match { case Success(number) => println(s"Got: ${number}") case Failure(error) => println(s"Failed: ${error.getMessage}") } val good = Try("42".toInt) println(good.getOrElse(0)) println(Try("99".toInt).map(_ * 2)) } }

Try[A] is either Success(value: A) or Failure(exception: Throwable). It wraps a computation that might throw and makes the success/failure explicit in the return type. This is the functional alternative to try/catch — map, flatMap, and recover can chain operations without nested try/catch blocks.

Either — typed error results
def divide(x, y) return [:error, "cannot divide by zero"] if y == 0 [:ok, x / y] end case divide(10, 2) in [:ok, result] then puts "Result: #{result}" in [:error, message] then puts "Error: #{message}" end
object Main { def divide(x: Int, y: Int): Either[String, Int] = if (y == 0) Left("cannot divide by zero") else Right(x / y) def main(args: Array[String]): Unit = { divide(10, 2) match { case Right(result) => println(s"Result: ${result}") case Left(error) => println(s"Error: ${error}") } divide(10, 0) match { case Right(result) => println(s"Result: ${result}") case Left(error) => println(s"Error: ${error}") } println(divide(10, 2).map(_ * 2)) } }

Either[L, R] represents one of two possible outcomes. By convention, Right holds the success value and Left holds the error — "right" is both a direction and an adjective meaning correct. map and flatMap operate on Right and pass Left through unchanged, enabling functional error propagation without exceptions.

recover on Try
result = Integer("abc") rescue -1 puts result chain = (Integer("abc") * 2) rescue 0 puts chain
import scala.util.Try object Main { def main(args: Array[String]): Unit = { val result = Try("abc".toInt).recover { case _: NumberFormatException => -1 } println(result) val chain = Try("abc".toInt) .map(_ * 2) .recover { case _ => 0 } println(chain) } }

recover converts a Failure into a Success using a partial function over the exception. It is the functional equivalent of Ruby's rescue modifier. recoverWith is similar but the handler returns a Try itself, enabling fallback computations that might also fail.

Custom exception class
class ValidationError < StandardError def initialize(field, message) super("#{field}: #{message}") end end begin raise ValidationError.new("email", "is invalid") rescue ValidationError => error puts "Validation failed: #{error.message}" end
object Main { class ValidationError(field: String, message: String) extends Exception(s"${field}: ${message}") def validate(email: String): Unit = if (!email.contains("@")) throw new ValidationError("email", "is invalid") def main(args: Array[String]): Unit = { try { validate("not-an-email") } catch { case error: ValidationError => println(s"Validation failed: ${error.getMessage}") } } }

Custom exception classes extend Exception (or any of its subclasses) and call the parent constructor with a message. Scala uses throw new ExceptionType() to raise and catch { case e: ExceptionType => ... } to handle — directly mirroring Ruby's raise and rescue.

For Comprehensions
for with yield
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |number| number * 2 } puts doubled.inspect
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) val doubled = for (number <- numbers) yield number * 2 println(doubled) // Equivalent to: numbers.map(_ * 2) val labels = for (number <- numbers) yield s"item-${number}" println(labels) } }

A for ... yield expression transforms a collection — each element is bound, then the yield expression produces the corresponding output element. The result type mirrors the input type: a for over a List yields a List. Without yield, the for loop is imperative (like Ruby's each); with yield, it is a map.

for with guard (filter)
squares = (1..10).select { |n| n.even? }.map { |n| n * n } puts squares.inspect
object Main { def main(args: Array[String]): Unit = { val squares = for { number <- 1 to 10 if number % 2 == 0 } yield number * number println(squares.toList) // Equivalent to: (1 to 10).filter(_ % 2 == 0).map(n => n * n) } }

An if clause inside a for comprehension is a guard that filters elements before yielding. Adding a guard is equivalent to inserting a filter call in the desugared method chain. The curly-brace form (for { ... }) allows multiple generators and guards on separate lines — the parenthesis form requires a single line.

Nested for comprehension
pairs = (1..3).flat_map { |x| (1..3).map { |y| [x, y] } } puts pairs.inspect
object Main { def main(args: Array[String]): Unit = { val pairs = for { x <- 1 to 3 y <- 1 to 3 } yield (x, y) println(pairs.toList) // Equivalent to: (1 to 3).flatMap(x => (1 to 3).map(y => (x, y))) } }

Multiple generators in a single for comprehension desugar into nested flatMap calls — each inner generator is applied inside a flatMap of the outer generator. This is Scala's concise notation for the Cartesian product pattern that Ruby expresses with nested flat_map and map.

for comprehension with Option
users = { "Alice" => "alice@example.com" } emails = { "alice@example.com" => "verified" } result = users["Alice"]&.then { |email| emails[email] } puts result puts users["Charlie"]&.then { |email| emails[email] }.inspect
object Main { def main(args: Array[String]): Unit = { val users = Map("Alice" -> "alice@example.com") val emails = Map("alice@example.com" -> "verified") val found = for { email <- users.get("Alice") status <- emails.get(email) } yield s"${email}: ${status}" println(found) val missing = for { email <- users.get("Charlie") status <- emails.get(email) } yield status println(missing) } }

When generators use Option, the for comprehension desugars into flatMap and map on Option. If any generator produces None, the entire comprehension short-circuits to None. This is the idiomatic Scala pattern for chaining operations that may fail — equivalent to Ruby's safe-navigation (&.) chain but with explicit type tracking.

Companion Objects
Companion object and class methods
class Circle attr_reader :radius def initialize(radius) = @radius = radius def area = Math::PI * @radius ** 2 def self.unit = new(1.0) def self.from_diameter(diameter) = new(diameter / 2.0) end unit = Circle.unit puts unit.area.round(4) puts Circle.from_diameter(10.0).area.round(4)
object Main { class Circle(val radius: Double) { def area: Double = Math.PI * radius * radius } object Circle { def unit: Circle = new Circle(1.0) def fromDiameter(diameter: Double): Circle = new Circle(diameter / 2) } def main(args: Array[String]): Unit = { val unit = Circle.unit val custom = Circle.fromDiameter(10.0) println(f"${unit.area}%.4f") println(f"${custom.area}%.4f") } }

A companion object shares its name with a class and has access to the class's private members. It holds the equivalent of Ruby's class methods (self.method_name). The companion object and class must be defined in the same file. Together they provide a clean separation between instance behavior (class) and static/factory behavior (object).

apply — syntactic factory method
class Person attr_reader :name, :age def initialize(name, age) = (@name, @age = name, age) def to_s = "Person(#{@name}, #{@age})" def self.call(name, age) = new(name, age) end alice = Person.call("Alice", 30) puts alice
object Main { class Person(val name: String, val age: Int) { override def toString: String = s"Person(${name}, ${age})" } object Person { def apply(name: String, age: Int): Person = new Person(name, age) def apply(name: String): Person = new Person(name, 0) } def main(args: Array[String]): Unit = { val alice = Person("Alice", 30) // calls Person.apply("Alice", 30) val bob = Person("Bob") // calls Person.apply("Bob") println(alice) println(bob) } }

When an object defines an apply method, calling ObjectName(args) is syntactic sugar for ObjectName.apply(args). This is why case classes can be constructed without new — the companion object's auto-generated apply handles it. Multiple apply overloads provide factory methods with different argument patterns.

Singleton object as a module
module MathUtils def self.square(n) = n * n def self.cube(n) = n ** 3 def self.prime?(n) return false if n < 2 (2...n).none? { |divisor| n % divisor == 0 } end end puts MathUtils.square(5) puts MathUtils.cube(3) puts MathUtils.prime?(17)
object MathUtils { def square(n: Int): Int = n * n def cube(n: Int): Int = n * n * n def isPrime(n: Int): Boolean = n > 1 && (2 until n).forall(n % _ != 0) } object Main { def main(args: Array[String]): Unit = { println(MathUtils.square(5)) println(MathUtils.cube(3)) println(MathUtils.isPrime(17)) println(MathUtils.isPrime(4)) } }

A standalone object (without a companion class) is a singleton — one instance, created lazily on first access. It is Scala's idiomatic equivalent of Ruby's module used as a namespace for utility functions. No new keyword is ever used; the object name acts as the value itself.

unapply — custom pattern matching
class Email def initialize(address) @user, @domain = address.split("@") end def deconstruct_keys(_keys) = { user: @user, domain: @domain } end case Email.new("alice@example.com") in { user:, domain: } puts "user=#{user}, domain=#{domain}" end
object Main { object Email { def unapply(address: String): Option[(String, String)] = address.split("@") match { case Array(user, domain) => Some((user, domain)) case _ => None } } def main(args: Array[String]): Unit = { "alice@example.com" match { case Email(user, domain) => println(s"user=${user}, domain=${domain}") case _ => println("not an email") } "not-an-email" match { case Email(user, domain) => println(s"user=${user}") case _ => println("not an email") } } }

An unapply method makes an object usable as a pattern in match expressions — this is called an extractor. When the match uses Email(user, domain), Scala calls Email.unapply(value) and, if it returns Some, binds the extracted values to user and domain. Ruby's equivalent is deconstruct_keys for hash patterns.