Um tour pela Unison

author:: chicoary
source:: Um tour pela Unison
clipped:: 2024-01-04
published:: agosto 19, 2023

#clippings


View | Edit


Traduzido de A tour of Unison.
Publicado em https://chicoary.wordpress.com/2023/08/19/um-tour-pela-unison/.

Este documento apresenta os fundamentos do uso do Unison codebase manager e da escrita do código Unison. Apresentaremos partes da linguagem principal do Unison e sua sintaxe à medida que avançarmos. A documentação da linguagem Unison é um recurso mais aprofundado se você tiver dúvidas ou quiser saber mais.

Se você quiser acompanhar este documento (altamente recomendado), este guia pressupõe que você já tenha percorrido as etapas do guia de início rápido e dado uma olhada em a grande ideia.

Este documento é um documento computável Unison!. Comentários e melhorias são muito bem-vindos! Saiba como contribuir.

👋 para o gerenciador de base de código do Unison

O Unison Codebase Manager, ou UCM, é a ferramenta de linha de comando que executa a linguagem de programação Unison e permite que você interaja com o código Unison que você escreveu e salvou. Em outras palavras, o UCM é a interface para sua base de código do Unison.

💡 Lembre-se: O código do Unison não é salvo como conteúdo de arquivo baseado em texto. Por esse motivo, precisamos de uma ferramenta que nos permita alterar e executar programas Unison.

Suas diversas responsabilidades incluem:

🎉 Executando o UCM

Por padrão, a execução do ucm em um diretório interagirá com qualquer arquivo com sufixo .u no diretório em que o comando foi emitido ao abrir a base de código padrão em seu diretório pessoal. Você receberá uma mensagem no UCM do tipo:

O que está acontecendo aqui? Este é o Unison Codebase Manager iniciando e configurando uma nova base de código. Estamos acostumados a pensar em nossa base de código como um saco de arquivos de texto que sofre mutação à medida que fazemos alterações em nosso código, mas no Unison a base de código é representada como uma coleção de árvores de sintaxe serializadas, identificadas por um hash de seu conteúdo e armazenadas em uma coleção de arquivos dentro de um diretório .unison no caminho que você forneceu ao ucm.

O formato da base de código do Unison tem algumas propriedades importantes:

O local da base de código em que o UCM o deixa, caso você nunca o tenha usado antes, é representado pelo . no prompt, .>. Chamamos isso de “a raiz” de sua base de código.

Em vez de criar várias bases de código para cada aplicativo em que estiver trabalhando, o Unison subdivide a base de código em “projetos”. Apresentaremos o conceito de projetos criando um para este tour e estabelecendo algumas convenções para organizá-lo.

Os projetos Unison são análogos aos repositórios de código-fonte. Eles ajudam a organizar sua base de código em aplicativos, bibliotecas e outros trabalhos nos quais você pode querer colaborar com outras pessoas ou compartilhar. Os projetos são divididos em branches para representar fluxos de trabalho independentes.

Dentro de um projeto, seu código é organizado em namespaces do Unison. Os namespaces são mapeamentos de nomes legíveis por humanos para suas definições. Os nomes no Unison são coisas como: math.sqrtbase.Optional.Somebase.Natbase.Nat.*++ ou foo. Ou seja: um . opcional, seguido de um ou mais segmentos separados por um ., sendo que o último segmento pode ser um nome de operador como * ou ++.

Em geral, pensamos nesses segmentos de nomes como se formassem uma árvore, muito parecida com um diretório de arquivos em que os nomes são como caminhos de arquivos na árvore. É mais comum trabalhar com o prompt do UCM na raiz do projeto, mas você também pode navegar pela árvore de namespaces com o comando namespace (ou equivalentemente cd).

Você pode usar cd .. para voltar um nível na árvore de namespace ou cd . para voltar à raiz de toda a sua base de código.

No console do gerenciador de base de código, crie um projeto tour com o comando project.create. É nele que você adicionará o código durante o restante deste guia.

Observe que o prompt muda para tour/main>, indicando que seu projeto atual agora é tour e sua ramificação atual é /main. Ao editar o código do Unison e interagir com o UCM, os comandos e o código do UCM têm “escopo” para esse projeto e ramificação, a menos que seja indicado de outra forma com um caminho absoluto.

Quando você cria um novo projeto, o UCM instala automaticamente a biblioteca padrão base para você. Ela está localizada em um namespace especial chamado lib.

🧠 O Unison procura dependências de projeto diretamente na raiz do projeto. O diretório lib em nosso projeto tour conterá todas as dependências necessárias para executar o código no projeto.

Vamos explorar a biblioteca base que acabou de ser baixada e nos acostumar a navegar em uma base de código Unison.

Você pode visualizar os termos e tipos em um namespace com o comando ucm ls.

O resultado deve ser uma lista numerada de definições e suas assinaturas associadas.

Devido à natureza append-only do formato da base de código, podemos armazenar em cache todos os tipos de informações interessantes sobre as definições na base de código e nunca precisaremos nos preocupar com a invalidação do cache. Por exemplo, o Unison é uma linguagem estaticamente tipada e sabemos o tipo de todas as definições na base de código, portanto, uma coisa útil e fácil de manter é um índice que nos permite pesquisar definições na base de código por seu tipo. Experimente os dois comandos find a seguir (a nova sintaxe é explicada abaixo):

O comando find aqui está procurando por definições cujos nomes incluam reverse. Ele procura primeiro em nosso próprio código no projeto e, em seguida, nas dependências em lib.

Aqui, fizemos uma pesquisa baseada em tipos, com find seguido de dois pontos, :, para pesquisar funções do tipo [a] -> [a]. Obtivemos uma lista de resultados e, em seguida, usamos o comando view para ver o código-fonte bem formatado de um desses resultados. Vamos apresentar um pouco da sintaxe do Unison:

Os nomes são armazenados separadamente das definições, portanto, a renomeação é rápida e 100% precisa

A base de código do Unison, em sua definição para List.reverse, não armazena nomes para as definições das quais ele depende (como a função List.foldLeft ); ele faz referência a essas definições por meio de seu hash. Como resultado, é fácil alterar o(s) nome(s) associado(s) a uma definição.

Vamos tentar isso. List.reverse é definido usando List. foldLeft. Vamos renomeá-lo para List.foldl para torná-lo mais familiar aos fãs de Haskell. Experimente o seguinte comando (você pode usar tab completion aqui, se quiser):

Observe que view agora mostra o nome foldl, portanto, a renomeação foi efetivada. Excelente!

Para que isso ocorra, o Unison apenas alterou o nome associado ao hash de List.foldLeft em um único lugar. O comando view procura os nomes dos hashes em tempo real, no momento em que está imprimindo o código.

O Unison não está fazendo um monte de mutações de texto em seu nome, atualizando possivelmente milhares de arquivos, gerando uma enorme diferença textual e também quebrando um monte de usuários de bibliotecas posteriores que ainda esperam que essa definição seja chamada pelo nome antigo. Os dois nomes são, para o Unison, a mesma coisa.

Portanto, renomeie e mude as coisas de lugar o quanto quiser! Não se preocupe em escolher um nome perfeito na primeira vez. Se quiser, dê vários nomes à mesma definição! Dar nomes às coisas já é difícil, renomeá-las não deveria ser uma provação.

O fato de as bases de código do Unison serem imutáveis e apenas anexadas (append-only) significa que podemos “retroceder” nossa base de código para um ponto anterior no tempo. Use o comando reflog para ver um registro das alterações da base de código. Você verá um texto de ajuda e uma lista numerada de hashes.

Reflog mantém o controle do histórico da base de código registrando o hash do espaço de nome raiz de toda a sua base de código. Os hashes do espaço de nomes mudam junto com as atualizações do termo e das definições de tipo que eles contêm. Quando renomeamos List.foldLeft, conceitualmente, o “estado” da base de código mudou, mas o formato baseado em registro do histórico da base de código significa que essas alterações podem ser recuperadas.

Vamos tentar desfazer (undo) a ação de renomeação. Use o comando reset-root para escolher um estado anterior da base de código para o qual retornar. Daremos a ele o hash da base de código imediatamente antes da emissão do comando move.term.

Ótimo! OK, vá beber um pouco de água 🚰 e depois vamos começar a escrever algum código Unison!

Arquivos de trabalho (scratch files) interativos do Unison

O gerenciador de base de código permite que você faça alterações na sua base de código e explore as definições que ela contém, mas ele também detecta (listen) alterações em qualquer arquivo que termine em .u no diretório atual. Quando um desses arquivos é salvo (o que chamamos de “arquivo de trabalho”), o Unison analisa e verifica o tipo desse arquivo. Vamos testar isso.

Mantenha seu terminal ucm em execução e abra um arquivo, scratch.u (ou foo.u, ou o que preferir), no editor de texto de sua preferência (se quiser realce de sintaxe para arquivos Unison, siga este link para obter instruções sobre como configurar seu editor).

Agora, coloque o seguinte em seu arquivo de trabalho (scratch file):

Isso define uma função chamada square. Ela recebe um argumento chamado x e retorna x multiplicado por ele mesmo.

A primeira linha, use base, informa ao Unison que você deseja usar nomes curtos para as bibliotecas base nesse arquivo (o que permite que você diga Nat em vez de ter que dizer base.Nat). O UCM preferirá a instância base encontrada em lib.

Quando você salva o arquivo, o Unison responde com:

Ele verificou o tipo da função square e inferiu que ela recebe um número natural e retorna um número natural, portanto, tem o tipo Nat -> Nat. Ele também nos diz que square está “ok para add“. Faremos isso em breve, mas, primeiro, vamos tentar chamar nossa função diretamente no arquivo scratch.u, apenas iniciando uma linha com >:

O Unison imprime:

Esse 6 | é o número da linha do arquivo. O > square 4 na linha 6 do arquivo, começando com um >, é chamado de ” watch expression”, e o Unison usa essas watch expressions em vez de ter um read-eval-print-loop (REPL) separado. O código que você está editando pode ser executado interativamente à medida que você avança, com um editor de texto completo à sua disposição, com as mesmas definições em todo o escopo, sem a necessidade de alternar para uma ferramenta separada.

use base é uma “wildcard use clause”, que nos permite usar qualquer coisa do namespace base no namespace lib sem qualificação. Por exemplo, referimo-nos a base.Nat como simplesmente Nat.

Dúvida: realmente queremos reavaliar todas as watch expressions em cada salvamento de arquivo? E se elas forem custosas? Felizmente, o Unison mantém um cache de resultados para expressões que ele avalia, com a chave do hash da expressão, e você pode limpar esse cache a qualquer momento sem efeitos negativos. Se um resultado para um hash estiver no cache, o Unison o retornará em vez de avaliar a expressão novamente. Portanto, você pode pensar e usar seus arquivos de trabalho .u um pouco como planilhas, que só recomputam a quantidade mínima quando as dependências mudam.

Há mais um ingrediente que faz com que isso funcione de forma eficaz, que é a programação funcional. Quando uma expressão não tem efeitos colaterais, seu resultado é determinístico e você pode armazená-lo em cache, desde que tenha uma boa chave para usar no cache, como o hash baseado em conteúdo do Unison. O sistema de tipos do Unison não permitirá que você faça I/O dentro de uma dessas expressões de observação ou qualquer outra coisa que faça com que o resultado mude de uma avaliação para outra.

Vamos experimentar mais alguns exemplos:

Testando seu código

Vamos adicionar um teste para nossa função square:

Salve o arquivo, e o Unison retornará com:

Algumas observações sobre a sintaxe:

A função test.check tem a assinatura test.check : Boolean -> [test.Result]. Ela usa uma expressão Boolean e retorna uma lista de resultados de teste, do tipo [base.test.Result] (tente view test.Result). Nesse caso, havia apenas um resultado, e era um teste aprovado.

Um teste baseado em propriedades

Vamos testar isso um pouco mais detalhadamente. square deve ter a propriedade de que square a * square b == square (a * b) para todas as escolhas de a e b. A biblioteca de testes suporta a criação de testes baseados em propriedades como essa. Há uma nova sintaxe aqui, explicada a seguir:

Isso testará nossa função com várias entradas diferentes.

Notas sobre a sintaxe

Adicionar código à base de código

A função square e os testes que escrevemos para ela ainda não fazem parte da base de código. Até o momento, eles existem apenas em nosso arquivo de rascunho. Vamos adicioná-los agora. Vá para o UCM e digite add. Você deverá obter algo como:

Você acabou de adicionar uma nova função e alguns testes à sua base de código Unison sob o namespace tour. Tente digitar view square ou view square.tests.prop1. Observe que o Unison insere declarações use precisas ao renderizar seu código. As instruções use não fazem parte do seu código quando ele está na base de código. Ao renderizar o código, um conjunto mínimo de instruções use é inserido automaticamente pela code printer, de modo que você não precisa ser preciso com suas instruções use.

Se você digitar test no prompt do Unison, ele “executará” seu conjunto de testes:

Mas, na verdade, ele não precisava executar nada! Todos os testes haviam sido executados anteriormente e armazenados em cache de acordo com seu hash Unison. Em uma linguagem puramente funcional como a Unison, testes como esses são determinísticos e podem ser armazenados em cache e nunca mais executados. Chega de executar os mesmos testes várias vezes!

Movimentação e renomeação de termos

Quando adicionamos square, estávamos no namespace tour, portanto square e seus testes estão em tour.square. Também podemos mover os termos e namespaces para locais diferentes em nossa base de código com os comandos move.

Quando terminar de alterar algumas coisas, você poderá usar find sem argumentos para visualizar todas as definições no namespace atual:

Observe também que não precisamos executar novamente nossos testes após essa reorganização. Os testes ainda estão armazenados em cache:

Isso é obtido gratuitamente, apesar da renomeação, porque o cache de teste é codificado pelo hash do teste, não pelo nome do teste.

Quando você está começando a escrever algum código, às vezes é bom colocá-lo em um namespace temporário, talvez chamado temp ou scratch. Mais tarde, sem quebrar nada, você pode mover esse namespace ou partes dele para outro lugar, usando os comandos move.termmove.type e move.namespace.

Modificação de definições existentes

Em vez de iniciar uma função do zero, muitas vezes você deseja apenas modificar ligeiramente algo que já existe. Aqui faremos uma alteração na implementação da nossa função mySquare.

Usando o comandoedit

Tente digitar edit mySquare no UCM:

Isso copia a definição pretty-printed do mySquare em seu arquivo de trabalho “acima do fold”. Ou seja, ele adiciona uma linha que começa com --- (fold) e coloca o que já estava no arquivo abaixo dessa linha. O Unison ignora qualquer conteúdo de arquivo abaixo do “fold”.

Vamos editar o mySquare e, em vez disso, definir o mySquare x (apenas por diversão) como a soma dos primeiros x números ímpares (aqui está uma bela ilustração geométrica do motivo pelo qual isso dá os mesmos resultados):

Adicionando uma definição atualizada à base de código

Observe que a mensagem diz que mySquare pode ser atualizado (is ok to update). Vamos tentar isso:

Somente os testes afetados são executados novamente em update

Se executarmos novamente os testes, eles não serão armazenados em cache desta vez, pois uma de suas dependências foi realmente alterada:

Observe que a mensagem indica que os testes não foram armazenados em cache. Se fizermos test novamente, obteremos os resultados recém armazenados em cache.

O rastreamento de dependência para determinar se um teste precisa ser executado novamente é 100% preciso e é rastreado no nível das definições individuais. Você só executará novamente um teste se uma das definições individuais das quais ele depende tiver sido alterada.

Publicar o código e instalar as bibliotecas do Unison

🎉 Finalmente, você precisará se organizar para escrever o código Unison e se inscrever no Unison-share! Vá para Unison Share e siga as instruções para vincular sua base de código local para hospedagem de código!

O código é publicado na própria solução de hospedagem de código da Unison, Unison Share, usando o comando push e as bibliotecas são instaladas por meio do comando pull. Não há necessidade de ferramentas separadas para gerenciar dependências ou publicar código, e você nunca encontrará conflitos de dependência no Unison.

O que vem a seguir?

🌟Configure UCM to set author and license information

🌟Learn more about Unison projects

🌟Take a look at the language documentation and get started writing Unison code

🌟Learn how to update terms and resolve conflicts

📚O The Unison Share catalogue contém um índice de projetos Unison de código aberto. Se estiver trabalhando em algo no Unison, conte-nos sobre isso no canal #libraries no Unison slack

📚A mental model for Abilities in Unison covers a unique aspect of Unison’s type system

📚Learn how to write Unison docs


Blogpost MOC Plan MOC
Published MOC

Todo MOC