As características foram introduzidas no Mojo recentemente, então pensei em experimentá-las. Atualmente, as características integradas incluem CollectionElement , Copyable , Destructable , Intable , Movable , Sized e Stringable (parece que o sufixo "-able" é uma coisa nessas convenções de nomenclatura!).
As características funcionam em estruturas. Para implementar uma característica, basta adicionar um método a uma estrutura que corresponda à característica; em seguida, passe o nome da característica como parâmetro:
@value struct Duck(Quackable): fn quack(self): print("Quack!")
O decorador @value
no Mojo insere os métodos de ciclo de vida como __init__()
, __copyinit__()
e __moveinit__()
na estrutura, simplificando um pouco nossa vida, pois não precisamos adicioná-los nós mesmos.
As características no Mojo ainda não suportam implementações padrão para métodos, portanto, o ...
no corpo do método Quackable
acima. Você também pode usar pass
, que terá o mesmo efeito que em Python.
Apesar do nome diferente, a abordagem básica das características no Mojo me lembra as interfaces Go. No Go, você definiria uma estrutura Duck
e implementaria uma interface Quackable
como esta:
type Quackable interface { quack() }
E para fazer uma estrutura que satisfaça esta interface:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
Compare-o com uma implementação do Mojo:
trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")
Acho que a versão Mojo é ainda mais concisa, aninhando a definição do método quack()
dentro do tipo de estrutura Duck
. Além de trabalhar em estruturas, as características no Mojo não requerem uma palavra-chave implements
, assim como no Go.
Curiosamente, nos documentos do Mojo, afirma-se que as implementações padrão ainda não são permitidas, o que significa que poderão ser permitidas no futuro. Isso significa que o Mojo pode seguir uma abordagem diferente do Go, que se concentra em estruturas que satisfazem uma interface, e não em implementá -la.
Implementações de métodos padrão não são necessárias em Go, e um efeito semelhante é alcançado com a incorporação, já que a implementação de interface/característica é simplesmente um conceito que não existe em Go. As coisas podem ser diferentes no Mojo.
O valor das características e interfaces é tornar o código reutilizável. Por exemplo, no Mojo, você pode escrever funções que aceitam tipos de características…
fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()
E então passe estruturas diferentes que implementam a mesma característica da entrada - tudo funcionará! Por exemplo, aqui está uma estrutura Goose
que implementa Quackable
:
@value struct Goose(Quackable): fn quack(self): print("Honk!")
E aqui, para chamar make_it_quack
tanto no Goose
quanto no Duck
(lembre-se, você precisa da função main
no Mojo como ponto de entrada para o seu programa):
def main(): make_it_quack(Duck()) make_it_quack(Goose())
A saída disso será
Quack! Honk!
Se eu tentei passar algo que não implementa o traço Quackable
para a função make_it_quack
, digamos StealthCow
, o programa não será compilado:
@value struct StealthCow(): pass
make_it_quack(StealthCow())
A mensagem de erro abaixo poderia ter sido mais descritiva; talvez a equipe Mojo melhore isso?
error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified
O mesmo se eu remover o método quack
de Goose
; aqui, obtemos um erro descritivo agradável:
struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented
A vantagem desse tipo de verificação de tipo estático sobre a abordagem Python pura é que podemos detectar erros facilmente e evitar enviá-los para a produção, pois o código simplesmente não será compilado.
As características no Mojo já suportam herança, então nossa característica `Quackable` pode estender uma característica `Audible` da seguinte forma:
trait Audible: fn make_sound(self): ...
trait Quackable(Audible): fn quack(self): ...
Isso significa que uma estrutura Duck
terá que implementar tanto quack
quanto make_sound
para estar em conformidade com a característica Quackable
.
Isso é semelhante ao conceito de "incorporação de interface" em Go, onde para criar uma nova interface que herda de outras interfaces, você incorporaria interfaces pai assim:
type Quackable interface { Audible // includes methods of Audible in Quackable's method set }
As características também aceitam métodos estáticos que funcionam sem criar uma instância de uma estrutura:
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()
Você chama um método estático como este:
def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()
O que produzirá:
Quack! Honk! Swimming
Observe que na última chamada de método, uma instância Duck não é criada. É assim que os métodos estáticos em Python funcionam, afastando-se um pouco da programação orientada a objetos. Mojo baseia-se nesta funcionalidade Python.
Curiosamente, o truque do Go com uma interface{}
que permite transmitir qualquer tipo e era popular entre a comunidade Go antes da introdução do Go Generics não funcionará com uma função fn
do tipo Mojo.
Sua estrutura deve implementar pelo menos um dos métodos de ciclo de vida como __len__
ou __str__
, que neste caso, o tornaria compatível com Sized
ou Stringable
, para ser usado com funções que aceitam tipos de características.
Na verdade, esta não é uma limitação real e faz muito sentido no Mojo, já que, com o Mojo, se você precisar de digitação dinâmica, você pode simplesmente recorrer à função def
mais flexível e, em seguida, aplicar a magia usual do Python para trabalhar com desconhecidos. tipos.
As funções mais estritas do Mojo fn
também funcionam em estruturas genéricas usando tipos como DynamicVector
; leia mais sobre isso aqui .
Outra limitação que vejo tem a ver com estruturas e seus métodos, e não com características, e é um obstáculo potencial para a implementação da Arquitetura Limpa/separação do código em partes diferentes.
Considere um dos exemplos anteriores com uma definição de método de estrutura Go vs. Mojo:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
@value struct Duck(Quackable): fn quack(self): print("Quack!")
O exemplo Mojo, seguindo uma sintaxe mais parecida com Python, aninha a definição do método diretamente dentro da estrutura, enquanto a versão Go permite separá-la da própria estrutura. Nesta versão, se eu tiver uma estrutura muito longa com muitos tipos e métodos, será um pouco mais difícil de ler.
Porém, não é uma diferença crítica, apenas algo para estar ciente.
As características do Mojo já são bastante úteis, apesar da linguagem estar em seus primórdios. Embora a filosofia Go seja baseada na simplicidade e tente manter os recursos no mínimo, é provável que vejamos mais funcionalidades adicionadas às características do Mojo no futuro, tornando-as ainda mais poderosas e permitindo vários casos de uso diferentes.