Um tour pela Unison
author:: chicoary
source:: Um tour pela Unison
clipped:: 2024-01-04
published:: agosto 19, 2023
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:
- Verificação de digitação e compilação de novos códigos
- Organizar, navegar e encontrar definições do Unison
- Armazenar o estado de sua base de código
- Executar programas e binários do Unison
- Publicar e extrair bibliotecas do Unison
🎉 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:
- É append-only: uma vez que um arquivo no diretório
.unison
é criado, ele nunca é modificado ou excluído, e os arquivos são sempre nomeados de forma única e determinística com base em seu conteúdo. - Como resultado, uma base de código Unison pode ser versionada e sincronizada com o Git ou qualquer ferramenta semelhante e nunca gerará um conflito nessas ferramentas.
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.sqrt
, base.Optional.Some
, base.Nat
, base.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 oucd .
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 projetotour
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:
- List.reverse A sintaxe
: [a] -> [a]
é a sintaxe para fornecer uma assinatura de tipo a uma definição. Pronunciamos o símbolo:
como “‘has type” (tem o tipo), como emreverse
has the type[a] -> [a]
. [Nat]
é a sintaxe para o tipo que consiste em listas de números naturais (termos como[0, 1, 2]
e[]
terão esse tipo) e, de forma mais geral,[Foo]
é o tipo de listas- Qualquer variável em minúsculas em uma assinatura de tipo é considerada como sendo universally quantified, portanto,
[a] -> [a]
realmente significa e poderia ser escritoforall a . [a] -> [a]
, que é o tipo de função que recebe uma lista cujos elementos são de algum tipo e retorna uma lista de elementos do mesmo tipo. List.reverse as
recebe um parâmetro, chamadoas
. O que vem depois do é chamado de corpo da função, e aqui é um block, que é delimitado por whitespace.acc a -> ..
é a sintaxe para uma função anônima.- Os argumentos da função são separados por espaços e a aplicação da função é mais forte do que qualquer operador, de modo que
f x y + g p q
é analisado como(f x y) + (g p q)
. Você sempre pode usar parênteses para controlar o agrupamento de forma mais explícita.
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.
O 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:
- O prefixo
test>
informa ao Unison que o que vem a seguir é uma “test watch expression”. Observe que também estamos dando um nome a essa expressão,square.tests.ex1
. - Não há nada de especial no nome
square.tests.ex1
; poderíamos chamar essas ligações de qualquer coisa que quiséssemos. Aqui, usamos a convenção de que os testes para uma definiçãofoo
vão parafoo.tests
.
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
- O bloco Unison, que começa depois de um , pode ter qualquer número de ligações (como
a = ...
), todas no mesmo nível de indentação, terminadas por uma única expressão (aquiexpect (square ..)
), que é o resultado do bloco. - Você pode chamar um parâmetro de função de
_
se quiser ignorá-lo. Aqui,go
ignora seu argumento; seu objetivo é apenas fazer com quego
seja lazily evaluated para que possa ser executado várias vezes pela funçãoruns
. - O
natInOrder
é um delayed computation. Umadelayed computation
é aquela em que o resultado não é computado imediatamente. A assinatura de umadelayed computation
pode ser considerada como uma função sem argumentos, retornando o resultado final:() -> a
. O!
em!gen.natInOrder
avalia adelayed computation
, invocando um valor do tipoNat
. - O
natInOrder
vem dotest
–test.gen.natInOrder
. É um “gerador” de números naturais. O!natInOrder
gera um desses números começando em0
e incrementando em um a cada vez que é chamado.
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.term
, move.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