paint-brush
Особенности Mojo: как они соотносятся с интерфейсами Go?к@a2svior
1,848 чтения
1,848 чтения

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

к Valentin Erokhin6m2023/12/26
Read on Terminal Reader

Слишком долго; Читать

Пошаговое руководство по работе с трейтами Mojo с примерами и сравнением трейтов Mojo с интерфейсами Go. Ограничения и предостережения черт Моджо.
featured image - Особенности Mojo: как они соотносятся с интерфейсами Go?
Valentin Erokhin HackerNoon profile picture
0-item

Черты были введены в Моджо недавно, поэтому я решил их попробовать. В настоящее время встроенные свойства включают 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

Несмотря на другое название, базовый подход к трейтам в 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.

Ограничения свойств Mojo по сравнению с интерфейсами Go

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