Classes and structs
Everything in d6 is an object, and you define your own kinds of object with
class. For plain bundles of data, struct writes the class for you.
A class
class Account
attr_reader owner: String
attr_reader balance: Int
def initialize(owner: String, balance: Int = 0)
@owner = owner
@balance = balance
end
def deposit(amount: Int) -> Account
@balance += amount
self
end
def to_s() = "#{@owner}: #{@balance}"
end
account = Account.new("Ada")
account.deposit(50).deposit(25)
puts(account.balance()) # => 75
puts(account) # => Ada: 75
The pieces:
ClassName.new(...)creates an object and passes its arguments toinitialize.@nameis an instance variable, a field of the object. Fields are private: code outside the class reads them through methods.attr_reader name: Typewrites a reader method for you, soaccount.balance()returns@balance.attr_writerwrites a setter andattr_accessorboth. Attributes always state their type.selfis the object a method was called on. Returning it, asdepositdoes, lets calls chain.to_sis whatputsand string interpolation use to show the object.
With an attr_accessor (or any method named name=), assignment syntax
calls the setter:
class Label
attr_accessor text: String
def initialize(text: String)
@text = text
end
end
label = Label.new("draft")
label.text = "final"
puts(label.text()) # => final
Inside a method, you can call the object's other methods without self..
Class methods
def self.name defines a method you call on the class itself, typically a
constructor with a meaningful name:
class Temperature
attr_reader degrees: Float
def initialize(degrees: Float)
@degrees = degrees
end
def self.freezing() -> Temperature = Temperature.new(0.0)
end
puts(Temperature.freezing().degrees()) # => 0.0
Private methods
Methods after private can only be called from inside the class, without a
receiver:
class Report
def initialize(rows: Array)
@rows = rows
end
def summary() = "#{@rows.length()} rows, #{format_total()}"
private
def format_total() = "total #{@rows.sum()}"
end
puts(Report.new([3, 4]).summary()) # => 2 rows, total 7
Calling report.format_total() from outside raises an error saying the
method is private.
Inheritance
A class can extend one other class with <. super(...) calls the parent's
version of the current method:
class Animal
attr_reader name: String
def initialize(name: String)
@name = name
end
def describe() = "#{@name} the animal"
end
class Dog < Animal
def describe() = super() + ", who barks"
end
puts(Dog.new("Rex").describe()) # => Rex the animal, who barks
value is ClassName tests whether a value is an instance of a class or one
of its subclasses. It also works with the built-in types:
class Animal
end
class Dog < Animal
end
puts(Dog.new() is Animal) # => true
puts(5 is Int) # => true
puts("5" is Int) # => false
Modules
A module groups methods that several classes can share. include mixes them
into a class. Inside a module method, call the including object's methods
through self.:
module Greets
def greet() = "Hello from #{self.name()}"
end
class Robot
include Greets
attr_reader name: String
def initialize(name: String)
@name = name
end
end
puts(Robot.new("R2").greet()) # => Hello from R2
Operators and comparison
Operators are methods, so a class can define +, ==, <=> and the rest.
Defining <=> and including Comparable gives you <, >, between?,
and lets sort and max work on your objects:
class Version
include Comparable
attr_reader major: Int
attr_reader minor: Int
def initialize(major: Int, minor: Int)
@major = major
@minor = minor
end
def <=>(other: Version) -> Int
@major == other.major() ? @minor <=> other.minor() : @major <=> other.major()
end
def to_s() = "#{@major}.#{@minor}"
end
versions = [Version.new(1, 10), Version.new(1, 2), Version.new(0, 9)]
puts(versions.max()) # => 1.10
puts(Version.new(1, 2) < Version.new(1, 10)) # => true
Structs
When a class is mostly data, struct declares the fields and generates the
constructor, readers, == and to_s:
struct Point(x: Int, y: Int)
end
a = Point.new(1, 2)
b = Point.new(x: 1, y: 2)
puts(a) # => Point(x: 1, y: 2)
puts(a.x()) # => 1
puts(a == b) # => true
Two structs are equal when they are the same struct and their fields are equal, which makes them good keys and good test expectations. A struct body can add methods like any class:
struct Point(x: Int, y: Int)
def distance_from_origin() -> Float = Math.sqrt((@x * @x + @y * @y).to_f())
end
puts(Point.new(3, 4).distance_from_origin()) # => 5.0
A plain class that extends a struct and adds fields of its own gets an ==
and a to_s covering those fields too, unless it writes its own.
Sealed classes
A sealed class lists its variants: it cannot be created directly, only
through its subclasses. In return, the compiler knows the complete set of
subclasses and checks that a case over one covers them all:
sealed class Shape
end
class Circle < Shape
attr_reader radius: Float
def initialize(radius: Float)
@radius = radius
end
end
class Square < Shape
attr_reader side: Float
def initialize(side: Float)
@side = side
end
end
def area(shape: Shape) -> Float
case shape
when Circle then 3.14159 * shape.radius() * shape.radius()
when Square then shape.side() * shape.side()
end
end
puts(area(Square.new(3.0))) # => 9.0
Delete the when Square line and the program no longer compiles:
error: case is not exhaustive over its subject's known closed type; missing: Square; add the missing branch or an 'else'
Adding a new subclass later turns every case that forgot it into a compile
error, instead of a nil at run time.
Coming from another language
- Ruby: attributes are declared with a name and a type
(
attr_reader name: String), not with symbols.obj.is_a?(C)is writtenobj is C. Classes are not objects you can pass around at run time. - Python: there is no
selfparameter;@nameisself.name, andinitializeis__init__. Fields are private unless you add readers. - JavaScript:
constructorisinitialize,this.xis@x, andextendsis<. Astructis a class with value equality, which plain JavaScript objects lack.
The classes and modules reference covers visibility, class variables, reopening classes, generics and more.
Next: Types.