Особенности 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, что сделает их еще более мощными и позволит использовать множество различных вариантов использования.
Оригинал