OCaml: типизация и написание скриптов

ocaml учебник

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

OCaml и семейство ML

OCaml относится к семейству языков ML. К нему же относятся ныне редкий Standard ML, Microsoft F#, который во многом представляет собой клон OCaml, и, с оговорками, Haskell.

Многие языки семейства ML способны производить статическую проверку типов в коде, где нет ни одного объявления типа переменной благодаря механизму вывода типов. Ограниченную форму вывода типов многие уже видели в Go, где можно не объявлять примитивные типы, а просто писать var x = 10. Swift предоставляет те же возможности. OCaml идет гораздо дальше и выводит типы функций.

Теоретическая основа вывода типов — алгоритм Хиндли — Милнера. Детерминированный вывод типов возможен не во всех системах. В частности, Haskell использует тот же подход, но функции в нем требуют явного объявления типа. Цена детерминированности — отсутствие полиморфизма ad hoc (перегрузки функций). Каждая функция в OCaml может иметь один и только один тип. Отсутствие перегрузки функций компенсируется «функторами» — параметризованными модулями.

РЕКОМЕНДУЕМ:
— Язык программирования Ada
— Программирование на языке Ada

Swift заимствовал многие концепции из ML, в частности алгебраические типы и параметрический полиморфизм, которые обеспечивают удобную и безопасную работу функций с коллекциями (списками) независимо от типа значений. Новые языки для JS также зачастую заимствуют из них, так что знакомство с ML полезно для их понимания.

Почему OCaml?

Традиционное применение OCaml и языков семейства ML — разработка компиляторов, средств статического анализа и автоматического доказательства теорем. К примеру, на нем был написан компилятор Rust до того, как он научился компилировать сам себя. OCaml также нашел применение в финансовой сфере, где ошибка в коде может за пару минут довести компанию до банкротства, например в Lexifi и Jane Street.

Пригодным к применению в качестве скриптового его делает особенность реализации: он предоставляет одновременно интерпретатор, компилятор в байт-код и компилятор в машинный код для популярных платформ, в том числе x86 и ARM. Можно начать разработку в интерактивной оболочке интерпретатора, затем записать код в файл. А если скрипт превращается в полноценную программу, скомпилировать ее в машинный код.

С помощью сторонних инструментов вроде js_of_ocaml и BuckleScript код на OCaml также можно компилировать в JavaScript. В Facebook таким способом переписали большую часть Facebook Messenger на ReasonML, это альтернативный синтаксис для OCaml.

В отличие от Haskell, в OCaml используется строгая, а не ленивая модель вычислений, что упрощает ввод-вывод и анализ производительности. Кроме того, он поддерживает изменяемые переменные (ссылки) и прочие средства императивного программирования.

Мы не будем затрагивать веб-скрипты и сосредоточимся на традиционных. В этой статье я также намеренно опускаю создание собственных типов, систему модулей и многие другие возможности. Первая цель — показать «вкус» языка.

В этой статье предполагается использование совместимой с POSIX системы: Linux, FreeBSD или macOS. Сам OCaml работает на Windows, но менеджер пакетов OPAM пока не поддерживает эту систему, поэтому библиотеки пришлось бы собирать вручную со всеми зависимостями. Теоретически использовать OPAM можно в Cygwin, но я не пробовал.

Установка OCaml

Стандартным менеджером пакетов для OCaml стал OPAM. Кроме библиотек, он также может устанавливать сам компилятор и переключаться между разными версиями. Многие системы предоставляют какую-то версию OCaml в репозиториях (часто устаревшую), но с помощью OPAM легко поставить самую свежую от имени обычного пользователя. Для Linux и macOS авторы предоставляют статически скомпилированную версию.

Как установить OPAM, вы можете прочитать в документации.

После установки мы поставим самую новую на настоящий момент версию компилятора 4.07 и несколько утилит и библиотек, которые потребуются нам в примерах.

Для проверки работоспособности запустим utop — интерактивную оболочку интерпретатора. Стандартный интерпретатор ( ocaml) слишком «спартанский» — без поддержки истории команд и автодополнения, поэтому его мы будем использовать только в неинтерактивном режиме.

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

Компилятор в байт-код называется ocamlc, а нативный компилятор — ocamlopt. Сегодня мы столкнемся с ocamlc, но рассматривать компиляцию не будем.

Работа в OCaml

Для первого примера мы напишем традиционный hello world с дополнительной фичей — возможностью установить приветствие с помощью переменной окружения GREETING.

Сохраним код в файл hello.ml и попробуем запустить:

Вместо выполнения из командной строки мы можем импортировать файл в интерактивный интерпретатор с помощью директивы #use. В качестве бонуса мы также увидим выведенные типы всех переменных и функций.

Для простоты выполнения можно сделать файл исполняемым и добавить #!/usr/bin/env ocaml. На работу интерпретатора это не повлияет, но с точки зрения компиляторов будет ошибкой синтаксиса, поэтому в нашем примере мы этого не делаем.

Теперь разберем, что происходит в этом коде. Код начинается с комментария, комментарии заключаются в символы (* ... *).

Сначала мы определяем две переменные с помощью ключевого слова let. Синтаксис: let <имя> = <значение>. Кроме глобальных объявлений переменных, мы будем использовать локальные — с помощью конструкции let <имя> = <значение> in <выражение>. Такие объявления могут быть выложенными.

Затем мы определяем функцию, которая выдает значение переменной окружения GREETING, если она определена, или значение по умолчанию, если нет. Синтаксис функций мало отличается от синтаксиса для переменных: let <имя> <список формальных параметров> = <тело>.

В качестве формального параметра мы берем () — значение типа unit, которое часто используется как заглушка: функций без аргументов и возвращаемых значений в OCaml быть не может. Как и во всех функциональных языках, оператора return тут нет — функции не возвращают значения, а вычисляются в них.

Если переменная не определена, функция Sys.getenv выдает исключение Not_found, которое мы обрабатываем с помощью конструкции try ... with ....

Особенности исключений в OCaml

Исключения в OCaml не являются объектами, и в нем нет иерархии исключений. Можно создавать новые исключения всего одной строкой и передавать в них значения любых типов:

Далее мы определяем функцию с настоящим аргументом — адресатом приветствия. Выделенной точки входа, вроде main в C, в OCaml нет. Все выражения программы просто вычисляются сверху вниз. Мы вызываем функцию greet и игнорируем ее значение с помощью конструкции let _ = greet "world".

Более правильным способом будет let () = greet "world", потому что попытка использовать значение не типа unit на правой стороне выражения станет ошибкой типизации. Это частный случай сопоставления с образцом.

Просмотр выведенных типов

Мы уже видели, что интерактивный интерпретатор показывает все выведенные типы, но это можно сделать и в неинтерактивном режиме. Самый простой способ увидеть типы переменных и функций — запустить ocamlc -i. Эта же команда часто применяется для автоматической генерации интерфейсов модулей.

Типы функций пишутся через стрелки, в unit -> string на левой стороне стрелки — тип аргумента, а на правой — тип возвращаемого значения. У функции со многими аргументами стрелок будет больше: int -> int -> string — функция от двух целочисленных значений, которая возвращает строку.

Быстро просматривать выведенные типы очень помогает сторонний инструмент под названием Merlin. Он интегрируется со многими популярными редакторами и позволяет увидеть тип выделенного выражения в тексте.

Типобезопасный printf

Попробуем внести в нашу программу ошибки. Например, используем неправильный формат в printf. В большинстве языков это будет ошибкой времени выполнения или ошибкой сегментации, но не в OCaml. Заменим %s на %d и запустим:

Тип выражения с printf выводится в зависимости от строки формата, и несовпадение формата с типом фактического параметра приводит к ошибке компиляции.

Попробуем заменить greet "world" на greet 42:

Эффект тот же: компилятор понял, что greet имеет тип string -> unit, и возмутился, когда вместо строки в качестве аргумента использовали число.

Инфиксные операторы и частичное применение функций

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

Если оператор начинается с символа *, следует поставить после скобки пробел, чтобы компилятор не принял выражение за начало комментария:

Стандартная библиотека предоставляет ряд полезных операторов вроде |>, который позволяет легко строить цепочки выражений без лишних скобок. Если бы его не было, его можно было бы легко определить как let (|>) x f = f x.

Здесь значение hello world передается функции String.length, а затем значение передается дальше в printf. Особенно удобно это бывает в интерактивном интерпретаторе, когда нужно применить еще одну функцию к предыдущему выражению.

Можно заметить, что у printf здесь указан только один аргумент — форматная строка. Дело в том, что в OCaml любую функцию со многими аргументами можно применить частично (partial application), просто опустив часть аргументов.

Тип функции Printf.printf "%s %s\n" будет string -> string -> unit — формат зафиксирован, но аргументы для строк свободны. Если указать еще один аргумент, мы получим функцию типа string -> unit с фиксированным первым аргументом, которую сможем применить ко второму.

Чтение файлов

Теперь напишем тривиальный аналог cat — программу, которая читает строки из файла и выдает их на стандартный вывод. Здесь мы задействуем оператор |> и средства императивного программирования.

Функция fail выводит сообщение об ошибке и завершает выполнение программы с кодом 1. Два выражения разделены точкой с запятой, как в большинстве императивных языков. Нужно только помнить, что в OCaml точка с запятой не завершает выражения, а разделяет их. Ставить ее после последнего выражения нельзя. Последнее выражение в цепочке становится возвращаемым значением функции.

РЕКОМЕНДУЕМ:
Программирование в консоли

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

Файл мы открываем с помощью функции open_in : string -> in_channel. В OCaml дескрипторы файлов для чтения и записи — это значения разных несовместимых типов ( in_channel и out_channel), поэтому попытки писать в файл, открытый только для чтения или наоборот, будут пойманы еще на этапе проверки типов.

В функции read_file мы используем цикл while с достаточно очевидным синтаксисом.

Изменяемые переменные

По умолчанию все переменные в OСaml неизменяемые. Их значения можно переопределить, но модифицировать нельзя. Такой подход гораздо безопаснее, но иногда обойтись без присваивания сложно.

Для этой цели применяются ссылки. Создать ссылку можно с помощью функции ref, которая возвращает значения типа 'a ref. Буква с апострофом означает, что этот тип полиморфный — на ее месте может быть любой тип, например int ref или string ref.

Присвоить ссылке новое значение можно с помощью оператора :=, а получить ее текущее значение — с помощью !.

Для демонстрации мы напишем аналог wc, который считает строки в файле. Ради простоты возьмем наш cat, но прочитанные строки проигнорируем с помощью let _ = и добавим lines := !lines + 1 через точку с запятой. Очень важно не забывать получать значение ссылки с помощью !, иначе будет ошибка компиляции — значения и ссылки на них четко разделены.

Веб-скрепинг

Можно поспорить, что предыдущие примеры не были такими уж «скриптовыми». В завершение я продемонстрирую извлечение заголовков новостей из главной страницы сайта с помощью библиотеки lambdasoup.

Мы могли бы выполнить запрос HTTP из самого скрипта, например с помощью библиотеки cohttp, но для экономии времени будем читать вывод curl со стандартного ввода.

Сохраним код в файл ./x_news.ml и выполним:

Что происходит в этом коде? Сначала мы подключаем библиотеки. Механизм их подключения не является частью языка, а реализован в библиотеке компилятора topfind. Импортируем ее с помощью #use "topfind". Затем мы используем директиву #require из этой библиотеки, чтобы подключить пакет lambdasoup, установленный из OPAM. В utop мы можем сразу использовать #require, потому что он автоматически подключает topfind, но в стандартном интерпретаторе этот шаг необходим.

Директивы интерпретатора также не являются частью языка, а реализуются через расширения. При желании можно даже создать свои.

С помощью open Soup.Infix мы импортировали модуль, где определены инфиксные операторы LambdaSoup в основное пространство имен. К остальным его функциям мы будем обращаться по их полным названиям, вроде Soup.read_channel. Оператор $$ выглядит знакомо для каждого пользователя библиотек вроде jQuery — он извлекает соответствующие селектору элементы из дерева HTML, которое мы получили из Soup.parse.

Поскольку на главной странице сайта заголовки новостей находятся в тегах <h3> с классом entry-title, а сам текст заголовка — в его дочернем теге <span>, в get_news_header мы применяем селектор span и передаем результат функции Soup.leaf_text, которая извлекает из элемента текст.

Элементы в HTML могут не иметь текста, поэтому для функции Soup.leaf_text авторы использовали особый тип: string option. Значения типа string option могут быть двух видов: None или Some <значение>. Такие типы — альтернатива исключениям, которые в принципе невозможно проигнорировать, поскольку тип string option несовместим со string и, чтобы извлечь значение, нужно обработать и ситуацию с None.

В функции default мы разбираем оба случая с помощью оператора match, который можно рассматривать как эквивалент case из C и Java, хотя его возможности шире. В случае с None мы возвращаем указанное в аргументе значение по умолчанию, а в случае с Some — возвращаем присоединенную к Some строку.

Наконец, в Soup.iter мы передаем анонимную функцию, которую она применит к каждому элементу из news.

Заключение

Компактный синтаксис, отсутствие необходимости писать объявления типов и интерактивные средства разработки делают OCaml пригодным для больших серьезных проектов и для небольших программ и скриптов. В то же время статическая проверка типов помогает писать надежные и безопасные программы, а возможность компиляции в машинный код или JS обеспечивает языку широкий спектр применения.

Многие возможности системы типов остались за кадром — например, в ряде случаев она может обнаружить проблемы с логикой кода вроде бесконечных циклов, а не только простое несовпадение типов переменных.

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

Если вы ищете новый язык для прикладных программ, не посмотреть на OCaml будет большим упущением.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий