PONY λ M2 Modula-2

Swift.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Swift and Ruby

Swift 6.3 Ruby 4.0
Basics & Syntax
Hello, World
print("Hello, World!")
puts "Hello, World!" # No parentheses needed on a call. No semicolons. No braces around a method body. # Ruby drops nearly every piece of syntax that is not carrying meaning. puts 1 + 2 * 3
Both languages let the file body be the program. Ruby goes further and drops the parentheses too — puts "x" is a method call with the parentheses omitted, which is legal on every call and is the house style. It takes about a week to stop feeling like something is missing.
No let, no var, no types
let fixed = 1 var counter = 0 counter += 1 // fixed = 2 ← will not compile let name = "Ada" let age = 36 print("\(name) is \(age)") let letter = """ Dear \(name), Regards """ print(letter)
counter = 0 # no keyword, no type, always reassignable counter += 1 FIXED = 1 # SCREAMING_CASE makes a CONSTANT — and reassigning it merely # WARNS. Ruby still lets it happen. name = "Ada" age = 36 puts "#{name} is #{age}" # interpolation needs the full #{...} puts 'single quotes do NOT interpolate: #{name}' # The squiggly heredoc is Swift's multi-line string: the ~ strips the indent. letter = <<~HEREDOC Dear #{name}, Regards HEREDOC puts letter
There is no let: a capitalized name makes a "constant", and Ruby's idea of constant is a warning printed to stderr rather than an error. The immutability discipline is entirely on you. What Ruby does freeze, as of 4.0, is string literals — "hello" << " world" now raises FrozenError, which is the language quietly conceding the point. Note that the quote style matters: single-quoted strings do no interpolation and no escape processing at all.
Dynamic Typing
No types, and no compiler
struct User { let name: String let age: Int } func describe(_ user: User) -> String { "\(user.name) is \(user.age)" } let ada = User(name: "Ada", age: 36) print(describe(ada)) // print(ada.nmae) ← compile error: no such property // print(describe("nope")) ← compile error: wrong type // Both are caught BEFORE the program runs.
# No declarations, no annotations, no compiler. A method takes whatever it is # given and fails when it fails. def describe(user) "#{user.name} is #{user.age}" end User = Struct.new(:name, :age) # the quickest data type Ruby has ada = User.new("Ada", 36) puts describe(ada) # DUCK TYPING: anything with .name and .age works. No protocol, no conformance. Robot = Struct.new(:name, :age) puts describe(Robot.new("R2", 5)) # A misspelled method is an error only when that LINE RUNS. begin puts ada.nmae rescue NoMethodError => error puts "NoMethodError: #{error.message}" end
Everything the Swift compiler does for you is gone: no type checking, no exhaustiveness, no null safety, no arity checking of a block you passed along. A typo is a NoMethodError at the moment that line executes, which is why Ruby culture is so test-obsessed — the test suite is the type checker, and it is not optional. What you get back is duck typing: describe above accepts anything with a name and an age, with no protocol declared and no conformance to write, which is why mocking and adapting in Ruby take no ceremony at all.
nil Instead of Optionals
nil is an object, and nothing checks it
func findName(_ id: Int) -> String? { id == 1 ? "Ada" : nil } // The compiler refuses to let you use it unwrapped. print(findName(1)?.uppercased() ?? "(none)") if let name = findName(1) { print(name.count) } func shout(_ id: Int) -> String { guard let name = findName(id) else { return "(nobody)" } return name.uppercased() } print(shout(99))
def find_name(id) = id == 1 ? "Ada" : nil # an endless method (Ruby 3+) # &. is the safe-navigation operator — Swift's ?. exactly. puts find_name(1)&.upcase || "(none)" puts find_name(99)&.upcase || "(none)" # || is the nil-coalescing operator... with a catch: it triggers on FALSE too, # because only nil and false are falsy. (0 and "" are TRUTHY.) puts (find_name(99) || "(none)") def shout(id) name = find_name(id) return "(nobody)" if name.nil? # the guard-let equivalent: a trailing if name.upcase end puts shout(99) # nil is an OBJECT with methods. That is what makes it survivable. puts nil.to_a.inspect # [] puts nil.to_s.inspect # "" puts nil.nil? # true # But call a method it does not have, and you get Ruby's nil crash: begin find_name(99).upcase rescue NoMethodError => error puts "NoMethodError: #{error.message}" end
Ruby has &. (safe navigation) and || (nil-coalescing), so the operators feel familiar — but nothing makes you use them, and undefined method 'upcase' for nil is the error you will meet most often. Two Ruby-specific notes. nil is a real object with methods (nil.to_a is [], nil.to_s is ""), which makes a surprising amount of code survive it. And only nil and false are falsy — 0 and "" are truthy — so value || default is not quite ??: it also fires when the value is legitimately false.
Blocks & Enumerable
Trailing closures become blocks
Swift's trailing closure is Ruby's block — but a block is not a value, it is a parameter of the call itself, and that difference is the foundation of the whole language.
let numbers = [1, 2, 3, 4] print(numbers.map { $0 * 2 }) print(numbers.filter { $0 % 2 == 0 }) print(numbers.reduce(0, +)) // A function taking a closure. func twice(_ body: () -> Void) { body() body() } twice { print("hello") } // A closure stored in a value. let double: (Int) -> Int = { $0 * 2 } print(double(21))
numbers = [1, 2, 3, 4] puts numbers.map { |number| number * 2 }.inspect puts numbers.select { |number| number.even? }.inspect # filter is 'select' puts numbers.sum puts numbers.reduce(0) { |total, number| total + number } # &: is the symbol-to-proc shorthand — Swift's \.keyPath, roughly. puts numbers.map(&:to_s).inspect # A method taking a block. 'yield' calls it — there is no parameter to declare. def twice yield yield end twice { puts "hello" } # 3.times { } — because Integer has a 'times' method that takes a block. 3.times { |index| print index } puts # A block CAN be captured as a value (a proc/lambda), but that is the exception. double = ->(number) { number * 2 } puts double.call(21) puts double.(21)
A block is passed to every method invisibly, and yield calls it — so a method that takes a block declares nothing, and control structures that look built-in (3.times, each, File.open) are just methods with blocks. That is why Ruby needs no defer: File.open(path) { |file| … } closes the file when the block ends, which is Swift's defer and with expressed as an ordinary method. The idioms to pick up are |parameters| (Swift's $0 is |number|, named), &:symbol (map(&:to_s)), and ->() { } for the rare case where you need a closure as a value.
Enumerable: the standard library you already know
let names = ["Ada", "Grace", "Alan", "Barbara"] print(names.filter { $0.count > 3 }.map { $0.uppercased() }) print(names.map(\.count).reduce(0, +)) print(names.sorted()) print(names.first { $0.hasPrefix("G") } ?? "(none)") print(Dictionary(grouping: names, by: \.count).keys.sorted()) print(names.contains { $0.count > 5 }) let ages = ["Ada": 36, "Alan": 41] print(ages.mapValues { $0 + 1 })
names = ["Ada", "Grace", "Alan", "Barbara"] puts names.select { |name| name.length > 3 }.map(&:upcase).inspect puts names.sum(&:length) puts names.sort.inspect puts (names.find { |name| name.start_with?("G") } || "(none)") puts names.group_by(&:length).keys.sort.inspect puts names.any? { |name| name.length > 5 } ages = { "Ada" => 36, "Alan" => 41 } puts ages.transform_values { |age| age + 1 }.inspect # And the ones Swift makes you build: each_slice, each_cons, partition, tally, # each_with_object, zip, flat_map, chunk_while, lazy... puts names.partition { |name| name.length > 3 }.inspect puts names.map(&:length).tally.inspect puts (1..Float::INFINITY).lazy.map { |number| number * 2 }.first(3).inspect
The mapping is almost a rename — filter is select, first(where:) is find, contains is any?, mapValues is transform_values — and then Enumerable keeps going, with tally, partition, each_cons, each_slice, chunk_while, each_with_object and dozens more. This is the part of Ruby that will actually make you happy: it is the richest collection library of any language on this site, and any class becomes a full member of it by defining each and include Enumerable. .lazy even means the same thing it does in Swift.
Classes & Open Classes
Classes, and everything is one
class Rectangle { let width: Double var height: Double var area: Double { width * height } // computed property init(width: Double, height: Double) { self.width = width self.height = height } func scaled(by factor: Double) -> Rectangle { Rectangle(width: width * factor, height: height * factor) } } let rectangle = Rectangle(width: 3, height: 4) print(rectangle.area) rectangle.height = 10 print(rectangle.area)
class Rectangle # attr_reader generates the getters; attr_accessor also generates setters. attr_reader :width attr_accessor :height def initialize(width, height) @width = width # @ marks an INSTANCE VARIABLE. It is private, always: @height = height # there is no way to read it from outside except a method. end # A "computed property" is just a method. Ruby has no property syntax, and # needs none — a getter IS a method, so the caller cannot tell them apart. def area = width * height def scaled(by:) # a keyword argument, and it is REQUIRED Rectangle.new(width * by, height * by) end end rectangle = Rectangle.new(3, 4) puts rectangle.area rectangle.height = 10 # calls the generated setter, height= puts rectangle.area puts rectangle.scaled(by: 2).area # Everything is an object, including classes and nil. puts 3.times.to_a.inspect puts Rectangle.class puts nil.class
The "uniform access principle" is doing quiet work here: a Ruby getter is a method, so rectangle.area and rectangle.width look identical to a caller and either can be swapped for the other without changing a single call site — which is what Swift's computed properties buy you, arrived at from the other direction. attr_reader/attr_accessor generate the accessors (and they are themselves ordinary method calls that write methods — see Metaprogramming). Instance variables (@width) are always private; there is no way to reach one from outside except through a method.
Extensions become open classes — with no guardrails
// An extension can add methods and computed properties to a type you do not own. extension String { var initials: String { split(separator: " ").compactMap { $0.first }.map(String.init).joined() } func shout() -> String { uppercased() + "!" } } print("Ada Lovelace".initials) print("hello".shout()) // What it CANNOT do: change an existing method. 'uppercased()' is safe from you, // and from every library you import.
# There are no extensions, because there is no need: a class is never closed. # Reopening String adds methods to EVERY string in the program, forever. class String def initials = split(" ").map { |word| word[0] }.join def shout = upcase + "!" end puts "Ada Lovelace".initials puts "hello".shout # And you can REDEFINE an existing method. Globally. Including one in the # standard library. Nothing stops you, and nothing warns the code that depended # on the old behavior. class Integer def even? = "I have been redefined" # a terrible idea, and perfectly legal end puts 2.even? # The sanctioned version: a refinement, which is scoped to the file that # activates it — Ruby's answer to exactly this danger. module Shouting refine String do def polite = "#{self}, please" end end
A Swift extension can add to a type but never change it: uppercased() means what it always meant, no matter what any library does. Ruby has no such protection — reopening Integer and redefining even? is legal, global, and instantly affects every gem in the process. This is "monkey patching", it is genuinely powerful (Rails is built on it), and it is how a language gets a reputation for spooky action at a distance. refine is the scoped, sanctioned version, and almost nobody uses it. Treat the ability the way you treat unsafeBitCast.
Protocols Become Mixins
Protocols and protocol extensions become modules
// Conform to Comparable, implement <, and get <=, >, >=, sort, max, min free. struct Money: Comparable, CustomStringConvertible { let cents: Int var description: String { "$\(Double(cents) / 100)" } static func < (left: Money, right: Money) -> Bool { left.cents < right.cents } } let prices = [Money(cents: 300), Money(cents: 100), Money(cents: 200)] print(prices.sorted().map(\.description)) print(prices.max()!) print(Money(cents: 100) < Money(cents: 200))
class Money include Comparable # a MIXIN — the module's methods become this class's attr_reader :cents def initialize(cents) @cents = cents end # Define ONE method, the spaceship operator, and Comparable gives you # < <= > >= == between? clamp sort max min — exactly like conforming to # Comparable in Swift, and by the same logic. def <=>(other) = cents <=> other.cents def to_s = "$#{cents / 100.0}" end prices = [Money.new(300), Money.new(100), Money.new(200)] puts prices.sort.map(&:to_s).inspect puts prices.max.to_s puts Money.new(100) < Money.new(200) # A module is also how you share behavior WITH state, which a Swift protocol # extension cannot do — and it can hook into the class as it is included. module Greetable def greet = "Hello from #{self.class}" end class Money include Greetable end puts Money.new(1).greet
A module is a protocol and a protocol extension in one: include Comparable mixes its methods into your class, and — exactly as in Swift — you implement one primitive (<=>, the "spaceship" operator) and inherit every comparison operator, sort, max, min and clamp for free. include Enumerable works the same way from each. The difference is that modules are dynamic: nothing declares conformance at compile time, nothing checks that you implemented <=>, and a missing method surfaces as a NoMethodError the first time someone sorts your objects.
Pattern Matching
switch becomes case/in — without exhaustiveness
enum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double) } func area(_ shape: Shape) -> Double { switch shape { // EXHAUSTIVE: a missing case will not compile case .circle(let radius): return 3.14159 * radius * radius case .rectangle(let width, let height): return width * height } } print(area(.circle(radius: 1))) print(area(.rectangle(width: 2, height: 3)))
Circle = Struct.new(:radius) Rectangle = Struct.new(:width, :height) def area(shape) # case/in is structural pattern matching (Ruby 3). It DECONSTRUCTS as it matches. case shape in Circle[radius] 3.14159 * radius * radius in Rectangle[width, height] width * height end end puts area(Circle.new(1)) puts area(Rectangle.new(2, 3)) # It also matches the shape of hashes and arrays — which Swift cannot do, and # which is exactly what you want for decoded JSON. message = { type: "click", x: 3, y: 4 } case message in { type: "click", x:, y: } puts "click at #{x},#{y}" in { type: "key", key: } puts "key #{key}" end case [1, 2, 3, 4] in [first, *rest] puts "#{first} then #{rest.inspect}" end # NO exhaustiveness: an unmatched value raises NoMatchingPatternError at RUN TIME. begin case 42 in String then puts "never" end rescue NoMatchingPatternError => error puts "NoMatchingPatternError: #{error.message}" end
Ruby 3's case/in is a real structural pattern match: it deconstructs objects (any class can opt in with deconstruct/deconstruct_keys), and it matches the shape of hashes and arrays, which switch cannot do. What it does not have is exhaustiveness — nothing tells you a case is missing, and an unmatched value raises NoMatchingPatternError at run time. That is louder than Python's silent fall-through, and it is still not the compile-time guarantee that makes Swift enums safe to extend. (Note also the older case/when, which is a different construct: it tests with === and does no destructuring.)
Metaprogramming
Methods can be written at run time
The capability with no Swift equivalent, and the reason Rails looks the way it does. A class body is ordinary code that runs, so it can define methods while it runs.
// Swift's dynamism is limited and deliberate. There is no way to define a method // at run time, no method_missing, and no eval. What you have instead: // - @dynamicMemberLookup, for a fixed protocol of dynamic access // - Mirror, for read-only reflection // - macros (Swift 5.9+), which generate code at COMPILE time struct Person { let name: String let age: Int } let ada = Person(name: "Ada", age: 36) // Reflection: read-only, and it cannot add anything. for child in Mirror(reflecting: ada).children { print(child.label ?? "?", child.value) }
class Person # attr_accessor is NOT syntax. It is a method call, running right now, in the # class body — and what it does is DEFINE METHODS. You can write it yourself: def self.my_attr_accessor(*names) names.each do |name| define_method(name) { instance_variable_get("@#{name}") } define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) } end end my_attr_accessor :name, :age def initialize(name, age) @name = name @age = age end # method_missing catches calls to methods that do not exist — which is how # Rails gives you find_by_name_and_age without defining it. def method_missing(method_name, *args) if method_name.to_s.start_with?("shout_") field = method_name.to_s.delete_prefix("shout_") public_send(field).to_s.upcase else super end end def respond_to_missing?(method_name, include_private = false) method_name.to_s.start_with?("shout_") || super end end ada = Person.new("Ada", 36) puts ada.name ada.age = 37 puts ada.age puts ada.shout_name # never defined — method_missing built it on the fly puts ada.respond_to?(:shout_name) puts Person.instance_methods(false).sort.inspect
Read the left column again: Swift gives you read-only reflection and compile-time macros, and that is deliberate — the compiler must be able to know every method that exists. Ruby gives you define_method (write a method at run time), method_missing (intercept a call to a method that does not exist), public_send (call a method by name), and instance_variable_get. That is not a dark corner of the language; it is how attr_accessor itself works, and it is why an ActiveRecord model gets a method per database column without anyone writing one. The price is exactly what you would guess: your editor cannot autocomplete it, your reader cannot grep for it, and the compiler that would have caught the typo does not exist.
Errors & Exceptions
throws vanishes; rescue takes its place
enum ParseError: Error { case notANumber(String) case notPositive(Int) } // 'throws' is in the signature; 'try' is required at every call site. func parsePositive(_ text: String) throws -> Int { guard let number = Int(text) else { throw ParseError.notANumber(text) } guard number > 0 else { throw ParseError.notPositive(number) } return number } do { print(try parsePositive("42")) print(try parsePositive("-1")) } catch ParseError.notPositive(let number) { print("not positive: \(number)") } catch { print("failed: \(error)") } print((try? parsePositive("nope")) ?? -1)
class NotANumber < StandardError; end class NotPositive < StandardError attr_reader :number def initialize(number) @number = number super("not positive: #{number}") end end # No 'throws', no 'try'. Nothing in the signature says this can fail. def parse_positive(text) number = Integer(text, exception: false) raise NotANumber, text if number.nil? raise NotPositive, number if number <= 0 number end begin puts parse_positive("42") puts parse_positive("-1") rescue NotPositive => error puts "not positive: #{error.number}" rescue StandardError => error puts "failed: #{error.message}" ensure puts "done" # ensure is 'finally' — and it is also Swift's defer end # The whole method body can be a begin block: 'rescue' at method level. def safe_parse(text) parse_positive(text) rescue StandardError -1 end puts safe_parse("nope") # And the nil-returning alternative, which needs no exception at all. puts (Integer("nope", exception: false) || -1)
The throws annotation is gone, so nothing in a signature warns a caller that a method can raise — the information Swift puts in the type system lives in the documentation. rescue is catch, ensure is finally (and doubles as Ruby's defer), and a method body is implicitly a begin block, so rescue can appear at method level with no wrapper. Two conventions worth knowing: rescue StandardError, never bare Exception (which would swallow SignalException and NoMemoryError); and many standard-library methods offer a nil-returning alternative (Integer(text, exception: false)) so that expected failure needs no exception at all.