Особенности Mojo: как они соотносятся с интерфейсами Go?

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE