Ballerina - Uma linguagem de programação orientada a dados
Pontos-chave
- O sistema de tipagem flexível da Ballerina traz o melhor das linguagens de tipagem estática e dinamicamente em termos de segurança, clareza e velocidade de desenvolvimento.
- Ballerina trata os dados como cidadãos de primeira classe que podem ser criados sem cerimônia adicional, assim como strings e números.
- Ballerina oferece uma linguagem de consulta rica que permite aos desenvolvedores expressar eloquentemente a lógica de negócios e manipulação de dados.
- Nos registros da Ballerina, os campos podem ser obrigatórios ou opcionais.
- Bailarina suporta JSON fora da caixa.
Nos sistemas de informação que criei na última década, os dados são trocados entre programas como aplicativos frontend, servidores back-end e trabalhadores de serviços. Esses programas usam formatos de intercâmbio, como JSON, para se comunicar pela rede.
Ao longo dos anos, notei que a complexidade de um programa depende não apenas da complexidade das necessidades de negócios, mas também da abordagem que adoto para representar dados em meus programas.
Em linguagens tipadas estaticamente (como Java, C#, Go, OCaml ou Haskell), parece natural representar dados com tipos ou classes personalizadas, enquanto em linguagens tipadas dinamicamente (como JavaScript, Ruby, Python ou Clojure), geralmente usamos estruturas de dados genéricas, como mapas e tabelas.
Cada abordagem tem seus benefícios e custos. Quando representamos dados com tipos estáticos, obtemos grande suporte de nosso IDE e da segurança de nosso sistema de tipos, mas isso torna o código mais detalhado e o modelo de dados mais rígido.
Por outro lado, em linguagens tipadas dinamicamente, representamos dados com mapas flexíveis. Isso nos permite criar rapidamente códigos de pequeno a médio porte sem qualquer tipo de cerimônia, mas operamos na natureza. Nosso IDE não nos ajuda a completar automaticamente nomes de campos e, quando digitamos nomes de campos incorretamente, obtemos erros de tempo de execução.
A abordagem refrescante da Ballerina para os tipos
Até descobrir a Ballerina, eu achava que o compromisso era parte integrante da programação com a qual éramos forçados a conviver. Mas eu estava errado: é possível combinar o melhor dos dois mundos. É possível mover-se rapidamente sem comprometer a segurança e a clareza. É possível beneficiar de um sistema de tipo flexível.
Não tenho condições de andar, é muito lento.
Tenho medo de correr, é muito arriscado.
Quero me movimentar com facilidade e confiança. Como uma bailarina.
Dados como cidadão de primeira classe
Quando escrevemos um programa que manipula dados, é melhor tratar os dados como um cidadão de primeira linha. Um dos privilégios dos cidadãos de primeira classe é que eles podem ser criados sem cerimônia adicional, assim como números e strings.
Infelizmente, em linguagens tipadas estaticamente, os dados geralmente não têm o privilégio de serem criados sem cerimônia. Você deve usar um construtor nomeado para criar dados. Quando os dados não estão aninhados, a falta de literais de dados não é muito problemática, como ao criar um membro da biblioteca de 17 anos chamado Kelly Kapowski.
Member kelly = new Member(
"Kelly",
"Kapowski",
17
);
Mas com dados aninhados, o uso de um construtor nomeado torna-se detalhado. Veja como é a criação de dados quando incluímos a lista de livros que Kelly possui atualmente, assumindo um modelo de dados de biblioteca simplista, onde um livro tem apenas um título e um autor.
Member kelly = new Member(
"Kelly",
"Kapowski",
17,
List.of(
new Book(
"The Volleyball Handbook",
new Author("Bob", "Miller")
)
)
);
Em linguagens tipadas dinamicamente, como JavaScript, o uso de literais de dados torna muito mais natural a criação de dados aninhados.
var kelly = {
firstName: "Kelly",
lastName: "Kapowski",
age: 17,
books: [
{
title: "The Volleyball Handbook",
author: {
firstName: "Bob",
lastName: "Miller"
}
}
]
};
O problema com a abordagem de dados de linguagens tipadas dinamicamente é que os dados são selvagens. A única coisa que você sabe sobre seus dados é que eles são um mapa aninhado. Portanto, você deve confiar na documentação para saber que tipo de dados você tem em mãos.
A primeira coisa que gostei em Ballerina foi que ele me deu a capacidade de criar meus tipos personalizados, mantendo a conveniência de criar dados por meio de literais de dados.
Com Ballerina, assim como em uma linguagem tipada estaticamente, criamos nossos tipos de registro personalizados para representar nosso modelo de dados. Veja como criamos os tipos de registro Autor, Livro e Membro:
type Author record {
string firstName;
string lastName;
};
type Book record {
string title;
Author author;
};
type Member record {
string firstName;
string lastName;
int age;
Book[] books;
};
E com Ballerina, como em linguagens tipadas dinamicamente, criamos dados com literais de dados.
Member kelly = {
firstName: "Kelly",
lastName: "Kapowski",
age: 17,
books: [
{
title: "The Volleyball Handbook",
author: {
firstName: "Bob",
lastName: "Miller"
}
}
]
};
É claro que, como em uma linguagem tradicional digitada estaticamente, o sistema de digitação nos permite saber quando perdemos um campo em um registro. Nosso código não será compilado e o compilador nos dirá exatamente o porquê.
Author yehonathan = {
firstName: "Yehonathan"
};
ERROR [...] missing non-defaultable required record field 'lastName'
No VSCode, quando a extensão Ballerina é instalada, você é notificado do campo ausente enquanto digita.
Agora, você provavelmente está se perguntando se o sistema de tipos da Ballerina é estático ou dinâmico. Vamos dar uma olhada mais de perto.
Sistema de tipos flexíveis da Ballerina
Em um programa orientado a dados, enriquecer dados com campos computados é bastante comum. Por exemplo, suponha que eu queira enriquecer os dados de um autor com um campo chamado fullName que contenha o nome completo do autor.
Em uma linguagem tradicional tipada estaticamente, eu precisaria criar um novo tipo para esses dados avançados, talvez um novo tipo chamado EnrichedAuthor. Em Ballerina, não é obrigatório; O sistema de tipos permite que você adicione campos ao registro em tempo real, usando notação entre colchetes, assim como em uma linguagem digitada dinamicamente. Por exemplo, veja como adicionamos um campo fullName a um registro Author:
Author yehonathan = {
firstName: "Yehonathan",
lastName: "Sharvit"
};
yehonathan["fullName"] = "Yehonathan Sharvit";
Acho essa habilidade bastante surpreendente. De certa forma, Ballerina permite que nós desenvolvedores tenhamos nosso bolo e o comamos também, introduzindo elegantemente uma diferença semântica entre duas notações diferentes:
-
Quando usamos a notação de pontos para acessar ou editar um campo em um registro, a Ballerina nos dá a mesma segurança e ajuda a que estamos acostumados em idiomas digitados estaticamente.
-
Quando usamos a notação entre colchetes para acessar ou editar um campo em um registro, Ballerina nos dá a mesma flexibilidade que temos em linguagens digitadas dinamicamente.
Em alguns casos, queremos ser mais rigorosos e proibir completamente a adição de campos. Não tem problema: bailarina suporta discos fechados. A sintaxe para registros fechados é semelhante à sintaxe para registros abertos, exceto que a lista de campos é cercada por dois caracteres | . |
type ClosedAuthor record {|
string firstName;
string lastName;
|};
ClosedAuthor yehonathan = {
firstName: "Yehonathan",
lastName: "Sharvit"
};
O sistema de tipos não permite adicionar um campo a um registro fechado.
yehonathan["fullName"] = "Yehonathan Sharvit";
ERROR [...] undefined field 'fullName' in 'ClosedAuthor'
Ballerina também suporta campos opcionais em registros usando o ponto de interrogação. No registro abaixo, o primeiro nome do autor é opcional.
type AuthorWithOptionalFirstName record {
string firstName?;
string lastName;
};
Ao acessar um campo opcional em um registro, você precisa se certificar de lidar com o caso em que o campo não está presente. Em linguagens tradicionais digitadas dinamicamente, a falta de um verificador de tipo estático torna muito fácil esquecer de lidar com esse caso. Tony Hoare introduziu referências nulas em 1965 em uma linguagem de programação chamada ALGOL, e mais tarde ele considerou isso um erro de bilhões de dólares.
Em Ballerina, o sistema de tipos está lá para você. Digamos que você queira escrever uma função que coloque o primeiro nome de um autor em maiúsculas.
function upperCaseFirstName(AuthorWithOptionalFirstName author) {
author.firstName = author.firstName.toUpperAscii();
}
Este código não será compilado: o sistema de tipos (e a extensão Ballerina VSCode) irá lembrá-lo de que não há garantia de que o campo opcional está lá.
ERROR [...] undefined function 'toUpperAscii' in type 'string?'
Então, como corrigimos nosso código para lidar adequadamente com a ausência do campo opcional? É bem simples; Após acessar o campo opcional, você verifica se ele está lá ou não. Em Ballerina, a ausência de um campo é representada por ().
function upperCaseFirstName(AuthorWithOptionalFirstName author) {
string? firstName = author.firstName;
if (firstName is ()) {
return;
}
author.firstName = firstName.toUpperAscii();
}
Observe que nenhuma conversão de tipo é necessária. O sistema de tipos é inteligente o suficiente para entender que a variável firstName é garantida como uma cadeia de caracteres depois de verificar que firstName não é ().
Outro aspecto do sistema semelhante ao Ballerina que considero muito útil, no contexto da programação orientada por dados, é que os tipos de registro só são definidos através da estrutura de seus campos. Permitam-me que esclareça.
Quando escrevemos um programa que manipula dados, a maior parte da nossa base de código é composta por funções que recebem dados e retornam dados. Cada função tem requisitos em relação à forma dos dados que recebe.
Em idiomas tipados estaticamente, esses requisitos são expressos como tipos ou classes. Ao examinar uma assinatura de função, você sabe exatamente qual é a forma dos dados para os argumentos de função. O problema é que isso às vezes cria um acoplamento estreito entre código e dados.
Vou dar um exemplo. Suponha que você quisesse escrever uma função que retornasse o nome completo de um autor, você provavelmente escreveria algo assim:
function fullName(Author author) returns string {
return author.firstName + " " + author.lastName;
}
A limitação dessa função é que ela só funciona com registros do tipo Autor. Acho um pouco decepcionante que ele não funcione com registros do tipo membro. Afinal, um membro de registro também tem campos de cadeia de caracteres firstName e lastName.
Como uma observação adicional, algumas linguagens tipadas estaticamente permitem que você supere essa limitação criando interfaces de dados.
Linguagens digitadas dinamicamente são muito mais flexíveis. No JavaScript, por exemplo, você implementará a função assim:
function fullName(author) {
return author.firstName + " " + author.lastName;
}
O argumento function é chamado author, mas, na verdade, ele funciona com qualquer elemento de dados que tenha campos de cadeia de caracteres firstName e lastName. O problema é que, quando você passa dados que não têm um desses campos, você obtém uma exceção de tempo de execução. Além disso, a forma esperada de dados dos argumentos de função não é expressa no código. Então, para saber que tipo de dados a função espera, temos que confiar na documentação (que nem sempre está atualizada) ou investigar o código da função.
O sistema de tipos flexíveis da Ballerina permite que você especifique a forma de seus argumentos de função, sem comprometer a flexibilidade. Você pode criar um novo tipo de registro, que menciona apenas os campos de registro que a função precisa para funcionar corretamente.
type Named record {
string firstName;
string lastName;
};
function fullName(Named a) returns string {
return a.firstName + " " + a.lastName;
}
O sistema de tipos flexíveis da Ballerina permite que você especifique a forma dos argumentos da sua função, sem comprometer a flexibilidade. Você pode criar um novo tipo de registro, que menciona apenas os campos de registro que a função precisa para funcionar corretamente.
type Named record {
string firstName;
string lastName;
};
function fullName(Named a) returns string {
return a.firstName + " " + a.lastName;
}
DICA PRO: Você pode usar um tipo de registro anônimo para especificar a forma dos argumentos da sua função.
function fullName(record {
string firstName;
string lastName;
} a)
returns string {
return a.firstName + " " + a.lastName;
}
Você é livre para chamar sua função com qualquer registro que contenha os campos obrigatórios, seja um membro ou um autor, ou qualquer outro registro que contenha os dois campos de cadeia de caracteres esperados pela função.
Member kelly = {
firstName: "Kelly",
lastName: "Kapowski",
age: 17,
books: [
{
title: "The Volleyball Handbook",
author: {
firstName: "Bob",
lastName: "Miller",
fullName: "Bob Miller"
}
}
]
};
fullName(kelly);
// "Kelly Kapowski"
fullName(kelly.books[0].author);
// "Bob Miller"
Aqui está uma analogia que acho útil para ilustrar a abordagem de Ballerina aos tipos: os tipos são como óculos que usamos em nossos programas para olhar a realidade. Mas precisamos lembrar que o que vemos através de nossas lentes é apenas um aspecto da realidade. Essa não é a realidade em si. Como diz o ditado: o mapa não é o território.
Por exemplo, não é correto dizer que a função fullName – definida acima – recebe é um registro Named . É mais preciso dizer que a função fullName decide olhar para os dados que recebe através das lentes de um registro nomeado.
Vamos a outro exemplo. Em Ballerina, dois registros de tipos diferentes que têm exatamente os mesmos valores de campo são considerados iguais.
Author yehonathan = {
firstName: "Yehonathan",
lastName: "Sharvit"
};
AuthorWithBooks sharvit = {
firstName: "Yehonathan",
lastName: "Sharvit"
};
yehonathan == sharvit;
// true
No início, esse comportamento me surpreendeu. Como dois registros de tipos diferentes poderiam ser considerados iguais? Mas quando pensei na analogia dos óculos, fez sentido para mim:
os dois tipos são duas lentes diferentes que olham para a mesma realidade. Em nossos programas, o que mais importa é a realidade, não os objetivos. Às vezes, as linguagens tradicionais tipadas estaticamente parecem enfatizar mais as lentes do que a realidade.
Até agora, vimos como a Ballerina aproveita os tipos para que eles não atrapalhem, mas nos ajudem a tornar nosso fluxo de trabalho de desenvolvimento mais eficiente. Ballerina vai um passo além e nos permite manipular dados de forma poderosa e conveniente através de uma linguagem de consulta expressiva.
O poder de uma linguagem de consulta expressiva
Como proponente da programação funcional, meus comandos “pão com manteiga” quando preciso manipular dados consistem em funções de ordem superior, como mapear, filtrar e reduzir. Ballerina suporta programação funcional, mas a maneira idiomática de lidar com a manipulação de dados em Ballerina é através de sua linguagem de consulta expressiva, que nos permite expressar a lógica de negócios de forma eloquente.
Suponhamos que tenhamos uma coleção de registros e queiramos manter apenas registros que satisfaçam uma determinada condição e enriqueçam esses registros com um campo computado. Por exemplo, digamos que queremos apenas manter livros com a palavra “Voleibol” no título, e enriquecê-los com o nome completo do autor.
Esta é a função que enriquece o registro do Autor dentro de um Livro.
function enrichAuthor(Book book) returns Book {
book.author["fullName"] = fullName(book.author);
return book;
}
Poderíamos usar mapa e filtro para enriquecer nossa coleção de livros, usando mapa, filtro e algumas funções anônimas.
function enrichBooks(Book[] books) returns Book[] {
return books.filter(function(Book book) returns boolean {
return book.title.includes("Volleyball");
}).
map(function(Book book) returns Book {
return enrichAuthor(book);
});
}
Mas é bastante prolixo e um pouco irritante declarar os tipos das duas funções anônimas. Ao usar a linguagem de consulta Ballerina, o código é mais compacto e fácil de ler.
function enrichBooks(Book[] books) returns Book[] {
return from var book in books
where book.title.includes("Volleyball")
select enrichAuthor(book);
}
A linguagem de consulta Ballerina será abordada com mais detalhes em nossa série Ballerina.
Antes de falarmos sobre JSON, vamos escrever um pequeno teste de unidade para nossa função. Em Ballerina, os registros são considerados iguais quando têm os mesmos campos e valores. Isso facilita a comparação dos dados retornados por uma função com os dados esperados.
Book bookWithVolleyball = {
title: "The Volleyball Handbook",
author: {
firstName: "Bob",
lastName: "Miller"
}
};
Book bookWithoutVolleyball = {
title: "Friendship Bread",
author: {
firstName: "Darien",
lastName: "Gee"
}
};
Book[] books = [bookWithVolleyball, bookWithoutVolleyball];
Book[] expectedResult = [
{
title: "The Volleyball Handbook",
author: {
firstName: "Bob",
lastName: "Miller",
fullName: "Bob Miller"
}
}
];
enrichBooks(books) == expectedResult;
// true
DICA PRO: Ballerina vem com uma estrutura de teste de unidade pronta para uso.
Agora que vimos a flexibilidade e facilidade que a Ballerina oferece em torno da representação e manipulação de dados em um programa, vamos ver como a Ballerina nos permite trocar dados com outros programas.
Suporte JSON pronto para uso
JSON é provavelmente o formato mais popular para troca de dados. Muitas vezes, os programas envolvidos em sistemas de informação se comunicam enviando cadeias de caracteres JSON uns para os outros. Quando um programa precisa enviar dados pela rede, ele serializa uma estrutura de dados em uma cadeia de caracteres JSON. E quando um programa recebe uma cadeia de caracteres JSON, ele precisa analisá-la para convertê-la em uma estrutura de dados.
Ballerina, sendo uma linguagem projetada para a era da nuvem, suporta serialização JSON e análise JSON pronta para uso. Qualquer registro pode ser serializado em uma cadeia de caracteres JSON, conforme mostrado aqui:
AuthorWithBooks yehonathan = {
firstName: "Yehonathan",
lastName: "Sharvit",
numOfBooks: 1
};
yehonathan.toJsonString();
// {"firstName":"Yehonathan", "lastName":"Sharvit", "numOfBooks":1}
Por outro lado, uma cadeia de caracteres JSON pode ser analisada em um registro. Aqui, precisamos ter cuidado e garantir que lidamos com casos em que a cadeia de caracteres JSON não é uma cadeia de caracteres JSON válida ou não está em conformidade com a forma de dados esperada.
function helloAuthor(string authorStr) returns error? {
Author|error author = authorStr.fromJsonStringWithType();
if (author is error) {
return author;
} else {
io:println("Hello, ", author.firstName, "!");
}
}
DICA PRO: A bailarina aceita erros e nos permite escrever sucintamente a mesma lógica de uma forma mais compacta através de uma construção de verificação especial.
function helloAuthor(string authorStr) returns error? {
Author author = check authorStr.fromJsonStringWithType();
io:println("Hello, ", author.firstName, "!");
}
Nota: O suporte a JSON em Ballerina vai muito além da serialização e análise. Na verdade, Ballerina vem com um tipo json que permite manipular dados exatamente como em uma linguagem dinâmica. JSON avançado em Ballerina será abordado mais tarde em nossa série sobre Ballerina.
Exploramos os benefícios que a Bailarina oferece quando se trata de representação, manipulação e comunicação de dados. Concluiremos nossa exploração com um exemplo de um miniprograma orientado por dados que ilustra esses benefícios.
Um último exemplo: manipular dados com facilidade e confiança
Imagine construir um sistema de gerenciamento de bibliotecas composto por vários programas que trocam dados sobre membros, livros e autores. Um dos programas é obrigado a processar os dados dos membros, enriquecendo-os com campos calculados com o nome completo do membro, mantendo apenas livros cujos títulos contenham “Voleibol” e adicionando o nome completo do autor a cada livro.
O programa se comunica pela rede usando JSON: ele recebe dados de membros no formato JSON e deve retorná-los no formato JSON.
Aqui está o que o código para este programa seria com Ballerina.
Primeiro, criamos nossos tipos de registro personalizados.
type Author record {
string firstName;
string lastName;
};
type Book record {
string title;
Author author;
};
type Member record {
string firstName;
string lastName;
int age;
Book[] books?; // books is an optional field
};
Em seguida, uma pequena função de utilitário que calcula o nome de exibição de qualquer registro que contenha campos de cadeia de caracteres firstName e lastName. Expressamos essa restrição usando um registro anônimo.
function fullName(record {
string firstName;
string lastName;
} a)
returns string {
return a.firstName + " " + a.lastName;
}
Usamos a linguagem de consulta Bailarina para filtrar e enriquecer livros:
- Mantenha apenas livros com “Voleibol” no título
- Enriqueça cada livro com o nome completo do autor
function enrichAuthor(Author author) returns Author {
author["fullName"] = fullName(author);
return author;
}
function enrichBooks(Book[] books) returns Book[] {
return from var {author, title} in books
where title.includes("Volleyball") // filter books whose title include Volleyball
let Author enrichedAuthor = enrichAuthor(author) // enrich the author field
select {author: enrichedAuthor, title: title}; // select some fields
Agora, escrevemos nossa lógica de negócios: uma função que enriquece um Membro Record com:
- Nome completo do membro
- Livros filtrados e enriquecidos
function enrichMember(Member member) returns Member {
member["fullName"] = fullName(member); // fullName works on member and authors
Book[]? books = member.books; // books is an optional field,
if (books is ()) { // handle explicitly the case where the field is not present
return member;
}
// the type system is smart enough to understand that here books is guaranteed to be an array
member.books = enrichBooks(books);
return member;
}
Finalmente, escrevemos o ponto de entrada para o programa que faz o seguinte:
- Analisar entrada JSON em um registro de membro
- Chame a função que processa a lógica de negócios para obter um registro do tipo Membro Avançado
- Serializar o resultado para JSON
Observe que temos que lidar com o fato de que a cadeia de caracteres JSON que recebemos é inválida. Veja como fazer isso:
- Declaramos que o valor de retorno pode ser uma cadeia de caracteres ou um erro.
- Chamamos de verificação sobre o que é retornado por fromJsonStringWithType. Ballerina propaga automaticamente um erro, caso a string JSON que recebemos seja inválida.
function entryPoint(string memberJSON) returns string|error {
Member member = check memberJSON.fromJsonStringWithType();
var enrichedMember = enrichMember(member);
return enrichedMember.toJsonString();
}
É isso para o código que lida com a lógica em si. Você pode encontrar o código completo no GitHub.
A fim de torná-lo uma aplicação real, eu usaria um dos muitos protocolos prontos que Ballerina fornece para se comunicar através da rede, como HTTP, GraphQL, Kafka, gRPC, WebSockets, etc.
Conclusão
Enquanto trabalhava nos trechos de código apresentados neste artigo, senti que estava revivendo a sensação agradável que meu IDE me deu ao trabalhar em linguagens digitadas estaticamente. Fiquei surpreso ao descobrir que, para desfrutar dessa experiência, desta vez não precisei abrir mão do poder expressivo e da flexibilidade em que me tornei viciado desde que comecei a trabalhar com linguagens digitadas dinamicamente.
A principal coisa que sinto falta em Ballerina é a capacidade de atualizar dados sem mutá-los, como estou acostumado na programação funcional. Não consegui implementar esse recurso como uma função personalizada no Ballerina, pois requer suporte para lidar com tipos genéricos. Mas espero que, num futuro próximo, essa capacidade seja adicionada à linguagem.
Vejo a Ballerina como uma linguagem de programação de uso geral, cuja abordagem de dados a torna uma excelente escolha para a construção de sistemas de informação. Na minha opinião, isso se deve aos valores fundamentais da Ballerina em relação à representação de dados, manipulação de dados e comunicação de dados.
- Trata os dados como um cidadão de primeira classe
- Seu sistema de tipagem flexível oferece mais flexibilidade do que as linguagens de tipagem estáticas tradicionais, sem comprometer a segurança e as ferramentas
- Seu sistema de tipagem flexível oferece mais ferramentas e segurança do que linguagens tipadas dinamicamente, sem comprometer a velocidade e o poder de expressão
- Possui uma linguagem de consulta expressiva para manipulação de dados
- Ele suporta JSON pronto para uso para trocar dados pela rede
Você pode descobrir mais sobre Ballerina visitando ballerina.io.
Nos próximos artigos de nossa série sobre Ballerina, abordaremos aspectos adicionais da Ballerina, como tabelas, consultas avançadas, tratamento de erros, mapas, tipo json, conectores e muito mais… Você pode se inscrever em nossa newsletter para ser avisado quando o próximo artigo da série sobre Bailarina for publicado.
Sobre o autor
Yehonathan Sharvit
Yehonathan Sharvit é autor de Data-Oriented Programming. Escreve código desde 2001, em linguagens como C++, Java, JavaScript, Ruby, Python, Clojure e Ballerina. Ele está atualmente trabalhando para a CyCognito como um Clojure Wizard. Além de sua contribuição para bibliotecas de software, que são usadas para construir pipelines de dados em escala, seu trabalho é ajudar outros desenvolvedores a escrever código elegante e fácil de manter.