Структуры
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.
Начальные значения задаются в конструкторе.
class Point: def __init__(self, x, y): self.x = x self.y = y
p = Point(3, 4)p.z = 5 # можноstruct Point: var x: Int var y: Int
def __init__(out self, x: Int, y: Int): self.x = x self.y = yPython хранит поля в словаре, поэтому их можно добавлять на ходу. Mojo раскладывает структуру в памяти заранее — зато обращение к полю идёт по смещению, известному на этапе компиляции, а не через поиск по хеш-таблице.
@fieldwise_init вместо ручного конструктора
Заголовок раздела «@fieldwise_init вместо ручного конструктора»Конструктор, который просто раскладывает аргументы по полям, писать не обязательно:
@fieldwise_initstruct 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_initstruct 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_initstruct 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_initstruct 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_initstruct 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_initstruct 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_initstruct 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_initstruct 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_initstruct 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 раз два
Два момента, на которых спотыкаются все:
- Требования к типу перечисляются через
&, и каждое здесь работает:Deinitableнужен, чтобы значение вообще можно было положить в поле (error: field 'first' has non-'Deinitable' type 'T'),ImplicitlyCopyable— чтобы сработал@fieldwise_initи самаPairумела копироваться,Writable— чтобыshow()могла её напечатать. - Внутри структуры на параметр ссылаются как
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.
Всё вместе
Заголовок раздела «Всё вместе»Корзина интернет-магазина: две структуры, статический конструктор, константы, своя печать и своё сравнение.
# Структуры на практике: товар, корзина и трейты.
@fieldwise_initstruct 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 отличается от простого
импорта соседнего файла.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0