Особенности Mojo: как они соотносятся с интерфейсами Go?
26 декабря 2023 г.Недавно в Моджо появились черты характера, поэтому я решил их опробовать. В настоящее время встроенные признаки включают CollectionElement, Копируемый, Разрушаемый< /a>, Intable, Перемещаемый, Размер и Stringable (похоже, что суффикс "-able" вещь в этих соглашениях об именах!). n n Трейты работают со структурами. Чтобы реализовать признак, вы просто добавляете метод в структуру, соответствующую этому признаку; затем передайте имя признака в качестве параметра: n
@value
struct Duck(Quackable):
fn quack(self):
print("Quack!")
Декоратор @value
в Mojo вставляет методы жизненного цикла, такие как __init__()
, __copyinit__()
и __moveinit__()
в структуру, что немного упрощает нашу жизнь, поскольку нам не нужно добавлять их самостоятельно.
Трейты в Mojo пока не поддерживают реализации методов по умолчанию, поэтому ...
в теле метода Quackable
выше. Вы также можете использовать pass
, который будет иметь тот же эффект, что и в Python.
Тракты Mojo и интерфейсы Go
Несмотря на другое название, основной подход к трейтам в 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!
Ошибки характеристик
Если бы я попытался передать что-то, что не реализует признак Quackable
, в функцию make_it_quack
, скажем, 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.
Ограничения возможностей Mojo по сравнению с интерфейсами Go
Интересно, что трюк Go с пустым интерфейсом{}
, который позволяет передавать любой тип и был популярен в сообществе Go до появления универсальных шаблонов Go, не будет работать с функцией типа Mojo < код>фнкод>.
Ваша структура должна реализовать хотя бы один из методов жизненного цикла, например __len__
или __str__
, что в данном случае приведет к тому, что она будет соответствовать Sized
. или Stringable
для использования с функциями, которые принимают типы свойств.
На самом деле это не является реальным ограничением и имеет большой смысл в Mojo, поскольку в Mojo, если вам нужна динамическая типизация, вы можете просто вернуться к более гибкой функции def
, а затем применить обычная магия Python для работы с неизвестными типами.
Более строгие функции Mojo fn
также работают с общими структурами, используя такие типы, как DynamicVector
; подробнее об этом читайте здесь.
Еще одно ограничение, которое я вижу, связано со структурами и их методами, а не с типажами, и является потенциальным препятствием для реализации чистой архитектуры/разделения кода на разные части.
Рассмотрим один из предыдущих примеров с определением метода структуры Go и 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, что сделает их еще более мощными и позволит использовать множество различных вариантов использования.
Оригинал