Как-то заглянув на GitHub, обнаружил Koka — язык функционального программирования со статической типизацией. Koka разрабатывается с 2012 года Daan Leijen в Microsoft Research, USA. Его исходники выкладываются на GitHub под лицензией Apache 2.0. Как признаются его авторы, он ещё не готов для промышленного применения: у него нет библиотек, менеджера пакетов и полной поддержки в средах разработки. При этом сам язык достаточно стабилен, а компилятор полностью разработан. Отдельными моментами язык напоминает Rust, Haskell и Scala. Сам же по себе он интересен контролем побочных эффектов. Это его основная фишка. Приглашаю познакомиться с Koka и обсудить его свойства.


fun main(): console ()
  println("Hello, World!")

Сначала пробежимся по свойствам языка, а затем рассмотрим один небольшой пример.


Итак, Koka обладает следующими свойствами:


  • статическая типизация с выводом типов;
  • функциональная парадигма;
  • неизменяемость при наличии настоящих переменных;
  • разделение на чистый код и код с побочными эффектами;
  • синтаксис, напоминающий таковой у Rust и Scala;
  • идентификаторы допускают символ дефис - (как в Lisp);
  • комбинация синтаксисов со скобками и с отступами;
  • типы-объединения и типы-структуры;
  • полиморфизм;
  • отсутствие циклов (при этом рекурсивный код при компиляции
    может быть преобразован в циклический);
  • оператор возврата из функции необязателен:
    результатом будет значение последнего выражения;
  • сопоставление с образцом;
  • оператор точка, позволяющий заменить вложенные вызовы функций на их последовательность
    (как операция |> в Elm или Elixir);
  • оператор with, позволяющий избавиться от использования анонимных функций;
  • необязательные и именованные параметры функций;
  • отсутствие сборщика мусора;
  • компиляция в машинный код и другие целевые платформы;
  • имеется REPL.

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


Инструкции по установке Koka есть на главной странице проекта. Поддерживаются только 64-х разрядные системы. Можно собрать из исходников, которые написаны на Haskell. Также дистрибутив можно получить с GitHub проекта. Например, для Debian и Ubuntu предлагаются готовые к установке пакеты.


После установки, кроме компилятора со стандартной библиотекой языка, будут установлены дополнения для подсветки синтаксиса в GNU Emacs, Atom и Visual Studio Code. В Code расширение было подхвачено автоматически. А в Emacs потребовалось написать в .emacs/init.el пару строчек:


(load "/usr/share/koka/v2.4.0/contrib/emacs/koka-mode")
(require 'koka-mode)

Вычисляем числа Фибоначчи, или программирование с эффектами


Чтобы познакомиться с Koka на деле, напишем программу, которая вычисляет n-е число Фибоначчи и печатает его в консоли.


Синтаксис языка достаточно прост, чтобы набросать начальную версию программы:


fun main()
  val n = 10
  println(fib(n))

fun fib(n : int) : int
  match n
    0 -> 0
    1 -> 1
    _ -> fib(n - 1) + fib(n - 2)

Сохраним файл с именем fibonacci.kk и попробуем скомпилировать эту программу:


koka fibonacci.kk

И получим вот такое сообщение компилятора:


compile: fibonacci.kk
loading: std/core
loading: std/core/types
loading: std/core/hnd
check  : fibonacci
fibonacci.kk(5, 5): error: effects do not match
  context        :     fib
  term           :     fib
  inferred effect: (<>)
  expected effect: <div|_e>
  because        : effect cannot be subsumed

Здесь мы видим, что компиляция прошла успешно, то есть синтаксически программа корректна, но вот проверку программа уже не прошла: функция fib была неявно объявлена с эффектом total, коротко обозначаемым как <>, что означает чистоту. Но чекер обнаружил, что у функции эффект другого типа div. Этот тип эффектов возникает в программах, в которых не гарантируется останов. По видимому компилятор так решил, потому что наша функция рекурсивна. (Добавление проверки на отрицательный аргумент не делает функцию с точки зрения чекера тотальной. Оставлю читателю этот эксперимент в качестве упражнения.)


У нас есть два варианта:


  • задать конкретный тип эффекта div;
  • или попросить вывести тип эффекта, указав _.

fun main()
  val n = 10
  println(fib(n))

fun fib(n : int) : _ int
  match n
    0 -> 0
    1 -> 1
    _ -> fib(n - 1) + fib(n - 2)

Теперь программа успешно соберётся. Запустим и убедимся в правильности результата:


./.koka/v2.4.0/gcc-debug/fibonacci

Вообще, этот код с подвохом: мы не задали тип для функции main. Каков же он на самом деле? Давайте попробуем разобраться.


Запустим REPL:


koka

Загрузим наш модуль:


:l fibonacci.kk

И запросим тип функции fib:


:t fib

Получим ожидаемое:


check  : interactive
(n : int) -> div int

Теперь для main:


:t main

Оказывается у main тип такой:


check  : interactive
() -> <console,div> ()

То есть main ничего не возвращает (имеет пустой тип ()) и имеет два эффекта: div и console. Первый эффект обусловлен вызовом из main функции с таким эффектом, а второй связан с тем, что main осуществляет ввод-вывод.


В Koka есть ещё два вида эффектов:


  • exn для функций, генерирующих исключения;
  • ndet для недетерминированных функций.

Но вы можете создавать свои собственные эффекты и их обработчики.


Так что давайте этом займёмся: добавим в наш код печать отладочной информации — будем выводить в консоль каждое вычисленное значение числа Фибоначчи. Обычное решение в этом случае — вставить в нужное место кода функцию печати:


fun debug-info(value : int) : _ int
  println(value)
  return value

fun fib(n : int) : _ int
  debug-info(match n {
    0 -> 0
    1 -> 1
    _ -> fib(n - 1) + fib(n - 2)})

fun main()
  val n = 10
  println(fib(n))

Но что если мы хотим в разных местах кода для вызова fib печатать по разному? Вот здесь нам уже пригодятся эффекты с обработчиками:


effect fun debug-info(value : int) : int

fun fib(n : int) : _ int
  debug-info(match n {
    0 -> 0
    1 -> 1
    _ -> fib(n - 1) + fib(n - 2)})

fun main()
  val n = 10

  with handler
    fun debug-info(value)
      println(value)
      return value
  println(fib(n))

  with handler
    fun debug-info(value)
      println("* " ++ show(value) ++ " *")
      return value
  println(fib(n))

В первой строке кода мы объявили абстрактный эффект, а перед вызовом кода, где вычисляется fib мы определяем обработчик этого эффекта с помощью конструкции with handler.


На практике


Как уже говорилось выше, для Koka нет ни библиотек (кроме стандартной), ни менеджера пакетов, а для редакторов кода есть только синтаксическая подсветка. И кажется, что Koka никуда не годится, однако это не совсем так. Автор языка разработал на нём транслятор для markdown в HTML и PDF (с привлечением TeX) для научных текстов Madoko. (В Ubuntu для работы Madoko потребуется дополнительно установить пакет texlive-science.)


Что дальше поизучать


  • Посмотреть и послушать доклад на конференции от самого автора языка Daan Leijen. Видео длинное, два с половиной часа, но достаточно интересное и понятное.
  • Проштудировать официальную документацию. На одной странице рассмотрены все концепции языка.
  • Ознакомиться с примерами кода в исходных текстах Koka на GitHub и с исходниками его стандартной библиотеки (там же).
  • Попытаться прочитать научные статьи, в которых описывается математика языка и его компилятора. Ссылки можно найти в README исходных текстов.

© Симоненко Евгений, 2022

Комментарии (25)


  1. youngmysteriouslight
    16.02.2022 00:30
    +6

    Вполне интересно. Можно подробнее описать систему типов? Есть операторы типов высших порядков, например? Какой используется полиморфизм?

    Кстати, первый пример с fib удивляет. Кажется, что fib должна быть самой настоящей total, потому что она примитивно-рекурсивна. Как доказать компилятору, что она всегда завершается?

    upd. Только что увидел заявление

    • отсутствие сборщика мусора

    Это как? Я не в теме, но как слышал, только Rust успешно смог организовать работу с функциями, чтобы обойтись без сборщика мусора, и за это заплатили цену введением неинтуитивных концепций, вроде владения. Тут такой же масштаб амбиций или имеется в виде что-то другое?


    1. easimonenko Автор
      16.02.2022 01:03
      +4

      Большое спасибо за вопросы! Наверное чтобы на них ответить, мне нужно будет написать ещё один пост. К сожалению официальная документация неполна, поэтому придётся копать глубже.

      Кстати, первый пример с fib удивляет. Кажется, что fib должна быть самой
      настоящей total, потому что она примитивно-рекурсивна. Как доказать
      компилятору, что она всегда завершается?

      Меня это тоже сначало удивило. Но ведь тип int влючает в себя также и отрицательные числа, для которых fib не остановится. Попробовал сейчас этот пример при `n = -1`, и он рухнул с ошибкой сегментирования памяти. Наверное в каком нибудь Idris такое даже не скомпилировалось бы. (Но я в Idris и Agda не особо разбираюсь.)

      Я не в теме, но как слышал, только Rust успешно смог
      организовать работу с функциями, чтобы обойтись без сборщика мусора, и
      за это заплатили цену введением неинтуитивных концепций, вроде владения.
      Тут такой же масштаб амбиций или имеется в виде что-то другое?

      Вот в этой статье авторы описывают алгоритм, благодаря которому они избавились от GC: Alex Reinking, Ningning Xie, Leonardo de Moura, and Daan Leijen: “Perceus: Garbage Free Reference Counting with Reuse” MSR-TR-2020-42, Nov 22, 2020. Distinguished paper at PLDI'21. https://www.microsoft.com/en-us/research/publication/perceus-garbage-free-reference-counting-with-reuse/


      1. youngmysteriouslight
        16.02.2022 01:28
        +2

        Примерно понятно.

        Попробовал сейчас этот пример при n = -1, и он рухнул с ошибкой сегментирования памяти.

        Это и не должно работать. Функция определена так, чтоб аргумент был положительным; а по-хорошему, сигнатура должна быть соответствующей, nat -> nat.

        Наверное в каком нибудь Idris такое даже не скомпилировалось бы.

        Как Int -> Int, конечно, нет, а Nat -> Nat — да, причём тотальна без доп. вопросов. В Idris бывает проблема, когда функция тотальна или рассматриваемый тип не населён, но компилятор не сразу догадывается, там надо доказывать, но Idris предоставляет средства логики первого порядка для доказательств, или believe_me накрайняк.


        1. 0xd34df00d
          16.02.2022 09:36
          +3

          Логика там не только первого порядка, но да, завершимость можно доказывать (я про это даже писал тут).


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


    1. funca
      16.02.2022 01:14
      +4

      Концепция владения в Rust скорее непривычна. В бэкграунде там простая идея, что каждое значение может использоваться максимум один раз, которая в свою очередь формализована в affine types https://en.m.wikipedia.org/wiki/Substructural_type_system .


      1. csl
        16.02.2022 01:27
        +2

        Не споря с вами: там и одна из первых прикладных реализаций субструктурной типизации и линейной (здесь подразумевается любой вариант субструктурной типизации) логики.

        https://habr.com/ru/post/497762/comments/#comment_21514808


        1. funca
          16.02.2022 02:00
          +1

          @0xd34df00dвиднее, что он имел ввиду. :) Вики пишут, что ещё Clean это использовал уже 30 лет с хвостиком тому назад (я с ним не знаком, могу ошибаться. судя по описанию там это применяли для проверки корректности кода, а не управления памятью).

          Писать на языке именно с линеарными типами (это когда каждое значение нужно где-то поиспользовать) наверное ацки неудобно. Интересно как там будет выглядеть применение функций в роде print (результат возврата которой придется тоже кому-то отдавать)?


          1. csl
            16.02.2022 02:12
            +1

            Что использует Clean, виднее @vkni.

            Некоторые особенности https://habr.com/ru/post/596193/comments/#comment_23858079


            1. vkni
              16.02.2022 19:30
              +1

              Clean использует unique типы, родственные линейным, где действительно можно просто "съедать" значение. Насколько я понимаю, к линейным типам это сводится если в линейных сделать какую-нибудь функцию eat:: *a -> ().


              1. funca
                17.02.2022 16:51
                +1

                Такой eat не тайпчекнится если у нас линеарные типы. Но это можно с афиными где разрешается использовать значение <=1 раз.


          1. 0xd34df00d
            16.02.2022 09:32
            +3

            Писать на языке именно с линеарными типами (это когда каждое значение нужно где-то поиспользовать) наверное ацки неудобно. Интересно как там будет выглядеть применение функций в роде print (результат возврата которой придется тоже кому-то отдавать)?

            Да не, это все вполне пристойно засахаривается. Другое дело, что, ИМХО, не надо бросаться весь код переписывать на линейных типах, и они скорее нужны либо для корректности (ну там не забыть закрыть хендл и не закрыть его дважды), либо для производительности и мутабельности без явной ST.


            1. vkni
              16.02.2022 19:35
              +1

              Что характерно, в Tackling the Awkward Squad, Саймон-Пейтон рассказывает про IO монады, фактически, переписывая всё на Clean. Ну когда он вводит на пятой странице type IO a = World -> (a, World). Только на реальном Клине там нужно ещё поставить * и заменить ключевое слово type на продукт сумрачного голландского гения:

              :: IO a = *World -> *(a, *World)


          1. vkni
            16.02.2022 19:45
            +2

            судя по описанию там это применяли для проверки корректности кода, а не управления памятью

            Там это применяли для реализации побочных эффектов, но без монад, т.к. Clean - это один из чистых ленивых языков, наследников Miranda. Сейчас Clean очень похож на Haskell. Реализация ввода-вывода в Clean фактически изложена на 5 странице самой понятной статьи про IO в Haskell, Tackling the Awkward Squad:... - https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/mark.pdf

            Посколько голландцы, как и французы, пытались сделать что-то промышленно-годное, в отличие от англичан, они заморачивались производительностью, поэтому использовали уникальные типы не только для ввода-вывода, но и для реализации изменяемых массивов.


            1. funca
              17.02.2022 16:53
              +1

              О практическом использовании в хаскеле https://wiki.zhen-zhang.com/tech/notes/books/AT-TAPL/Substructural%20Types/


  1. csl
    16.02.2022 01:03
    +1

    Удачное проведение параллелей между оператором точка в Koka и операцией |> в Elixir. Ещё бы добавить подсветку синтаксиса для Koka в highlight.js, чтобы и на Хабре улучшить отображение (выделение цветом, например).


    1. easimonenko Автор
      16.02.2022 01:08
      +1

      Про подсветку Koka я тоже подумал. К слову сказать в Emacs в markdown-mode она подхватывается.


    1. easimonenko Автор
      17.02.2022 12:10
      +2

      Посмотрел как добавить в highlight.js новый язык: они больше не принимают языки в саму библиотеку, все новые языки должны идти отдельными библиотеками. Но можно им написать, чтобы они добавили её себе для распространения. Поэтому автоматом на Хабре подсветка Koka не появится. https://github.com/highlightjs/highlight.js/blob/main/extra/3RD_PARTY_QUICK_START.md


  1. csl
    16.02.2022 01:38
    +1

    Может показаться интересным https://proofassistants.stackexchange.com


  1. Kroleg
    16.02.2022 04:23
    +4

    Крутая фича "отсутствие сборщика мусора"

    Как они это сделали? "Since mutable references are uncommon in our setting, we leave the responsibility to the programmer to break cycles"

    Чуда не произошло. :-(


    1. sashagil
      16.02.2022 09:18
      +2

      Пока нет, но вы оборвали цитирование.

      ... "However, we have plans for future improvements: since we know statically that only mutable references are able toform a cycle, we could generate code that tracks those datatypes at run time and may perform a more efficient form of incremental cycle collection."


  1. worldmind
    16.02.2022 08:14
    +2

    Начал читать и не понял зачем его создавали, в чём отличия от хаскела да идриса? По идее с этого должна начинаться статья. В коментах про отсутствие сборщика мусора, конкурент расту?


    1. sashagil
      16.02.2022 09:10
      +3

      Алгебраические эффекты вместо хаскелевских монад упрощают ряд вещей, это довольно интересная тема.


      1. worldmind
        16.02.2022 10:15
        +2

        А есть где сравнение упомянутых для чайников?


        1. sashagil
          16.02.2022 19:49
          +4

          Есть статьи, взвешенно-нейтрально разбирающие два подхода (в общем контексте программирования на Хаскеле, чаще всего), и есть более однобокие «вводные» статьи. Приведу ссылки, а ниже изложу своё поверхностное мнение (по воспоминаниям по просмотру статей, я темой интересовался).

          «Алгебраические эффекты» человеческим языком - перевод на Хабре вводной статьи прикладного программиста, который ближе к концу поясняет, что с Хаскелем он не знаком и потому научные статьи ему непонятны, он рассматривает тему с практическими примерами на JavaScript (ссылка на оригинал статьи для удобства). Более недавняя статья другого программиста с примерами на OCaml: Friendship ended with Monads: Testing out Algebraic effects in OCaml for Animations.

          Научные статьи, рассматривающие алгебраические эффекты и альтернативные подходы (монадные трансформеры) в рамаках Хаскеля: Monad Transformers and Modular Algebraic Effects (What Binds Them Together), Freer Monads, More Extensible Effects. Я такие статьи, скорее, просматриваю - с Хаскелем шапочно знаком, но не программирую на нём.

          Уместна ссылка на давнюю статью автора Koka: Algebraic effects for Functional Programming.

          Моё поверхностное мнение: мне бы на Koka было бы удобнее программировать, чем разбираться с монадными трансформерами на Хаскеле. Koka - компактный язык, чётко иллистрирующий идею как "чисто" программировать в функциональном стиле с эффектами ("чисто" в том смысле, что нотация эффектов явная, и при этом не особо навязчивая). В плюс к этому - такое программирование хорошо укладывается у меня в голове (я "вижу", что будет происходить в машинном коде при выполнении - и при желании могу разобраться в сгенерированном компилятором Koka коде на стандартном C, реализуещем нужную семантику благодаря фиче C longjump, насколько я понимаю). Ну и -- при желании я этот код на C мог бы встроить в свою программу (на C++); не уверен, что получилось бы легко с встраиванием кода на Хаскеле.

          С Rust сравнивать, наверно, можно, но осторожно - размах не тот (Rust - промышленный язык, Koka - скорее, демонстрационный, ну, как сравнивать Rust и Idris).


  1. easimonenko Автор
    16.02.2022 10:25
    +6

    Странности какие-то на Хабре: заглядываю утром в статью и вижу, что слетели все ссылки. С чего бы это? Открываю статью в редакторе (markdown) и вижу, что список ссылок в конце статьи отредактирован. Это как? Автоматически? Но зачем? Или редактор? Но тогда редактору нужно поучить markdown сначала.

    Для тех, кто не понял, я использую списки ссылок в формате [Название]: URL. Затем в тексте обращаюсь к ним как [Название][] или [другой текст][Название].