Skip to content

Структуры

This content is not available in your language yet.

Классов в Mojo 1.0 нет — совсем:

error: classes are not supported yet
Что это значит

Слово class компилятор знает, но пока не поддерживает.

Как исправить

Используйте struct. Это не «урезанный класс», а другой инструмент: структура полностью раскладывается на этапе компиляции, поэтому она быстрее — и поэтому же в ней нельзя менять набор полей на ходу.

struct Point:
var x: Int
var y: Int
def __init__(out self, x: Int, y: Int):
self.x = x
self.y = y
def distance_sq(self) -> Int:
return self.x * self.x + self.y * self.y
def main():
var p = Point(3, 4)
print(p.x, p.y, p.distance_sq())
Результат

3 4 25

Три вещи, которые видит питонист сразу:

  • поля объявляются заранее, с типами, через var;
  • вместо self в первом аргументе конструктора — out self;
  • self в обычных методах есть, но передаётся неявно, как в Python.

out означает «этот аргумент функция создаёт». В момент входа в __init__ объекта ещё нет — он появляется, когда все поля получат значения. Компилятор за этим следит.

Это главное отличие от Python:

error: 'Point' value has no attribute 'z'
Что это значит

Вы попытались добавить поле объекту после создания — p.z = 5. В Mojo так нельзя: набор полей определён при компиляции, из него складывается размер структуры в памяти.

Как исправить

Объявите поле в самой структуре. Если набор данных заранее неизвестен, вам нужен не struct, а словарь.

Отсюда же и скорость: обращение к полю — это чтение по известному смещению, а не поиск по хеш-таблице __dict__, как в Python.

Значения по умолчанию у полей тоже не поддерживаются — попытка написать var x: Int = 0 даёт error: unknown tokens at the end of a declaration. Начальные значения задаются в конструкторе.

🐍 Python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
p.z = 5 # можно
🔥 Mojo
struct Point:
var x: Int
var y: Int
def __init__(out self, x: Int, y: Int):
self.x = x
self.y = y

Python хранит поля в словаре, поэтому их можно добавлять на ходу. Mojo раскладывает структуру в памяти заранее — зато обращение к полю идёт по смещению, известному на этапе компиляции, а не через поиск по хеш-таблице.

Конструктор, который просто раскладывает аргументы по полям, писать не обязательно:

@fieldwise_init
struct Point:
var x: Int
var y: Int
def main():
var p = Point(3, 4)
print(p.x, p.y)
Результат

3 4

Декоратор создаёт конструктор с аргументами в порядке объявления полей. В курсе он встречается постоянно — просто потому, что большинству структур ничего другого и не нужно.

__init__ перегружается так же, как обычные функции:

struct Temperature:
var celsius: Float64
def __init__(out self, celsius: Float64):
self.celsius = celsius
def __init__(out self, *, fahrenheit: Float64):
self.celsius = (fahrenheit - 32.0) / 1.8
def main():
var a = Temperature(20.0)
var b = Temperature(fahrenheit=68.0)
print(a.celsius, b.celsius)
Результат

20.0 20.0

Звёздочка здесь не украшение, а необходимость. Два конструктора с одним позиционным Float64 объявить нельзя — имена аргументов в сигнатуру не входят:

error: redefinition of function '__init__' with identical signature

Слово * делает fahrenheit только именованным, сигнатуры расходятся — и заодно вызов Temperature(fahrenheit=68.0) читается однозначно.

По умолчанию метод получает self только для чтения. Чтобы менять поля, нужен mut self:

@fieldwise_init
struct Counter(ImplicitlyCopyable):
var value: Int
def bump(mut self):
self.value += 1
def show(self):
print("значение:", self.value)
def main():
var c = Counter(0)
c.bump()
c.bump()
c.show()
Результат

значение: 2

Это те же соглашения об аргументах, что и у обычных функций из главы Функции — просто применённые к self.

Есть и статические методы, без self:

struct Config:
var size: Int
def __init__(out self, size: Int):
self.size = size
@staticmethod
def default() -> Config:
return Config(16)
def main():
print(Config.default().size)
Результат

16

Значение, известное на этапе компиляции, объявляется через comptime и живёт у типа, а не у экземпляра:

struct Circle:
var radius: Float64
comptime PI = 3.14159
def __init__(out self, radius: Float64):
self.radius = radius
def area(self) -> Float64:
return Self.PI * self.radius * self.radius
def main():
print(Circle(2.0).area())
print(Circle.PI)
Результат

12.56636 3.14159

Внутри структуры к ней обращаются через Self, снаружи — через имя типа. Памяти в экземпляре такая константа не занимает вовсе.

Вот место, где Mojo расходится с Python сильнее всего. Обычное присваивание структуры не работает:

error: value of type 'Point' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
Что это значит

var b = a для структуры — это запрос на копию. Mojo не делает копий молча: копия может оказаться дорогой, и вы должны знать, что она есть.

Как исправить

Компилятор перечисляет варианты прямо в подсказках:

note: consider transferring the value with '^'
note: you can copy it explicitly with '.copy()'

Второй совет появляется, только если структура объявлена Copyable.

Уровней ровно три, и выбирает их автор структуры:

Что объявленоЧто можно делать
ничеготолько передать владение: var b = a^
Copyableплюс явная копия: var b = a.copy()
ImplicitlyCopyableплюс обычное var b = a
@fieldwise_init
struct Point(Copyable):
var x: Int
def main():
var a = Point(1)
var b = a.copy()
var c = a^
print(b.x, c.x)
Результат

1 1

С ImplicitlyCopyable структура ведёт себя как обычное число: копия независима, изменение одной переменной не трогает другую.

@fieldwise_init
struct Point(ImplicitlyCopyable):
var x: Int
def main():
var a = Point(1)
var b = a
b.x = 99
print(a.x, b.x)
Результат

1 99

Перемещение (^) доступно всегда и никаких трейтов не требует: структура просто переезжает в новую переменную, старая после этого недоступна.

Некоторые трейты не требуют ни строчки кода — достаточно объявить их в скобках.

@fieldwise_init
struct Point(ImplicitlyCopyable, Writable):
var x: Int
var y: Int
def main():
print(Point(3, 4))
Результат

Point(x=3, y=4)

Похоже на @dataclass из Python, только бесплатно: представление собирается на этапе компиляции.

Свой вид печати задаётся методом write_to:

@fieldwise_init
struct Point(ImplicitlyCopyable, Writable):
var x: Int
def write_to[W: Writer](self, mut writer: W):
writer.write("[", self.x, "]")
def main():
print(Point(5))
Результат

[5]

@fieldwise_init
struct City(ImplicitlyCopyable, KeyElement):
var name: String
def main() raises:
var codes = Dict[City, Int]()
codes[City("Москва")] = 495
print(codes[City("Москва")])
Результат

495

Трейт KeyElement — это «умею сравниваться и хешироваться». Реализация выводится из полей, а вместе с ней бесплатно появляется и оператор ==.

Из имеющихся в 1.0 трейтов чаще всего встречаются Copyable, Movable, ImplicitlyCopyable, Deinitable, Writable, Hashable, KeyElement, Sized, Comparable, Boolable, Intable, Defaultable. Питоновских Stringable и Representable в 1.0 нет — за печать отвечает Writable.

Метод __deinit__ вызывается, когда значение больше не нужно:

@fieldwise_init
struct Resource:
var name: String
def __deinit__(deinit self):
print("освобождаем:", self.name)
def main():
var r = Resource("файл")
print("работаем с", r.name)
print("конец main")
Результат

работаем с файл освобождаем: файл конец main

Посмотрите на порядок внимательно: освобождение произошло до последней строки. Mojo уничтожает значение сразу после его последнего использования — не в конце области видимости, как C++, и не по счётчику ссылок с досбором циклов, как CPython.

error: structs only conform to traits or trait compositions; remove the struct type from the conformance list
Что это значит

В скобках после имени структуры перечисляются только трейты. Наследовать структуру от структуры нельзя.

Как исправить

Два обычных решения: композиция — вложить одну структуру полем в другую; или трейт — описать общее поведение и реализовать его в нескольких структурах.

Для питониста это звучит как потеря, но на практике композиция почти всегда выигрывает у наследования в читаемости — а трейты покрывают те случаи, ради которых наследование обычно и заводят.

Структура может работать с любым типом — тип указывается параметром в квадратных скобках:

@fieldwise_init
struct Pair[T: ImplicitlyCopyable & Writable & Deinitable](ImplicitlyCopyable):
var first: Self.T
var second: Self.T
def show(self):
print(self.first, self.second)
def main():
var a = Pair[Int](1, 2)
var b = Pair[String]("раз", "два")
a.show()
b.show()
Результат

1 2 раз два

Два момента, на которых спотыкаются все:

  1. Требования к типу перечисляются через &, и каждое здесь работает: Deinitable нужен, чтобы значение вообще можно было положить в поле (error: field 'first' has non-'Deinitable' type 'T'), ImplicitlyCopyable — чтобы сработал @fieldwise_init и сама Pair умела копироваться, Writable — чтобы show() могла её напечатать.
  2. Внутри структуры на параметр ссылаются как Self.T:
error: unqualified access to struct parameter 'T'; use 'Self.T' instead
Что это значит

Вы написали var first: T. Внутри тела структуры её собственные параметры пишутся через Self.

Как исправить

var first: Self.T. Решение компилятор подсказывает прямо в тексте ошибки.

Так устроены и List[Int], и Dict[String, Int] — никакой особой магии в них нет, они написаны на том же Mojo.

Корзина интернет-магазина: две структуры, статический конструктор, константы, своя печать и своё сравнение.

cart.mojo
# Структуры на практике: товар, корзина и трейты.
@fieldwise_init
struct Product(ImplicitlyCopyable, Writable):
"""Товар: название, цена в рублях и количество."""
var name: String
var price: Int
var quantity: Int
@staticmethod
def single(name: String, price: Int) -> Product:
"""Создаёт товар в единственном экземпляре."""
return Product(name, price, 1)
def total(self) -> Int:
"""Считает стоимость позиции целиком."""
return self.price * self.quantity
def __eq__(self, other: Self) -> Bool:
"""Товары считаются одинаковыми, если совпало название."""
return self.name == other.name
def write_to[W: Writer](self, mut writer: W):
"""Печатает позицию так, как её увидит покупатель."""
writer.write(self.name, " x", self.quantity, " = ", self.total(), " руб.")
struct Cart(Writable):
"""Корзина: товары плюс правила доставки."""
comptime FREE_DELIVERY_FROM = 3000
comptime DELIVERY_PRICE = 300
var items: List[Product]
def __init__(out self):
"""Создаёт пустую корзину."""
self.items = List[Product]()
def add(mut self, item: Product):
"""Добавляет позицию в корзину."""
self.items.append(item)
def subtotal(self) -> Int:
"""Считает стоимость товаров без доставки."""
var sum = 0
for item in self.items:
sum += item.total()
return sum
def delivery(self) -> Int:
"""Возвращает стоимость доставки с учётом порога бесплатной."""
return 0 if self.subtotal() >= Self.FREE_DELIVERY_FROM else Self.DELIVERY_PRICE
def write_to[W: Writer](self, mut writer: W):
"""Печатает чек целиком."""
for item in self.items:
writer.write(item, "\n")
writer.write("товаров на ", self.subtotal(), " руб.\n")
writer.write("доставка: ", self.delivery(), " руб.\n")
writer.write("итого: ", self.subtotal() + self.delivery(), " руб.")
def main():
var cart = Cart()
cart.add(Product("Клавиатура", 2500, 1))
cart.add(Product("Мышь", 900, 2))
cart.add(Product.single("Коврик", 400))
print(cart)
print()
print("порог бесплатной доставки:", Cart.FREE_DELIVERY_FROM)
print("первая позиция — клавиатура:", cart.items[0] == Product("Клавиатура", 1, 1))
var small = Cart()
small.add(Product.single("Кабель", 350))
print()
print(small)
Результат

Клавиатура x1 = 2500 руб. Мышь x2 = 1800 руб. Коврик x1 = 400 руб. товаров на 4700 руб. доставка: 0 руб. итого: 4700 руб.

порог бесплатной доставки: 3000 первая позиция — клавиатура: True

Кабель x1 = 350 руб. товаров на 350 руб. доставка: 300 руб. итого: 650 руб.

🎯 Проверь себя

Почему `var b = a` для структуры не компилируется?

Mojo не копирует молча. Структура должна объявить ImplicitlyCopyable, чтобы обычное присваивание делало копию. Иначе есть два явных пути: передать владение через a^ или, если объявлен Copyable, написать a.copy().

Что напечатает `print(p)` для структуры с трейтом `Writable`, если не писать `write_to`?

Имя типа и все поля: Point(x=3, y=4). Представление собирается компилятором из объявления структуры, как у @dataclass в Python, только без затрат во время выполнения.

Когда вызывается `__deinit__`?

Сразу после последнего использования значения, а не в конце функции. Поэтому в примере строка «освобождаем» печатается раньше, чем «конец main».

Чем заменить наследование?

Композицией — вложить структуру полем в другую структуру — или трейтом, если нужно описать общее поведение для нескольких типов. Наследования структур в Mojo нет: в скобках перечисляются только трейты.

Модули и пакеты: как разложить код по файлам, что такое __init__.mojo и чем mojo package отличается от простого импорта соседнего файла.

Примеры проверены на Mojo 1.0.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0