Primitivos


Um primitivo é semelhante a uma classe, mas há duas diferenças críticas:

  • Um primitivo não tem campos.
  • Há apenas uma instância de um primitivo definido pelo usuário.

Não ter campos significa que os primitivos nunca são mutáveis. Ter uma única instância significa que se seu código chama um construtor em um tipo primitivo, ele sempre obtém o mesmo resultado de volta (exceto para os primitivos embutidos "palavra máquina", cobertos abaixo).

Para que você pode usar um primitivo?

Há três usos principais dos primitivos (quatro, se você contar os primitivos "palavra de máquina" embutida).

Como um "valor de marcador": Por exemplo, o Pony freqüentemente usa o primitivo None para indicar que algo não tem "nenhum valor". É claro que ele tem um valor, para que você possa verificar o que é, e o valor é a única instância de None.

Como um tipo de "enumeração": Ao ter uma união de tipos primitivos, você pode ter uma enumeração de tipo seguro. Mostraremos os tipos de união mais tarde.

Como uma "enumeração de funções": Como os primitivos podem ter funções, você pode agrupar funções em um tipo primitivo. Você pode ver isto na biblioteca padrão, onde as funções de manipulação de caminhos estão agrupadas no Caminho primitivo, por exemplo.

// 2 "marker values"
primitive OpenedDoor
primitive ClosedDoor
// An "enumeration" type
type DoorState is (OpenedDoor | ClosedDoor)
// A collection of functions
primitive BasicMath
fun add(a: U64, b: U64): U64 =>
a + b
fun multiply(a: U64, b: U64): U64 =>
a * b
actor Main
new create(env: Env) =>
let doorState : DoorState = ClosedDoor
let isDoorOpen : Bool = match doorState
| OpenedDoor => true
| ClosedDoor => false
end
env.out.print("Is door open? " + isDoorOpen.string())
env.out.print("2 + 3 = " + BasicMath.add(2,3).string())

Os primitivos são bastante poderosos, particularmente como enumerações. Ao contrário das enumerações em outras linguagens, cada "valor" na enumeração é um tipo completo, o que facilita a anexação de dados e funcionalidade aos valores de enumeração.

Tipos primitivos embutidos

A palavra-chave primitive> também é usada para introduzir certos tipos embutidos de "palavra máquina". Além de terem um valor associado a elas, estas funcionam como primitivas definidas pelo usuário. Estes são:

  • Bool. Este é um valor de 1 bit que ou é verdadeiro ou falso.
  • ISize, ILong, I8, I16, I32, I64, I128. Inteiros assinados de várias larguras.
  • USize, ULong, U8, U16, U32, U64, U128. Inteiros não assinados de várias larguras.
  • F32, F64. Números inteiros de pontos flutuantes de várias larguras.

ISize/USize correspondem à largura de bit do tipo nativo size_t, que varia de acordo com a plataforma. ILong/ULong correspondem igualmente à largura de bit do tipo nativo longo, que também varia de acordo com a plataforma. A largura de bit de uma int nativa é a mesma em todas as plataformas que o Pony suporta, e você pode usar I32/U32 para isso.

Inicialização e finalização primitiva

Os primitivos podem ter duas funções especiais, _init e _final. É chamado antes de qualquer ator começar. A final é chamada depois que todos os atores tiverem terminado. As duas funções não assumem nenhum parâmetro. As funções _init e _final para diferentes primitivos sempre funcionam sequencialmente.

Um caso de uso comum para isto é a inicialização e limpeza das bibliotecas C sem arriscar o uso inoportuno por um ator.

Atores

Um ator é semelhante a uma classe, mas com uma diferença crítica: um ator pode ter comportamentos. Comportamentos

Um comportamento é como uma função, exceto que as funções são síncronas e os comportamentos são assíncronos. Em outras palavras, quando se chama uma função, o corpo da função é executado imediatamente, e o resultado da chamada é o resultado do corpo da função. Isto é como uma invocação de método em qualquer outra linguagem orientada a objetos.

Mas quando você chama um comportamento, o corpo da função não é executado imediatamente. Ao invés disso, o corpo do comportamento será executado em algum momento indeterminado no futuro.

Um comportamento parece uma função, mas em vez de ser introduzido com a palavra-chave fun, ele é introduzido com a palavra-chave be. Como uma função, um comportamento pode ter parâmetros. Ao contrário de uma função, ela não tem uma capacidade de receptor (um comportamento pode ser chamado em um receptor de qualquer capacidade) e não se pode especificar um tipo de retorno.

Então, o que retorna um comportamento? Comportamentos sempre retornam None, como uma função sem tipo de resultado explícito, porque eles não podem retornar algo que eles calculam (já que ainda não rodaram).

actor Aardvark
let name: String
var _hunger_level: U64 = 0
new create(name': String) =>
name = name'
be eat(amount: U64) =>
_hunger_level = _hunger_level - amount.min(_hunger_level)

Aqui temos um Aardvark que pode comer de forma assíncrona. Um Aardvark inteligente.

Passagem de mensagens

Se você está familiarizado com linguagens baseadas em atores como Erlang, você está familiarizado com o conceito de "passagem de mensagens". É a forma como os atores se comunicam uns com os outros. Comportamentos são o equivalente ao Pony. Quando você chama um comportamento em um ator, você está enviando uma mensagem a ele.

Se você não está familiarizado com a passagem de mensagens, não se preocupe com isso. Tudo será explicado abaixo.

Concorrente

Uma vez que os comportamentos são assíncronos, não há problema em executar o corpo de um conjunto de comportamentos ao mesmo tempo. Isto é exatamente o que o Pony faz. O Pony tem seu próprio programador cooperativo, que por padrão tem um número de fios igual ao número de núcleos de CPU em sua máquina. Cada linha do agendador pode estar executando um comportamento de ator a qualquer momento, portanto, os programas Pony são naturalmente concorrentes.

Sequencial

Os próprios atores, entretanto, são seqüenciais. Ou seja, cada ator executará apenas um comportamento de cada vez. Isto significa que todo o código em um ator pode ser escrito sem se importar com a concorrência: não há necessidade de fechaduras ou semáforos ou qualquer coisa do gênero.

Quando se escreve o código Pony, é bom pensar nos atores não como uma unidade de paralelismo, mas como uma unidade de seqüencialidade. Ou seja, um ator deve fazer apenas o que tem que ser feito seqüencialmente. Qualquer outra coisa pode ser quebrada em outro ator, tornando-o automaticamente paralelo.

No exemplo abaixo, o ator principal chama um comportamento call_me_later que, como sabemos, é assíncrono, por isso não vamos esperar que ele execute antes de continuar. Em seguida, executamos o método env.out.print, que também é assíncrono, e imprimimos o texto fornecido para o terminal. Agora que terminamos de executar o código dentro do ator principal, o comportamento que chamamos anteriormente será eventualmente executado, e ele imprimirá o último texto.

actor Main
new create(env: Env) =>
call_me_later(env)
env.out.print("This is printed first")
be call_me_later(env: Env) =>
env.out.print("This is printed last")

Como todo este código corre dentro do mesmo ator, e as chamadas para o outro comportamento env.out.print também são sequenciais, temos a garantia de que "Isto é impresso primeiro" é sempre impresso antes de "Isto é impresso por último".

Por que isto é seguro?

Por causa das capacidades do sistema do tipo seguro Pony. Já mencionamos brevemente as capacidades de referência quando falamos das capacidades de referência de receptores de funções. A versão curta é que eles são anotações sobre um tipo que torna todo este paralelismo seguro sem nenhuma sobrecarga de tempo de execução.

Cobriremos as capacidades de referência em profundidade mais tarde.

Os atores são leves

Se você já fez programação concorrente antes, você saberá que os fios podem ser pesados. Chaves de contexto podem causar problemas, cada thread precisa de uma pilha (que pode ser muita memória), e você precisa de muitas travas e outros mecanismos para escrever código de segurança thread.

Mas os atores são leves. Realmente leves. O custo extra de um ator, ao contrário de um objeto, é de cerca de 256 bytes de memória. Bytes, não kilobytes! E não há fechaduras e não há interruptores de contexto. Um ator que não está executando não consome outros recursos além dos poucos bytes extras de memória.

É bastante normal escrever um programa Pony que utiliza centenas de milhares de atores.

Encerramento de ator

Como as classes, os atores podem ter encerramentos. A definição de encerramento é a mesma (fun _final()). Todas as garantias e restrições para um encerramento de classe também são válidas para um encerramento de ator. Além disso, um ator não receberá nenhuma outra mensagem depois que seu encerramento for chamado.

Traços e Interfaces

Como outras linguagens orientados a objetos, o Pony tem subtipagem. Ou seja, alguns tipos servem como categorias das quais outros tipos podem ser membros.

Existem dois tipos de subtipos em linguagens de programação: nominal e estrutural. Elas são sutilmente diferentes, e a maioria das linguagens de programação tem apenas uma ou outra. Pony tem ambas!

Subtipagem nominal

Este tipo de subtipagem é chamado nominal porque se trata de nomes.

Se você já fez programação orientada a objetos antes, você pode ter visto muita discussão sobre herança única, herança múltipla, mixins, traços e conceitos similares. Todos estes são exemplos de subtipagem nominal.

A ideia central é que você tem um tipo que declara que tem uma relação com algum tipo de categoria. Em Java, por exemplo, uma classe (um tipo concreto) pode implementar uma interface (um tipo de categoria). Em Java, isto significa que a classe está agora na categoria que a interface representa. O compilador verificará se a classe realmente fornece tudo o que ela precisa.

Características: subtipagem nominal

Pony tem subtipagem nominal, usando traços. Uma característica se parece um pouco com uma classe, mas usa a palavra-chave trait e não pode ter nenhum campo.

trait Named
fun name(): String => "Bob"
class Bob is Named

Aqui, temos uma característica denominada Named que tem um único nome de função que retorna uma String. Ele também fornece uma implementação padrão de nome que retorna a string literal "Bob".

Também temos uma classe Bob que diz que é Named. Isto significa que Bob está na categoria Named. Em Pony, dizemos que Bob fornece Named, ou às vezes simplesmente Bob é Named.

Como Bob não tem sua própria função de nome, ele usa a função do trait. Se a função do trait não tivesse uma implementação padrão, o compilador reclamaria que Bob não tinha implementação do nome.

trait Named
fun name(): String => "Bob"
trait Bald
fun hair(): Bool => false
class Bob is (Named & Bald)

É possível que uma classe tenha relações com múltiplas categorias. No exemplo acima, a classe Bob fornece tanto a categoria Named como a categoria Bald.

trait Named
fun name(): String => "Bob"
trait Bald is Named
fun hair(): Bool => false
class Bob is Bald

Também é possível combinar categorias em conjunto. No exemplo acima, todas as classes de Bald são automaticamente nomeadas. Consequentemente, a classe Bob tem acesso tanto à implementação padrão do hair() quanto ao name() de suas respectivas características. Pode-se pensar na categoria Bald para ser mais específica do que a categoria Nomeada.

class Larry
fun name(): String => "Larry"

Aqui, temos uma classe Larry que tem uma função de nome com a mesma assinatura. Mas Larry não fornece Named!

Espere, por que não? Porque Larry não diz que é Named. Lembre-se, os traços são nominais: um tipo que quer fornecer um traço tem que declarar explicitamente que o faz. E Larry não faz.

Subtipagem estrutural

Há outro tipo de subtipagem, onde o nome não importa. É chamado de subtipagem estrutural, o que significa que se trata de como um tipo é construído, e nada a ver com nomes.

Um tipo concreto é um membro de uma categoria estrutural se por acaso tiver todos os elementos necessários, não importa o nome que lhe seja dado.

Se você tiver usado Go, você reconhecerá que as interfaces Go são tipos estruturais.

Interfaces: subtipagem estrutural

O Pony também tem subtipagem estrutural, utilizando interfaces. As interfaces parecem traits, mas usam a palavra-chave interface.

interface HasName
fun name(): String

Aqui, HasName se parece muito com Named, exceto por ser uma interface em vez de uma característica. Isto significa que tanto Bob como Larry fornecem HasName! Os programadores que escreveram Bob e Larry nem precisam estar cientes de que HasName existe. As interfaces Pony também podem ter funções com implementações padrão. Um tipo só as pegará se declarar explicitamente que é essa interface.

Devo usar traits ou interfaces em meu código? Ambos! As interfaces são mais flexíveis, portanto, se você não tiver certeza do que deseja, use uma interface. Mas as características também são uma ferramenta poderosa: elas param a subtipagem acidental.