# 6. Structs, records, classes and modules

> Chapter 6: define your own types with records, structs and classes; share behavior with inheritance, interfaces and mixins; group functions in modules.

Canonical: https://alice-lang.org/documentation/learn/types/

[Documentation index](https://alice-lang.org/documentation/llms.txt) · [Complete book](https://alice-lang.org/llms-full.txt)

So far the tea shop has used tuples such as `{"Assam", 2}`. They work, but `o[0]` and `o[1]` say nothing about what they hold, and nothing stops you from swapping them. Real programs give their data names and rules. This chapter shows the four ways Alice lets you define a type, and how to choose between them.

| You need                                                      | Use      |
| ------------------------------------------------------------- | -------- |
| a few named values that belong together and never change      | `record` |
| a small value that is copied, like a point or a color         | `struct` |
| an object with an identity and changing state                 | `class`  |
| functions that belong together, or behavior shared by classes | `module` |

## 6.1 Records: data with names

A **record** is the simplest way to name a group of values. One line declares the type, its fields and a constructor:

```alice
record Tea(name : String, cents : Int32)

assam = Tea.new("Assam", 250)
puts assam.name
puts assam.cents
puts assam
```

```text title="Output"
Assam
250
Tea { name = Assam, cents = 250 }
```

Records have **value equality**: two records with the same fields are equal, even if they were created separately. That makes them good hash keys:

```alice
record Tea(name : String, cents : Int32)

a = Tea.new("Assam", 250)
b = Tea.new("Assam", 250)
puts a == b
stock = {a => 12}
puts stock[b]
```

```text title="Output"
true
12
```

The fields of a record cannot be changed after it is made. That is a feature: a record you pass to a method comes back the same. If a price changes, make a new record.

## 6.2 Classes: objects with state

Some things in a program change over time: a shopping basket fills up, an account balance moves. Those are **objects**, and you define them with a **class**:

```alice
class Basket
  getter items : List(String)
  getter total_cents : Int32

  def initialize
    @items = List(String).new
    @total_cents = 0
  end

  def add(name : String, cents : Int32)
    @items << name
    @total_cents += cents
  end
end

basket = Basket.new
basket.add("Assam", 250)
basket.add("Scone", 400)
puts basket.items
puts basket.total_cents
```

```text title="Output"
["Assam", "Scone"]
650
```

Let's read the new parts:

- `@items` and `@total_cents` are **fields**: each `Basket` object has its own. The `@` marks them, as in Ruby.
- `initialize` is the **constructor**. `Basket.new` creates an object and runs it.
- `getter items : List(String)` declares the field and a method `items` that reads it. Outside the class, `basket.items` works, but nothing can assign `basket.items = ...`.
- `property` instead of `getter` would also allow assignment from outside. `setter` allows only assignment.

A short form assigns a field straight from a constructor parameter: `def initialize(@name : String)`.

### Every field must be set

What if a constructor forgets a field? In many languages the field silently starts as null, and the program crashes later. Alice checks it when it compiles:

```alice fail="AL0221"
class Guest
  @name : String

  def initialize
  end
end
```

```text title="Error"
error[AL0221]: field `@name` (string) is never assigned by every `initialize`
```

Give the field a value in every `initialize`, give it a default (`@name : String = ""`), or make it `String?` if it may really be missing.

### Keep the inside private

A class decides what the outside world may use. `private` methods are for the class itself:

```alice fail="AL0313"
class Account
  getter balance : Int32 = 0

  def deposit(amount : Int32)
    @balance = checked_add(amount)
  end

  private def checked_add(amount : Int32) : Int32
    raise ArgumentException.new("negative deposit") if amount < 0
    @balance + amount
  end
end

account = Account.new
account.checked_add(5)
```

```text title="Error"
error[AL0313]
is private here
```

### Printing your objects

`puts` prints an object with its `to_s` method. Define one to control what people see:

```alice
class Pot
  getter cups : Int32

  def initialize(@cups : Int32)
  end

  def to_s : String
    "a pot of #{cups} cups"
  end
end

puts Pot.new(6)
puts "Here is #{Pot.new(2)}."
```

```text title="Output"
a pot of 6 cups
Here is a pot of 2 cups.
```

## 6.3 Structs: values that are copied

Assigning a class object to a second variable shares it, as you saw with arrays. A **struct** is copied instead. The two kinds behave differently here:

```alice
struct CupS
  property ml : Int32
  def initialize(@ml : Int32)
  end
end

class CupC
  property ml : Int32
  def initialize(@ml : Int32)
  end
end

a = CupS.new(200)
b = a
b.ml = 50
puts a.ml

c = CupC.new(200)
d = c
d.ml = 50
puts c.ml
```

```text title="Output"
200
50
```

Changing `b` did not change `a`, because `b` is a copy. Changing `d` changed `c`, because both name the same object.

Why have both? Structs live inline, in a local variable or inside an array, with no separate allocation. An array of a million `Point` structs is one block of memory. That makes structs fast for small values such as points, colors and money amounts. Classes are better for bigger things with an identity, such as a basket or a connection.

`record struct` combines the two ideas: a record that is copied like a struct. A body can add methods, including operators:

```alice
record struct Money(cents : Int64)
  def +(other : Money) : Money
    Money.new(cents + other.cents)
  end

  def to_s : String
    format("%d.%02d", cents / 100, cents % 100)
  end
end

puts Money.new(250) + Money.new(1205)
```

```text title="Output"
14.55
```

## 6.4 Inheritance and interfaces

Sometimes several types are kinds of one thing. Tea and coffee are both drinks, with a name and a price, but they compute the price differently. A class can **inherit** from another with `<`:

```alice
abstract class Drink
  getter name : String

  def initialize(@name : String)
  end

  abstract def cents : Int32

  def label : String
    "#{name}: #{cents}"
  end
end

class Tea < Drink
  def initialize(name : String)
    super(name)
  end

  def cents : Int32
    250
  end
end

class Coffee < Drink
  def initialize
    super("Coffee")
  end

  def cents : Int32
    300
  end

  def label : String
    super + " (strong)"
  end
end

drinks = List(Drink).new
drinks << Tea.new("Assam")
drinks << Coffee.new
drinks.each { |d| puts d.label }
```

```text title="Output"
Assam: 250
Coffee: 300 (strong)
```

- `abstract class Drink` cannot be created on its own; `abstract def cents` must be defined by every subclass.
- `super(name)` calls the parent's constructor. `super` in `label` calls the parent's `label`.
- A `List(Drink)` holds any kind of drink, and each one answers `label` in its own way.

Constructors are not inherited: each subclass writes its own `initialize` and calls `super`.

An **interface** is a promise without code: "this type has these methods". A class can implement it with `<`, and a method can accept anything that keeps the promise:

```alice
interface Priced
  def cents : Int32
end

class Scone < Priced
  def cents : Int32
    400
  end
end

def show(item : Priced)
  puts "costs #{item.cents}"
end

show(Scone.new)
```

```text title="Output"
costs 400
```

## 6.5 Modules

A **module** groups related functions under one name. Functions defined with `def self.` are called on the module:

```alice
module Pricing
  def self.with_tax(cents : Int32, percent : Int32) : Int32
    cents + cents * percent / 100
  end
end

puts Pricing.with_tax(1000, 7)
```

```text title="Output"
1070
```

### Mixins: sharing methods with `include`

A module can also hold ordinary methods for classes to **include**. This is called a **mixin**. It shares behavior between classes that have no common parent:

```alice
module Greeting
  def greet : String
    "Welcome, #{name}!"
  end
end

class Guest
  include Greeting
  getter name : String

  def initialize(@name : String)
  end
end

puts Guest.new("Alice").greet
```

```text title="Output"
Welcome, Alice!
```

The mixin method uses `name`, which `Guest` provides. Two mixins come with Alice and save a lot of code:

- `include Comparable` plus a `<=>` method gives you `<`, `>`, `==`, `clamp`, and lets `sort`, `min` and `max` work on your type.
- `include Enumerable` plus an `each` method gives you `map`, `select`, `sum`, `includes?` and every other iterator from chapter 4.

```alice
class Version
  include Comparable
  getter major : Int32
  getter minor : Int32

  def initialize(@major : Int32, @minor : Int32)
  end

  def <=>(other : Version) : Int32
    major == other.major ? minor <=> other.minor : major <=> other.major
  end
end

versions = [Version.new(1, 2), Version.new(0, 9), Version.new(1, 0)]
best = versions.max
puts "#{best.major}.#{best.minor}"
puts Version.new(1, 0) < Version.new(1, 2)
```

```text title="Output"
1.2
true
```

### Reopening a class

You can add methods to a class after it was defined, even to a .NET type such as `Int32`. Alice compiles that into a .NET extension method:

```alice
class Int32
  def cents_text : String
    "#{self / 100}.#{(self % 100).to_s.rjust(2, "0")}"
  end
end

puts 1205.cents_text
```

```text title="Output"
12.05
```

Use this sparingly: a reader looking at `1205.cents_text` has to know where the method came from.

## 6.6 Generic types

A class can take a type as a parameter, like `Array(T)` does. Write the parameter in parentheses after the name:

```alice
class Shelf(T)
  getter items : List(T)

  def initialize
    @items = List(T).new
  end

  def put(item : T)
    @items.Add(item)
  end
end

teas = Shelf(String).new
teas.put("Assam")
teas.put("Sencha")
puts teas.items
cups = Shelf(Int32).new
cups.put(12)
puts cups.items
```

```text title="Output"
["Assam", "Sencha"]
[12]
```

`Shelf(String)` and `Shelf(Int32)` are two different types made from one definition. Inside a generic class, call the .NET method `Add` to append a `T`: the `<<` shortcut does not work on a `List(T)` yet. Write the type argument when you create one: `Shelf(Int32).new`.

## Project: an order type

Time to replace the tuples. An order line becomes a record, and an order becomes a class that collects lines and knows its total.

```alice
record Line(tea : String, cups : Int32, cents_each : Int32)
  def cents : Int64
    cents_each.to_i64 * cups
  end
end

class Order
  getter lines : List(Line)

  def initialize
    @lines = List(Line).new
  end

  def add(tea : String, cups : Int32, cents_each : Int32)
    @lines << Line.new(tea, cups, cents_each)
  end

  def total : Int64
    @lines.sum { |l| l.cents }
  end

  def to_s : String
    @lines.map { |l| "#{l.cups} x #{l.tea}" }.join(", ")
  end
end

order = Order.new
order.add("Assam", 2, 250)
order.add("Sencha", 1, 350)
puts order
puts order.total
```

```text title="Output"
2 x Assam, 1 x Sencha
850
```

Compare it with `o[0]` and `o[1]` from chapter 4. Now `l.tea` and `l.cups` say what they are, and the order's rules live in one place.

:::caution\[Common mistakes]

- **A field no constructor sets.** `error[AL0221]`. Assign it in every `initialize`, give it a default, or make it nilable.
- **Calling a private method from outside.** `error[AL0313]`. Make it public, or call a public method that uses it.
- **Expecting constructors to be inherited.** A subclass needs its own `initialize` that calls `super(...)`.
- **Leaving out the type argument.** `Shelf.new` does not compile; write `Shelf(Int32).new`.
- **Expecting a struct to be shared.** Assigning a struct copies it. Use a class when two places must see the same object.
  :::

## Exercises

1. Write a record `Guest(name : String, age : Int32)` with a method `adult? : Bool`. Print whether `Guest.new("Alice", 7)` is an adult.
2. Write a class `Kettle` with a `property temperature : Int32` that starts at 20, and a method `boil` that sets it to 100. Print the temperature before and after.
3. Make `Tea` records sortable by price: define a class `PricedTea` with `include Comparable` and `<=>`, and print the name of the cheapest of three teas with `min`.

<details>
<summary>Answers</summary>

1. Answer:

   ```alice
   record Guest(name : String, age : Int32)
     def adult? : Bool
       age >= 18
     end
   end

   puts Guest.new("Alice", 7).adult?
   ```

   ```text title="Output"
   false
   ```

2. Answer:

   ```alice
   class Kettle
     property temperature : Int32 = 20

     def boil
       @temperature = 100
     end
   end

   k = Kettle.new
   puts k.temperature
   k.boil
   puts k.temperature
   ```

   ```text title="Output"
   20
   100
   ```

3. Answer:

   ```alice
   class PricedTea
     include Comparable
     getter name : String
     getter cents : Int32

     def initialize(@name : String, @cents : Int32)
     end

     def <=>(other : PricedTea) : Int32
       cents <=> other.cents
     end
   end

   teas = [PricedTea.new("Sencha", 350), PricedTea.new("Assam", 250), PricedTea.new("Oolong", 400)]
   puts teas.min.name
   ```

   ```text title="Output"
   Assam
   ```

</details>

## Summary

- `record` declares named, immutable data with value equality in one line.
- `class` defines objects with fields, a constructor (`initialize`), and methods. `getter`, `property` and `setter` control access; `private` hides helpers.
- Every non-nilable field must be set by every constructor (`AL0221`).
- `struct` values are copied; class objects are shared.
- `<` inherits from a class or implements an interface. Modules hold functions (`def self.`) and mixins (`include`); `Comparable` and `Enumerable` give you many methods for one.

In [chapter 7](https://alice-lang.org/documentation/learn/sum-types.md) you will meet a type that is one of several shapes, and the `case` that takes it apart.

## Continue reading

- Previous: [5. Strings and text](https://alice-lang.org/documentation/learn/strings.md)
- Next: [7. Sum types and pattern matching](https://alice-lang.org/documentation/learn/sum-types.md)
