Черты были введены в Моджо недавно, поэтому я решил их попробовать. В настоящее время встроенные свойства включают CollectionElement , Copyable , Destructable , Intable , Movable , Sized и Stringable (похоже, суффикс «-able» присутствует в этих соглашениях об именах!).
Трейты работают со структурами. Чтобы реализовать признак, вы просто добавляете метод в структуру, соответствующую этому признаку; затем передайте имя черты в качестве параметра:
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Декоратор @value
в Mojo вставляет в структуру методы жизненного цикла, такие как __init__()
, __copyinit__()
и __moveinit__()
, немного упрощая нашу жизнь, поскольку нам не нужно добавлять их самостоятельно.
Трейты в Mojo пока не поддерживают реализации методов по умолчанию, поэтому ...
в теле метода Quackable
выше. Вы также можете использовать pass
, который будет иметь тот же эффект, что и в Python.
Несмотря на другое название, базовый подход к трейтам в Mojo напоминает мне интерфейсы Go. В Go вы должны определить структуру Duck
и реализовать интерфейс Quackable
следующим образом:
type Quackable interface { quack() }
И чтобы создать структуру, удовлетворяющую этому интерфейсу:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
Сравните это с реализацией Mojo:
trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")
Я думаю, что версия Mojo еще более лаконична: определение метода quack()
вложено в тип структуры Duck
. Помимо работы со структурами, трейты в Mojo не требуют ключевого слова implements
, как в Go.
Интересно, что в документации Mojo отмечается, что реализации по умолчанию пока не разрешены, а это означает, что они могут быть разрешены в будущем. Это означает, что Mojo может использовать подход, отличный от Go, который фокусируется на структурах, удовлетворяющих интерфейсу, а не на его реализации .
Реализации методов по умолчанию не нужны в Go, и аналогичный эффект достигается при встраивании, поскольку реализация интерфейса/характеристики — это просто концепция, которой не существует в Go. В Моджо все может быть по-другому.
Ценность свойств и интерфейсов заключается в возможности повторного использования кода. Например, в Mojo вы можете писать функции, принимающие типы признаков…
fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()
А затем передать в качестве входных данных разные структуры, реализующие ту же черту — все просто будет работать! Например, вот структура Goose
, реализующая Quackable
:
@value struct Goose(Quackable): fn quack(self): print("Honk!")
И здесь, чтобы вызвать make_it_quack
как в Goose
так и в Duck
(помните, вам нужна функция main
в Mojo как точка входа в вашу программу):
def main(): make_it_quack(Duck()) make_it_quack(Goose())
Результатом этого будет
Quack! Honk!
Если я попытаюсь передать в функцию make_it_quack
что-то, что не реализует свойство Quackable
, скажем, StealthCow
, программа не скомпилируется:
@value struct StealthCow(): pass
make_it_quack(StealthCow())
Сообщение об ошибке ниже могло бы быть более информативным; может быть, команда Mojo его улучшит?
error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified
То же самое, если я удалю метод quack
из Goose
; здесь мы получаем красивую описательную ошибку:
struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented
Преимущество такого рода статической проверки типов по сравнению с подходом на чистом Python заключается в том, что мы можем легко обнаружить ошибки и избежать отправки ошибок в производство, поскольку код просто не будет компилироваться.
Трейты в Mojo уже поддерживают наследование, поэтому наш трейт Quackable может расширять трейт Audible следующим образом:
trait Audible: fn make_sound(self): ...
trait Quackable(Audible): fn quack(self): ...
Это означает, что структура Duck
должна будет реализовать как quack
так и make_sound
, чтобы соответствовать признаку Quackable
.
Это похоже на концепцию «встраивания интерфейса» в Go, где для создания нового интерфейса, который наследуется от других интерфейсов, вы должны встроить родительские интерфейсы следующим образом:
type Quackable interface { Audible // includes methods of Audible in Quackable's method set }
Трейты также принимают статические методы, которые работают без создания экземпляра структуры:
trait Swims: @staticmethod fn swim(): ... @value struct Duck(Quackable, Swims): fn quack(self): print("Quack!") @staticmethod fn swim(): print("Swimming")
fn make_it_swim[T: Swims](): T.swim()
Вы вызываете статический метод следующим образом:
def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()
Что выведет:
Quack! Honk! Swimming
Обратите внимание, что при последнем вызове метода экземпляр Duck не создается. Именно так работают статические методы в Python, несколько отходя от объектно-ориентированного программирования. Mojo основывается на этой функциональности Python.
Интересно, что трюк Go с пустым interface{}
, который позволяет передавать любой тип и был популярен в сообществе Go до появления Go Generics, не будет работать с функцией fn
типа Mojo.
Ваша структура должна реализовать хотя бы один из методов жизненного цикла, например __len__
или __str__
, что в данном случае приведет ее в соответствие с Sized
или Stringable
для использования с функциями, которые принимают типы признаков.
На самом деле это не является реальным ограничением и имеет большой смысл в Mojo, поскольку в Mojo, если вам нужна динамическая типизация, вы можете просто вернуться к более гибкой функции def
, а затем применить обычную магию Python для работы с неизвестными типы.
Более строгие функции Mojo fn
также работают с универсальными структурами, используя такие типы, как DynamicVector
; подробнее об этом читайте здесь .
Еще одно ограничение, которое я вижу, связано со структурами и их методами, а не с типажами, и является потенциальным препятствием для реализации чистой архитектуры/разделения кода на разные части.
Рассмотрим один из предыдущих примеров с определением метода структуры Go vs. Mojo:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Пример Mojo, следуя синтаксису, более похожему на Python, вкладывает определение метода непосредственно в структуру, тогда как версия Go позволяет отделить его от самой структуры. В этой версии, если у меня очень длинная структура со множеством типов и методов, ее будет несколько сложнее читать.
Однако это не критическая разница, просто о ней следует знать.
Черты Mojo уже весьма полезны, несмотря на то, что язык находится на заре своего развития. Хотя философия Go заключается в простоте и старается свести количество функций к минимуму, вполне вероятно, что в будущем мы увидим больше функциональности, добавленной к функциям Mojo, что сделает их еще более мощными и позволит использовать множество различных вариантов использования.