🌱 Beginner's Guide lesson 8 of 12

Your Own Types with sampoan

So far we've used the types steav2 gives us: numbers, text, Booleans and arrays. But what if you're making a game with pets? A pet has a name and an age and a favourite food. You could keep three separate variables for each pet, but that gets messy fast. Instead, you can invent a brand new type called Pet that bundles them all together.

What is a sampoan?

sampoan means struct (short for "structure"). A struct is a type you design yourself, made up of named pieces called fields:

[ try-it.sts ]
bongSlanhOun "pets";
sampoan Pet {
    akthe name: Ahsor;
    akthe age: LekKut;
}

akthe myPet = Pet("Mew", 3);
jongyeytha myPet;
jongyeytha myPet.name;
jongyeytha myPet.age;
[ what it prints ]
Pet(Mew, 3)
Mew
3

Let's unpack that:

  • sampoan Pet { ... } is the blueprint. It says "a Pet has a name, which is text, and an age, which is a whole number". Each field is written like a variable with its type, but without a value.
  • Pet("Mew", 3) builds a pet from the blueprint. You list a value for each field, in the same order as in the blueprint: first the name, then the age.
  • myPet.name reads one field, using a dot. Read it as "myPet's name".

By tradition, type names start with a capital letter (Pet, Ahsor, LekKut), and variables start with a small one (myPet).

Changing fields

You can change a field just like a variable:

[ try-it.sts ]
bongSlanhOun "birthday";
sampoan Pet {
    akthe name: Ahsor;
    akthe age: LekKut;
}

akthe myPet = Pet("Mew", 3);
myPet.age += 1;
myPet.name = "Mew the Great";
jongyeytha myPet;
[ what it prints ]
Pet(Mew the Great, 4)

Methods

A struct can have its own functions too, written inside the blueprint. These are called methods (you've already used some: .len() and .push(x) on arrays). Inside a method, you can use the fields by name:

[ try-it.sts ]
bongSlanhOun "methods";
sampoan Rectangle {
    akthe width: Lek;
    akthe height: Lek;

    rupamun area() -> Lek {
        morvenh width * height;
    }

    rupamun isSquare() -> Boolean {
        morvenh width == height;
    }
}

akthe door = Rectangle(1, 2);
akthe tile = Rectangle(3, 3);
jongyeytha "door area: " + door.area();
jongyeytha "tile area: " + tile.area();
jongyeytha tile.isSquare();
[ what it prints ]
door area: 2
tile area: 9
ok

When you call door.area(), width and height mean door's width and height. Call tile.area() and they mean tile's.

Structs are copied

Here's something important about structs. When you put a struct into another variable, you get a copy, a completely separate twin:

[ try-it.sts ]
bongSlanhOun "copies";
sampoan Point {
    akthe x: Lek;
    akthe y: Lek;
}

akthe a = Point(1, 2);
akthe b = a;
b.x = 100;
jongyeytha a;
jongyeytha b;
[ what it prints ]
Point(1, 2)
Point(100, 2)

Changing b didn't touch a. It's like photocopying a drawing and then scribbling on the copy: the original stays neat. The same happens when you pass a struct into a function: the function gets its own copy to play with.

Because a method also gets a copy, a struct's method isn't allowed to change the struct's fields. steav2 stops you, rather than letting the change silently vanish:

[ x_x broken on purpose ]
bongSlanhOun "no changes";
sampoan Pet {
    akthe name: Ahsor;
    akthe age: LekKut;

    rupamun birthday() {
        age += 1;
    }
}
[ what it prints ]
[line 7] Error at 'age': a sampoan method gets a copy of nis, it can't change 'age' (mutate the struct from outside instead)

(nis means this: the value a method was called on. You'll see a lot more of it in the next lesson.) The struct way to do it is to have the method return a new, changed copy, and store that:

[ try-it.sts ]
bongSlanhOun "older";
sampoan Pet {
    akthe name: Ahsor;
    akthe age: LekKut;

    rupamun older() -> Pet {
        morvenh Pet(name, age + 1);
    }
}

akthe myPet = Pet("Mew", 3);
myPet = myPet.older();
jongyeytha myPet;
[ what it prints ]
Pet(Mew, 4)

Arrays of structs

Structs and arrays work great together. The type of a list of pets is [Pet]:

[ try-it.sts ]
bongSlanhOun "shelter";
sampoan Pet {
    akthe name: Ahsor;
    akthe age: LekKut;
}

akthe shelter: [Pet] = [Pet("Mew", 3), Pet("Rex", 5)];
shelter.push(Pet("Bubbles", 1));
somhab (akthe i: LekKut = 0; i < shelter.len(); i += 1) {
    jongyeytha shelter[i].name + " is " + shelter[i].age;
}
[ what it prints ]
Mew is 3
Rex is 5
Bubbles is 1

Bonus: teaching + new tricks

What does + mean for two points? steav2 doesn't know until you tell it! You can write a function whose name is an operator, and steav2 will use it whenever that operator appears between your types. This is called operator overloading:

[ try-it.sts ]
bongSlanhOun "operators";
sampoan Point {
    akthe x: Lek;
    akthe y: Lek;
}

rupamun +(a: Point, b: Point) -> Point {
    morvenh Point(a.x + b.x, a.y + b.y);
}

rupamun ==(a: Point, b: Point) -> Boolean {
    morvenh a.x == b.x ng a.y == b.y;
}

akthe start = Point(1, 2);
akthe step = Point(10, 0);
akthe finish = start + step;
jongyeytha finish;
jongyeytha finish == Point(11, 2);
[ what it prints ]
Point(11, 2)
ok

Without the == function, finish == Point(11, 2) wouldn't work at all: steav2 never guesses what "equal" should mean for your own types.

Your turn

  1. Make a sampoan Book with a title, an author and a number of pages, build two books, and print them. Hint: Book("...", "...", 300) in field order.
  2. Add a method isLong() -> Boolean to Book that says whether it has more than 300 pages.
  3. Make a sampoan Money with one field cents: LekKut, and a + for it, so Money(250) + Money(99) prints Money(349).

Solution for exercise 3:

[ try-it.sts ]
bongSlanhOun "money";
sampoan Money {
    akthe cents: LekKut;
}

rupamun +(a: Money, b: Money) -> Money {
    morvenh Money(a.cents + b.cents);
}

jongyeytha Money(250) + Money(99);
[ what it prints ]
Money(349)
  • steav2♡ help
  • Y2K✓ ready
  • wasminside
  • .stsvalid

best viewed at 1024x768 ☆ made with ♡ ☆ the reference is steav2's own docs