Structs, enums, and classes
rut has three user-defined type forms, each with one job:
enum— a small set of named constants.struct— an open data record: every field public, literal construction everywhere.class— a sealed record: private fields, construction through class methods only.
In all three, the type body is fields only — every method lives in
an impl block. A fn written inside a type body is a parse error.
The reference pages are
structs,
enums, and
classes and constructors.
Enums
An enum is a distinct named type over integer constants. No payloads, no methods, no computed members — where another language would use a union of literal strings, rut uses an enum:
use ink::{ Logger };
enum Light { Green, Yellow, Red }
enum Direction { Up = 1, Down, Left, Right } // 1, 2, 3, 4
pub fn main() {
let log = Logger.new("enums");
log.info(f"{Light.Green} {Direction.Left} {Direction.Right}");
}
Green Left Right
Members are the enum’s values and are spelled qualified:
Light.Green. Explicit initializers set where the numbering starts;
the rest continue from there.
when over an enum must be exhaustive — every member, or an else
arm (see control flow and when). Enums render as
their member name in format strings.
Structs — open records
Declare with struct, construct with a literal — anywhere in a
function body, nested inside other literals. There is no new:
use ink::{ Logger };
struct Point {
x: f32;
y: f32;
}
struct Rect {
min: Point;
max: Point;
}
struct Style {
color: u32 = 0xff00ff; // field initializer: literals may omit it
width: f32 = 1;
}
pub fn main() {
let log = Logger.new("structs");
let p = Point { x: 1, y: 2 }; // every field, by name
let s = Style {}; // defaults fill the rest
let r = Rect { min: Point { x: 0, y: 0 }, max: p };
log.info(f"r.max.x={r.max.x} width={s.width}");
}
r.max.x=1 width=1
All fields are public, always — member visibility in a struct is a compile error. Privacy is what classes are for.
Structs share. A struct value is a handle to a heap cell:
assignment, arguments, and returns all pass the handle, and a write
through any alias is visible through all of them. Writing needs a
mut binding or mut parameter:
use ink::{ Logger };
struct Point { x: f32; y: f32 }
pub fn main() {
let log = Logger.new("sharing");
let mut p = Point { x: 1, y: 2 };
let q = p; // q and p name ONE cell
p.x = 4; // q.x is 4 now
log.info(f"q.x={q.x} same cell: {q == p}");
}
q.x=4 same cell: true
== on struct values is cell identity — q == p is true (one
cell), p == Point { x: 4, y: 2 } is false (a different cell). To
compare field by field, write a function.
Recursive shapes are legal — a record may name itself through a
nullable field, because ?Node is one word:
use ink::{ Logger };
struct Node {
value: i32;
left: ?Node;
right: ?Node;
}
fn count(n: ?Node) -> i32 {
let mut c = 1;
if (n.left != nil) { c += count(n.left); }
if (n.right != nil) { c += count(n.right); }
return c;
}
pub fn main() {
let log = Logger.new("nodes");
let n = Node {
value: 1,
left: Node { value: 2, left: nil, right: nil },
right: nil,
};
log.info(f"count={count(n)}");
}
count=2
Classes — sealed records
A class adds three things to a struct: module-private fields,
construction gated through class methods, and the option of cleanup
hooks. There is no constructor keyword, no new operator, and no
outside literal — the only way to build a class value from outside is
to call a class method that chooses to.
use ink::{ Logger };
class Counter {
n: i32 = 0; // module-private — the class's business
}
impl Counter {
pub fn new() -> Self { // the construction surface
return Self { }; // the class-private literal
}
pub fn press(mut self) {
self.n = self.n.wrapping_add(1);
}
pub fn count(self) -> i32 { return self.n; }
}
pub fn main() {
let log = Logger.new("counter");
let c = Counter.new();
c.press();
c.press();
log.info(f"count={c.count()}"); // 2
}
count=2
The pieces:
- A class method is just a function without
self.Rect.new(w, h),Version.parse(s),Rect.from_square(s)— any no-selfmethod returningSelfis a constructor.newis a convention, not syntax. - The
Self { .. }literal is class-private — legal anywhere in the class’s own impl block, never outside. This is the seal. - Instance methods spell
selfexplicitly as the first parameter;mut selfmarks methods that write. There is nothis, no static methods, noget/setsyntax — a computed property is a method (c.count()). - Construction is validation. A constructor is an ordinary function — it can check arguments and refuse:
use ink::{ Logger };
class Rect {
w: f32;
h: f32;
}
impl Rect {
pub fn new(w: f32, h: f32) -> Self {
if (w <= 0 || h <= 0) {
panic("Rect: negative extents");
}
return Self { w: w, h: h };
}
pub fn from_square(s: f32) -> Self {
return Rect.new(s, s);
}
pub fn area(self) -> f32 { return self.w * self.h; }
}
pub fn main() {
let log = Logger.new("rect");
let r = Rect.new(3, 4);
let sq = Rect.from_square(2);
log.info(f"area={r.area()} square={sq.area()}");
}
area=12 square=4
A try-constructor answers the nullable — nil is a failed
validation, not a crash:
impl Version {
pub fn parse(s: str) -> ?Version {
// ... split "1.2" — on failure:
return nil;
}
}
No inheritance. There is no extends, no super, no overriding.
Code sharing is composition (hold a helper in a field) or free
functions; polymorphism is traits — the next chapter.
Visibility recap
- Struct members: public, always.
- Class members: unannotated is module-private;
pubexposes to importers (alsopub(mod),pub(super),pub(self)). - The types themselves follow the same rule:
pub class Vec<T>is importable, a bareclass Helperstays in its module. See modules and packages.
Struct or class?
Default to a struct: data in, data out, shared like every other value. Reach for a class when the type has rules that construction must enforce, holds private state that outsiders must not read, or owns a resource that needs explicit cleanup.
Put it together
use ink::{ Logger };
class Counter {
n: i32 = 0;
}
impl Counter {
pub fn new() -> Self {
return Self { };
}
pub fn press(mut self) {
self.n = self.n.wrapping_add(1);
}
pub fn count(self) -> i32 { return self.n; }
}
class Rect {
w: f32;
h: f32;
}
impl Rect {
pub fn new(w: f32, h: f32) -> Self {
if (w <= 0 || h <= 0) {
panic("Rect: negative extents");
}
return Self { w: w, h: h };
}
pub fn from_square(s: f32) -> Self {
return Rect.new(s, s);
}
pub fn area(self) -> f32 { return self.w * self.h; }
}
pub fn main() {
let log = Logger.new("classes");
let c = Counter.new();
c.press();
c.press();
let r = Rect.new(3, 4);
let sq = Rect.from_square(2.0f32);
log.info(f"count={c.count()} area={r.area()} square={sq.area()}");
}
count=2 area=12 square=4
Next: traits and impl blocks.