Objetos literais
Às vezes é realmente conveniente ser capaz de escrever um objeto inteiro em linha. No Pony, isto é chamado de objeto literal, e faz exatamente o que um objeto literal em JavaScript faz: ele cria um objeto que você pode usar imediatamente.
Mas o Pony é datilografado estaticamente, então um objeto literal também cria um tipo anônimo que o objeto literalmente preenche. Isto é semelhante às classes anônimas em Java e C#. No Pony, um tipo anônimo pode fornecer qualquer número de traços e interfaces.
Como é isto, então?
Basicamente se parece com qualquer outro tipo de definição, mas com algumas pequenas diferenças. Aqui está uma definição simples:
object
fun apply(): String => "hi"
end
Ok, isso é bem trivial. Vamos estendê-lo para que ele forneça explicitamente uma interface para que o compilador se certifique de que o tipo anônimo preenche essa interface. Você pode usar a mesma notação para fornecer traços também.
object is Hashable
fun apply(): String => "hi"
fun hash(): USize => this().hash()
end
O que não podemos fazer é especificar construtores em um objeto literal, porque o literal é o construtor. Então, como atribuímos aos campos? Bem, nós apenas atribuímos a eles. Por exemplo:
use "collections"
class Foo
fun foo(str: String): Hashable =>
object is Hashable
let s: String = str
fun apply(): String => s
fun hash(): USize => s.hash()
end
Quando atribuímos a um campo no construtor, estamos capturando a partir do escopo léxico em que o objeto literal está. Coisas bem divertidas! Ele nos permite ter fechamentos arbitrariamente complexos que podem até mesmo ter múltiplos pontos de entrada (ou seja, funções que você pode chamar em um fechamento).
Um objeto literal com campos é retornado como um ref por padrão, a menos que uma capacidade de referência explícita seja declarada especificando a capacidade após a palavra-chave do objeto. Por exemplo, um objeto com referências capturadas enviáveis pode ser declarado como iso, se necessário:
use "collections"
class Foo
fun foo(str: String): Hashable iso^ =>
object iso is Hashable
let s: String = str
fun apply(): String => s
fun hash(): USize => s.hash()
end
Podemos também capturar implicitamente valores do escopo léxico, usando-os no objeto literalmente. Às vezes, os valores que não são variáveis locais, não são campos e não são parâmetros de uma função são chamados de variáveis livres. Ao usá-las em uma função, estamos nos fechando sobre elas - ou seja, capturando-as. O código acima poderia ser escrito sem os campos s:
use "collections"
class Foo
fun foo(str: String): Hashable iso^ =>
object iso is Hashable
fun apply(): String => str
fun hash(): USize => str.hash()
end
Lambdas
Os fechamentos arbitrariamente complexos são agradáveis, mas às vezes queremos apenas um simples fechamento. No Pony, você pode usar os lambdas para isso. Uma lambda é escrita como uma função (implicitamente nomeada aplica-se) delimitada entre parênteses curvos:
{(s: String): String => "lambda: " + s }
Isto produz o mesmo código que:
object
fun apply(s: String): String => "lambda: " + s
end
A capacidade de referência do objeto lambda pode ser declarada anexando-o após a chave de fechamento:
{(s: String): String => "lambda: " + s } isso
Isto produz o mesmo código que:
object iso
fun apply(s: String): String => "lambda: " + s
end
O Lambdas pode ser usado para capturar a partir do escopo léxico da mesma forma que os objetos literais podem atribuir a partir do escopo léxico a um campo. Isto é feito adicionando uma segunda lista de argumentos após os parâmetros:
class Foo
new create(env:Env) =>
foo({(s: String)(env) => env.out.print(s) })
fun foo(f: {(String)}) =>
f("Hello World")
Também é possível utilizar uma lista de captura para criar novos nomes para as coisas. Uma lista de captura é uma segunda lista entre parênteses, após os parâmetros:
new create(env:Env) =>
foo({(s: String)(myenv = env) => myenv.out.print(s) })
O tipo de lambda também é declarado usando colchetes. Dentro dos parênteses, os tipos de parâmetros de função são especificados entre parênteses, seguidos por um tipo opcional de dois pontos e retorno. O exemplo acima usa {(String)} para ser o tipo de uma função lambda que toma uma String como argumento e não retorna nada.
Se o objeto lambda não for declarado com uma capacidade de referência específica, a capacidade de referência é inferida a partir da estrutura da lambda. Se o lambda não tiver nenhuma referência capturada, ele será val por padrão; se ele tiver referências capturadas, ele será ref por padrão. O seguinte é um exemplo de um objeto val lambda:
use "collections"
actor Main
new create(env:Env) =>
let l = List[U32]
l.>push(10).>push(20).>push(30).push(40)
let r = reduce(l, 0, {(a:U32, b:U32): U32 => a + b })
env.out.print("Result: " + r.string())
fun reduce(l: List[U32], acc: U32, f: {(U32, U32): U32} val): U32 =>
try
let acc' = f(acc, l.shift()?)
reduce(l, acc', f)
else
acc
end
O método de redução neste exemplo requer o tipo lambda para que o parâmetro f exija uma capacidade de referência de val. O objeto lambda passado como argumento não precisa declarar uma capacidade de referência explícita porque val é o padrão para um lambda que não captura nada.
Como mencionado anteriormente, o lambda desugars para um objeto literalmente com um método de aplicação. A capacidade de referência para o método apply é o padrão para o box como qualquer outro método.
Em uma lambda que captura referências, isto precisa ser ref se a função precisar modificar qualquer uma das variáveis capturadas ou chamar métodos ref sobre elas. A capacidade de referência para o método (versus a capacidade de referência para o objeto que foi descrita acima) é definida colocando a capacidade antes da lista de argumentos entre parênteses.
use "collections"
actor Main
new create(env:Env) =>
let l = List[String]
l.>push("hello").push("world")
var count = U32(0)
for_each(l, {ref(s:String) =>
env.out.print(s)
count = count + 1
})
// Displays '0' as the count
env.out.print("Count: " + count.string())
fun for_each(l: List[String], f: {ref(String)} ref) =>
try
f(l.shift()?)
for_each(l, f)
end
Este exemplo declara o tipo de função aplicável que é gerada pela expressão lambda como sendo ref. A declaração do tipo lambda para o parâmetro f no método for_each também a declara como sendo ref. A capacidade de referência do tipo lambda também deve ser ref para que o método possa ser chamado. O objeto lambda não precisa declarar uma capacidade de referência explícita porque a ref é o padrão para uma lambda que tenha capturas.
O exemplo acima também observa uma realidade sutil de referências capturadas. À primeira vista, pode-se esperar que a contagem tenha sido aumentada pela aplicação de f. Entretanto, a reatribuição de uma referência, contagem = contagem + 1, dentro de uma lambda ou objeto literal nunca pode causar uma reatribuição no escopo externo.
Se a contagem fosse um objeto com capacidades de referência que permitissem mutação, a referência capturada poderia ser modificada com, por exemplo, count.incremental(). A mutação resultante seria visível em qualquer local que tivesse uma referência ao mesmo objeto que a contagem.
Atores Literais
Normalmente, um objeto literal é uma instância de uma classe anônima. Para torná-la uma instância de um ator anônimo, basta incluir um ou mais comportamentos na definição literal do objeto.
object
be apply() => env.out.print("hi")
end
Um ator literal é sempre devolvido como uma etiqueta.
Literais primitivos
Quando um tipo anônimo não tem campos e nenhum comportamento (como, por exemplo, um objeto declarado literalmente como lambda literal), o compilador o gera como um primitivo anônimo, a menos que uma capacidade de referência não-valorizada seja explicitamente dada. Isto significa que nenhuma alocação de memória é necessária para gerar uma instância desse tipo.
Em outras palavras, no Pony, um lambda que não fecha sobre nada não tem nenhuma alocação de memória. Legal. Um literal primitivo é sempre retornado como um val.