Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Введение

В текущий век технологий, цифровизации, открытий существует очень много способов реализации себя как творца, как созидателя. В графическом дизайне появилось очень много программ для графики, а также планшетов с подключением к компьютеру и без, с экраном и без него; в скульптуре, поэзии, спорте и любых иных сферах, которые только могут прийти в голову, цифровизация так или иначе повлияла на деятельность. То же применимо и к профессии инженера: появилось множество новых подпрофессий, появилось множество различных инструментов для создания систем (как текстовых, так и графических), много разных методик, ресурсов для обучения и т.д.

Однако же остается в силе вопрос: как поддерживать системы? Если системе больше пары лет, она использует множество сторонних решений (библиотек), а так же над ней работает много людей, появляется неизбежная энтропия: система становится запутанной, появляется множество багов, становится сложнее улучшать продукт. Люди решают это разными способами. Например, были придуманы системы версий (Git, Mercurial), которые позволяют создавать и отслеживать версии (кто бы мог подумать!), отслеживать авторов тех или иных участков систем, делать отдельные версии системы для тестов разных фич, и т.п. Также развиваются и языки программирования с окружением вокруг: постепенно внедряются решения для улучшения Developer Experience, появляются пакетные менеджеры (cargo и uv), стандартизируются уже существующие решения.

Но что, если я вам скажу, что можно в принципе не волноваться за кодовую базу, конфликты имен и прочее проблемы, связанные с кодом? Может показаться сказкой, но Unison - язык, который решил все эти проблемы на корню, просто избавившись от файлов кода и перенеся все в специальную базу!

Но обо всем по порядку: Unison - статически типизированный функциональный язык с автоматическим выводом типов, имеющий систему эффектов (которая называется “Способностями”), а также хранит код в виде AST (Абстрактное синтаксическое дерево) в базе данных. Это дает свои преимущества:

  • Время сборки почти мгновенное всегда, так как не нужно перекомпилировать уже добавленное;
  • Нет конфликта имен ни в каком виде;
  • Тесты тоже кешируются и исполняются практически мгновенно, так как нет нужды проверять все, если не было изменений;
  • Контроль версий уже встроен в саму систему, при этом нет проблем с форматированием или конфликтов при слиянии разных версий (точнее, они есть, но система сама помогает их разрешить).

Многие из этих терминов и слов могут быть непонятными, и это нормально - как говорил один великий мыслитель: “Век живи, век учись”. Все будет объяснено по ходу текста, так что не переживайте.

Во время обучения мы пройдемся через:

  1. Настройку и установку окружения;
  2. Базовые команды в UCM;
  3. Основы языка;
  4. Типы данных;
  5. Функции и их особенности;
  6. Способности функций;
  7. Знакомство с Unison Share;
  8. Написание своих программ и систем.

Сразу скажу: эту книгу я пишу во время освоения языка, так как в Интернете не нашел ни одной книги по Unison в принципе. Если будут опечатки, недостатки и ошибки, прошу присылать их мне на почту (me@ioze.net). Спасибо!

Итак, начнем!

Установка и настройка Unison

На самом деле, ваша система особо не важна, так как Unison работает на любой из них, а если и нет (вдруг вы пользуетесь системой из семейства BSD или подобной), то всегда можно скомпилировать все из исходников.

Установка Unison на Windows

Первым делом вам нужно установить «Windows Terminal» терминальным приложением по умолчанию для лучшей работы. Как только вы это сделали, вам нужно:

  • Скачать c GitHub исполняемый файл UCM по вот этой ссылке;
  • Распаковать архив в любом удобном вам месте;
  • Запустить ucm.exe.
  • Вы великолепны!

Установка Unison на Mac

Для максимального удобства лучше всего скачивать через Homebrew, однако, если по какой-либо причине вы не хотите устанавливать его или пользоваться, будут описаны все способы.

Через Homebrew

Если он еще не скачан, зайдите на официальный сайт и установите по инструкции. Затем вам нужно ввести (или скопировать) следующие команды в терминал:

brew tap unisonweb/unison
brew install unison-language

Они установят вам все нужные файлы, библиотеки и сам UCM. Для обновления используйте brew upgrade unison-language

Через Curl

Данный способ подходит, если вы хотите скачать все вручную. Введите в терминал следующие команды: Для Мак на Интел:

mkdir -p unisonlanguage && cd unisonlanguage
curl -L https://github.com/unisonweb/unison/releases/latest/download/ucm-macos-x64.tar.gz \
  | tar -xz
./ucm

Для Мак на М-процессорах:

mkdir -p unisonlanguage && cd unisonlanguage
curl -L https://github.com/unisonweb/unison/releases/latest/download/ucm-macos-arm64.tar.gz \
  | tar -xz
./ucm

Они создадут папку в вашем каталоге, установят туда архив с бинарниками, распакуют и запустят UCM. Учтите, что данный способ не сделает UCM глобальной программой; если вас это не устроит, придется изменять переменные среды.

Установка Unison на дистрибутивы Linux

Debian/Ubuntu/дистро, использующий apt

Для установки вам потребуется ввести следующие команды:

curl https://debian.unison-lang.org/public.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/unison-computing.gpg
echo 'deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/unison-computing.gpg] https://debian.unison-lang.org/ trixie main' | sudo tee /etc/apt/sources.list.d/unison-computing.list
sudo apt update
sudo apt install unisonweb
/usr/bin/ucm

Nix

Для установки через Nix прошу зайти на эту страницу, так как процесс установки может поменяться в любой момент.

Curl

Схоже с установкой под Мак (в том числе и по ограничениям). Вот команды:

mkdir -p unisonlanguage && cd unisonlanguage
curl -L https://github.com/unisonweb/unison/releases/latest/download/ucm-linux-x64.tar.gz \
	| tar -xz
./ucm

Компиляция из исходников

Если по какой-то причине вас не устраивает установка готовых файлов, или же под вашу систему таких файлов просто нет, вы можете спокойно скомпилировать Unison из исходных файлов. Для компиляции вам потребуется Haskell, а также Stack. Если у вас недостает чего-то, рекомендую установку через GHCup. После установки выполните следующие команды:

$ git clone https://github.com/unisonweb/unison.git
$ cd unison
$ stack --version # для проверки установки
$ stack build --fast --test && stack exec unison

Настройка окружения

Для разработки вам не потребуются сложные менеджеры проектов, куча плагинов для быстрого перемещения, супер-комбинации клавиш, потому что работа будет по факту вестись только в одном файле и в терминале. Поэтому вы вольны использовать любой редактор кода, который только пожелаете (если кому интересно, у меня Helix). Однако для комфорта все равно стоит поставить LSP - так у вас появится автодополнение, подсветка синтаксиса и проверка кода. Для его настройки перейдите на данную страницу, там все подробно расписано и рассказано. Если не поставите его - ничего страшного не случится.

Запуск и начало работы

Итак, вы установили Unison! Теперь откройте ваш любимый редактор кода, запустите UCM, и будем приступать к изучению! Все команды мы разберем по ходу изучения самого языка. Но сразу скажу: чтобы выйти из UCM, напишите exit :)

Основы Unison

В Unison все начинается с «терма». Если по-простому, то терм - это название какого-либо значения или функции. Возьмем такой код:

helloWorld : Text
helloWorld = "Hello, World!"

Такой код объявит переменную helloWorld, которая имеет тип Text и хранит в себе строку «Hello, World!». Сразу скажу, что все переменные неизменяемы - это один из столпов функционального программирования впринципе, и это удобно, когда системы становятся сложными. Если не верите мне - дочитайте книгу и попробуйте сами сделать аналогичные системы на Unison и на, например, Python :) А пока продолжаем!

Из-за того, что Unison сам умеет понимать, что присваивается переменной, мы можем написать без первой строки:

helloWorld = "Hello, World!"

И все будет работать точно так же. Но для наглядности я буду писать типы переменной.

Создайте файл scratch.u в той же папке, в которой у вас запущен UCM. Затем напишите код и сохраните файл. В терминале вы сразу увидите что-то подобное:

    Loading changes detected in ~/unisonlanguage/scratch.u.


  + helloWorld : Text

  Run `update` to apply these changes to your codebase.

Наш UCM сам автоматически отслеживает файл и показывает, что добавилось, удалилось или изменилось. Попробуем написать update:

quickstart/main> update

  Done.

quickstart/main>

Все, теперь ваша переменная может использоваться, даже не находясь в файле! Для наглядности можете попробовать очистить файл и вписать туда следующее:

> helloWorld

После сохранения файла в UCM сразу отобразится сообщение о том, что

  1. Не найдены изменения;
  2. «Вычислится» наше выражение (в данном случае - просто выведется).
quickstart/main>

  Loading changes detected in ~/unisonlanguage/scratch.u.


  No changes found.


    1 | > helloWorld
          ⧩
          "Hello, world!"

Данная стрелочка объявляет «контрольное выражение» - выражение, которое выводится при любом обновлении файла. Это довольно полезно при разработке, и я вам советую использовать их как можно чаще.

Также есть и другие базовые типы данных: натуральные, целые числа, числа с плавающей точкой, булева логика, строки, символы и байты:

isGood : Boolean -- булево
isGood = true 

natural : Nat -- натуральное число
natural = 42

justNum : Int -- целое число
justNum = +42

pi : Float -- число с плавающей точкой
pi = 3.14

aChar : Char -- символ
aChar = ?a

blob : Bytes -- сырые байты
blob = 0xsdeadbeef

Прошу обратить внимание, что целые числа обязаны всегда иметь знак перед числом, ведь если будет не +42, а 42, число будет интерпретировано как натуральное. Также для Char перед любым символом нужно поставить знак вопроса, а для Bytes - применить шаблон 0xs<шестнадцатиричный код>.

Также в терминале у нас при сохранении отобразится следующее:

  Loading changes detected in ~/unisonlanguage/scratch.u.


  + aChar   : Char
  + blob    : Bytes
  + isGood  : Boolean
  + justNum : Int
  + natural : Nat
  + pi      : Float

  Run `update` to apply these changes to your codebase.

Пока нет смысла сохранять, лучше немного поработаем с нашими переменными. Например, посмотрим, что будет, если сложить натуральное число с целым:

  I couldn't figure out what + refers to here:

     20 | > natural + justNum

  The name + is ambiguous. I tried to resolve it by type but no term with that
  name would pass typechecking. I think its type should be:

      Nat -> Int -> o

  If that's not what you expected, you may have a type error somewhere else in
  your code.
  Help me out by using a more specific name here or adding a type annotation.

  I found some terms in scope with matching names but different types. If one of
  these is what you meant, try using its full name:

  (Float.+) : Float -> Float -> Float
  (Int.+) : Int -> Int -> Int
  (Nat.+) : Nat -> Nat -> Nat
  (Pattern.+) : Pattern a -> Pattern a -> Pattern a
  (Class.+) : Class -> Class -> Class
  (Bag.+) : Bag a -> Bag a -> Bag a
  (Decimal.+) : Decimal -> Decimal -> Decimal
  (Integer.+) : Integer -> Integer -> Integer
  (Natural.+) : Natural -> Natural -> Natural
  (Rational.+) : Rational -> Rational -> Rational
  (Duration.+) : Duration -> Duration -> Duration
  (Instant.+) : Instant -> Duration -> Instant
  (LocalDate.+) : LocalDate -> Int -> LocalDate
  (Deprecated.+) : Natural.Deprecated -> Natural.Deprecated -> Natural.Deprecated

Мы увидели, что UCM не знает, какой именно оператор выбрать, потому что для каждого типа он определен свой. И мы также можем догадаться, что у нас не получится сложить два разных типа, пускай они и очень похожи. И если мы все-таки хотим это сделать, нам придется одну из переменных привести к другому типу. Но пока что мы не будем это делать, лучше посмотрим, что еще интересного можно делать здесь и сейчас:

19 | > helloWorld ++ " Hello, Unison!"
           ⧩
           "Hello, world! Hello, Unison!"

20 | > natural - 50
           ⧩
           0

Оператор ++ определен для строк и складывает их, тут все понятно. А вот второй случай интереснее: как вы помните, natural у нас имеет значение 42, однако же ошибки не случилось. То есть мы можем не переживать о том, что у нас может получиться неопределенное поведение, так как Nat всегда положителен. Честно скажу: кроме этих двух особенностей я не нашел особых отличий с другими языками программирования: для чисел есть математические операции, для логики есть булевые операции, и т.п. Остальные операторы будут появляться в книге и остальные типы будут рассказаны вам чуть позже. А сейчас я хочу поговорить немного о документации.

Документация

В коде поддерживаются комментарии:

  • два дефиса - для комментария в линию;
  • {-<любой текст>-} - для мультистрочных комментариев;
  • три дефиса - для разграничения файла; Последнее покажу на примере. Если мы впишем в файл следующее:
> helloWorld ++ " Hello, Unison!"
---
> natural - 50
Я люблю Unison

И сохраним, то UCM нам напишет следующее:

    19 | > helloWorld ++ " Hello, Unison!"
           ⧩
           "Hello, world! Hello, Unison!"

quickstart/main>

То есть второе выражение не было просчитано, и UCM даже не поругался на мое признание.

Но дело в том, что комментарии не «загружаются» в базу при ее обновлении, поэтому они используются только при разработке, а не для документации. Возникает закономерный вопрос: а как же тогда документировать и объяснять код?

И на помощь нам приходит особый тип Doc! Он определяется так: {{<текст>}}, и из коробки поддерживает форматирование, фото, видео, таблицы, Mermaid, LaTEX и даже исполнение кода внутри себя! Я ознакомлю вас с базовым функционалом, но, может, в конце книги оставлю полный список возможностей.

Чтобы добавить документацию к чему-либо, есть два способа:

  1. Он подходит для уже добавленных в базу объектов:
helloWorld.doc = {{Это - строка "Привет, мир!"}}
  1. Подходит для пишущихся сейчас объектов:
{{Очень важное число!}}
bestNumber = 42

После сохранения файла UCM пишет нам:

+ bestNumber     : Nat
+ bestNumber.doc : Doc
+ helloWorld.doc : Doc

Готово, у нас есть документация для модуля! Теперь напишем в самом UCM следующее:

quickstart/main> docs bestNumber

  Очень важное число!

quickstart/main>

Команда docs выводит документацию по объекту. То, что вам пригодится уже с первой минуты - это @source{<имя объекта>} для отображения исходников в Doc, @signature{<имя объекта>} для отображения сигнатуры объекта (это как раз тип объекта), а так же можно добавлять Doc внутри Doc. Давайте я вам покажу все три примера вместе:

{{
  @signature{bestNumber}
  
  Очень важное число!

  @source{bestNumber}

  {{bestNumber.doc.examples}}
}}
bestNumber = 42
bestNumber.doc.examples = {{
    Примеры:

    ```
       bestNumber + 1 
    ```     
}}

Сохраним файл и посмотрим на документацию в UCM:

quickstart/main> docs bestNumber

      bestNumber : Nat

  Очень важное число!

      bestNumber : Nat
      bestNumber = 42

  Примеры:

      bestNumber Nat.+ 1
      ⧨
      43

quickstart/main>

Такая мощная документация позволяет ей поддерживать и обновлять саму себя, а также исчерпывающе объяснять и показывать все нужное разработчику. И я вам сразу дал эти знания, чтобы вы могли с первого дня строить не только красивые, но и понятные системы :)

Функции

Если вы изучали другие языки по типу Java, Python, C++ и им подобным, вы, наверно, знаете, что такое функции. Однако в Unison они работают немного по-другому, поэтому я буду описывать с нуля.

Функция - объект, который при передаче других объектов вычисляет результат. Посмотрим на примере:

plusOne : Nat -> Nat
plusOne n1 =
  use Nat +
  n1 + 1

> plusOne 41

В сигнатуре функции мы видим два типа со стрелочкой посередине. Последний тип в цепочке - тип результата, остальные - типы аргументов (передающихся объектов):

plusOne : Nat -> Nat --- результат
           |
           ------------- аргумент

То есть в этой функции 1 аргумент. Следующим мы видим название функции, затем название аргумента:

plusOne n1

Так же мы видим здесь строку:

use Nat +

Если мы посмотрим в прошлую главу, то вспомним, что есть несколько определений оператора «+». Через use мы можем указать, какую версию оператора или терма мы хотим использовать. Вникнем в это позже, но важно знать, что Unison умеет сам выводить и добавлять use, так что большую часть времени вам не потребуется это писать.

И последним идет само рассчитывающееся выражение:

n1 + 1

При выполнении функции значение переменной подставится, выполнится расчет и вернется значение. Стоит помнить, что последнее выражение в функции - это и есть результат функции.

Сохраним файл и посмотрим, что нам выведет Unison:

quickstart/main>

  Loading changes detected in ~/unisonlanguage/scratch.u.


  + plusOne : Nat -> Nat

  Run `update` to apply these changes to your codebase.


    6 | > plusOne 41
          ⧩
          42

quickstart/main>

Вот, он рассчитал нам функцию! Теперь, по такой же схеме сделаем вторую функцию:

plusNums : Nat -> Nat -> Nat
plusNums n1 n2 =
  n1 + n2

> plusNums 41 1

И чтобы точно запомнилось:

plusNums : Nat -> Nat -> Nat --- результат
           |      |
           -------------------- аргументы

В терминале у нас отображается следующее:

    5 | > plusNums 41 1
          ⧩
          42

Так можно добавлять сколько угодно элементов, ограничений нет.

Еще одной особенностью является то, что в Unison функции каррированы, в отличии от других языков. Что это значит? Рассмотрим на примере:

plusThree : Nat -> Nat -> Nat -> Nat

Эта функция принимает три аргумента, но по факту, под капотом это выглядит вот так:

plusThree : Nat -> (Nat -> (Nat -> Nat))

То есть по факту мы можем передать один аргумент, чтобы получить функцию, принимающую два аргумента, затем ей передать второй, чтобы получить функцию, принимающую один аргумент, и уже ей передать последний аргумент, чтобы получить результат.

Звучит сложно, я знаю. Но на деле это проще чем кажется. Например, мы можем из нашей функции plusNums повторить функцию plusOne (если не удалили старую реализацию, сделайте это перед вводом):

plusOne = plusNums 1

И получим функцию, работающую точно так же и имеющую такую же сигнатуру:

+ plusOne  : Nat -> Nat

Это полезно во многих ситуациях, не стоит недооценивать подобную особенность.

Переменные

В функции можно точно так же, как и просто на верхнем уровне, объявлять переменные. Это удобно для хранения промежуточных результатов. Пример:

plusThree : Nat -> Nat -> Nat -> Nat
plusThree n1 n2 n3 =
  plusTwo = n1 + n2
  plusTwo + n3

Сейчас (и дальше) на подобных простых примерах я не буду выводить результат UCM: думаю, и так понятно, что делает эта функция.

Блоки

В Unison блоком называется участок кода, сгруппированный вместе для организации. Блок выделяется через отступы, можно еще дополнительно обозначать его через let. Отступы важны и имеют значение, и не только в этом конкретном примере, по мере изучения инструментов это будет видно. Также блоки могут быть вложены друг в друга. Покажу на примере с официального сайта Unison:

nesting : Text
nesting =
  parent = "outer"
  inner1 = let
    child1 = "child1"
    inner2 = let
      child2 = "child2"
      parent
    inner2
  inner1

Анонимные функции и функции высшего порядка

На самом деле, в функциональном программировании это очень важная тема, которую нужно понять для легкой и продуктивной работы. Я постараюсь объяснить все максимально просто.

Анонимные функции (лямбда-функции) - это такие же функции, но без имени. Во всем остальном они полностью схожи с обычными функциями.

А функции высшего порядка - это функции, которые в аргументах принимают функции. В Unison есть такая особенность, что функция тоже может быть аргументом, и из-за этого появляется множество возможностей. Рассмотрим на примерах:

a = [1, 2, 3, 4, 5]
> List.map (elem -> elem + 1) a
> List.map
  (i -> let
    x = i + 1
    y = x + 1
    z = y + 1
    z) [1, 2, 3]
    19 | > List.map (elem -> elem + 1) a
           ⧩
           [2, 3, 4, 5, 6]

    20 | > List.map
           ⧩
           [4, 5, 6]

Обратите внимание сначала на аргумент List.map. Это скобочки, в которых до стрелочки идут аргументы (если несколько аргументов, они разделяются пробелом), а после стрелочки - выражения. И видно, что лямбда-функции тоже поддерживают блоки, но let в них обязателен. Кстати, обычные функции с именем тоже можно передавать как аргумент, если подходит сигнатура (это вы увидите в следующей главе).

И заодно вы увидели пример функции высшего порядка. В следующей главе я расскажу про List, коллекции в целом и другие подобные функции.

Типы данных

В Unison есть не только базовые типы данных. В базовой библиотеке есть несколько очень полезных типов (называемых «коллекциями»), без которых сложно представить современную разработку, и сейчас вы узнаете о них.

Списки

Очень часто нам нужно хранить несколько объектов в одном месте (например, список студентов в группе). Для этого в Unison есть нативный тип данных - List. Он может хранить в себе несколько объектов одного типа. Под капотом список реализован как Finger Tree («Пальчиковое дерево»), что дает моментальный доступ к первому и последнему элементу, а также относительно быстрые вставку, удаление и соединение двух списков. Ниже будут примеры создания и использования списков:

    1 | > List.empty
          ⧩
          []

    2 | > []
          ⧩
          []

    3 | > List.singleton 1
          ⧩
          [1]

    4 | > [1]
          ⧩
          [1]

    5 | > List.fill 3 "hello"
          ⧩
          ["hello", "hello", "hello"]

    6 | > 1 +: [2, 3, 4]
          ⧩
          [1, 2, 3, 4]

    7 | > [1, 2, 3] :+ 4
          ⧩
          [1, 2, 3, 4]

    8 | > List.size [1, 2, 3, 4, 5]
          ⧩
          5

    9 | > List.insertAt 1 4 [1, 2, 3]
          ⧩
          [1, 4, 2, 3]

    10 | > List.replace 0 4 [1, 2, 3]
           ⧩
           [4, 2, 3]

    11 | > List.deleteAt 2 [1, 2, 3, 4]
           ⧩
           [1, 2, 4]

    12 | > [1, 2, 3] ++ [4, 5, 6]
           ⧩
           [1, 2, 3, 4, 5, 6]

    13 | > List.head [1, 2, 3]
           ⧩
           Some 1

    14 | > List.tail [1, 2, 3]
           ⧩
           Some [2, 3]

    15 | > List.last [1, 2, 3]
           ⧩
           Some 3

    16 | > List.reverse [1, 2, 3, 4]
           ⧩
           [4, 3, 2, 1]

Много текста, разберем все по порядку:

  • Создать список можно несколькими способами:
    • Через [];
    • Через List.empty;
    • Через List.singleton (в списке будет 1 элемент);
    • Через List.fill (тогда в списке будет N одинаковых элементов)
    • Через List.tabulate, первым аргументом передаем число, до которого будет идти счет (в нашем случае будет от 0 до 4), и функцию, в которую каждое число из ряда будет передавать, а результат функции будет добавлять в список. Вот пример:
      18 | > List.tabulate 5 (x -> x * 3)
             ⧩
             [0, 3, 6, 9, 12]
    
  • Добавлять элементы к списку в начало и в конец через операторы List.+: и List.:+ соответственно;
  • Получать количество элементов через List.size;
  • Соединять два списка через оператор ++;
  • Вставлять, заменять или удалять элементы по индексу через List.insertAt, List.replace и List.deleteAt соответственно (в сигнатурах первое число - индекс элемента);
  • Переворачивать список через List.reverse;
  • А также получать элементы списка:
    • List.head возвращает первый элемент;
    • List.tail возвращает все, кроме первого элемента;
    • List.last возвращает последний элемент;

Как вы можете заметить, результат функции не просто <элемент>, а Some <элемент>. Это - особый тип, который показывает, что элемент действительно есть (ведь, допустим, мы не можем взять первый элемент из пустого массива). В отличии от Java, C++ или Python, в Unison нет такого понятия, как null. Чуть позже мы разберем все эти типы, как они работают, для чего они нужны и что они предотвращают.

А пока что вернемся к особой функции в списках - List.map.