Содержание
Когда я решил изучать OCaml, одним из первых докладов, который я посмотрел, был доклад Ярона Мински ”Почему OCaml?”. В ней он рассказывает о причинах, по которым Jane Street выбрала OCaml в качестве языка, на котором они будут создавать практически все в компании. Что интересно, OCaml был тем языком, который я выбрал по очень конкретным и хорошо продуманным причинам, которые оставались верными со временем и опытом работы с языком.
Функциональное программирование
Несколько лет назад я много внимания уделял функциональному программированию, которое я считаю лучшим способом написания программного обеспечения и его аналогов. Хотя элементы функционального программирования можно использовать в таких языках, как Python и Ruby, их идиомы все равно ориентированы на более объектно-ориентированный подход, что делает такие идиомы во многих случаях практически непостижимыми, в основном при их совместном использовании.
Python и Clojure
Python был первым языком, который я, можно сказать, освоил досконально, еще в 2012 году. С тех пор я полюбил простоту, и во многом эта простота обусловлена влиянием Lisp.
Clojure, будучи функциональным и с синтаксисом, унаследованным от Lisp, легко привлек мое внимание, и я потратил много времени на изучение идиом этого языка. Но, по сути, чего-то не хватало, например, хорошей системы типов (типы и Clojure - это очень глубокая кроличья нора) и зависимости от JVM (то, что на самом деле хорошо в доменах, Clojure присутствует в основном как язык). Кроме того, инструментарий достаточно сложный, документации не хватает, а сообщество, как правило, очень ориентировано на консалтинг и предприятия.
Haskell
Я пробовал Haskell, но, хотя это интересный язык, есть несколько моментов, которые следует учитывать. Весь инструментарий имеет тенденцию к утяжелению, например, образ Docker имеет размер 700 МБ (когда я впервые попробовал, он был более 1 ГБ). Кроме того, есть определенные накладные расходы на выполнение более практичных вещей, что проще в Clojure, например, с состоянием, используя только atom.
Входит в OCaml
OCaml немного меняет ситуацию, синтаксис, более близкий к ML, с возможностью запускать побочные эффекты без особых сложностей помогает тем, кто учится, а также облегчает эксперименты и более прогрессивный дизайн.
Не обязательно понимать монаду IO, чтобы сделать простой Hello World, но можно запустить все за монадой, если это интересует программиста.
main :: IO () main = putStrLn "Hello world".
let () = print_endline "Hello world".
print_endline - это прямой вызов, здесь нет никаких дополнительных абстракций и практически нет накладных расходов на понимание одной строки кода.
Если у меня локально установлен инструментарий OCaml, я могу даже скомпилировать файл .ml простым вызовом ocamlc без необходимости создавать проект с нуля, чтобы провести небольшие эксперименты, как в Clojure. Babashka решает эту проблему, но создает дополнительное бремя для новичков.
(Хорошие) типы
Многие люди не любят типы, и я особенно считаю, что TypeScript создает некоторые трудности, если вы просто пытаетесь создать что-то очень простое. Кроме того, система типов, как мне кажется, не очень приятна, структурная подтипизация создает некоторые сообщения об ошибках, которые очень трудно читать и понимать.
Кроме того, есть еще одна проблема: необходимость постоянно объявлять типы, если вы хотите, чтобы компилятор проверил, что вы хотите.
В Python, например, следующая функция должна использовать Any для n и для возвращаемого типа:
def fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2) # def fib(n: Any) -> Any.
Но в следующем коде OCaml типы выводятся без проблем:
let rec fib n = if n < 2 then n else fib (n - 1) + (fib - 2) (* val fib : int -> int = <fun> *).
Ошибки становятся очевидными
Функциональные языки вообще имеют свойство позволять коду, который, вероятно, не следовало бы делать, быть более очевидным. С хорошей системой типов это становится еще более очевидным.
Например, сериализаторы должны быть чистыми функциями и не должны делать запросы к базе данных. По очевидным причинам, связанным с производительностью и обслуживанием, также редко кто ожидает, что сериализатор будет делать запрос к базе данных.
В OCaml для производственных систем мы обычно используем Lwt, который, по сути, является системой обещаний для асинхронного ввода-вывода и параллелизма. Но тип Lwt.t - это монада, а монады имеют свойство ”заражать” все, к чему прикасаются. Предположим, что ваш сериализатор имеет следующую сигнатуру, где он преобразует заданный тип t в строку для отправки в другой сервис (вероятно, JSON).
тип t val serialize : t -> string
Если вам нужно преобразовать какую-то информацию в строку, то именно такая сигнатура должна быть у вашего сериализатора. Однако если по какой-то причине внутри `сериализатора нужно сделать запрос к базе данных, то функция вынуждена иметь другую сигнатуру:
val serialize : t -> string Lwt.t.
Что делает очевидным, что serialize делает что-то, чего не должен делать.
Производительность
Еще один важный фактор - производительность. В OCaml естьy предсказуемая производительность, если вы немного изучите, как работает компилятор и как ваш код попадает в финальную сборку на той или иной архитектуре. В конце концов вы поймете, что можете с определенной легкостью предсказать заранее, как ваш код будет выполняться на самом деле.
Компилятор OCaml известен тем, что выдает очень эффективный код, что очень хорошо для языка, который предлагает такой уровень абстракции, как OCaml.
Классический пример - Fibonacci, посмотрите, насколько эффективен конечный код:
let rec fib n a b = if n < 2 then b else fib (n - 1) b (a + b).
OCaml использует наименьший значащий бит, чтобы отличать целые числа от указателей и выполнять нестандартные целочисленные операции. Поэтому код мало чем отличается от ожидаемого:
caml
Example__fib_268:
subq $8, %rsp
.L101:
cmpq (%r14), %r15
jbe .L102
.L103:
cmpq $5, %rax
jge .L100
movq %rdi, %rax
addq $8, %rsp
ret
.L100:
leaq -1(%rbx,%rdi), %rsi
addq $-2, %rax
movq %rdi, %rbx
movq %rsi, %rdi
jmp .L101
.L102:
call caml_call_gc@PLT
.L104:
jmp .L103
Аверс - несколько деталей:
Все операции выполняются непосредственно в регистрах. Рекурсии преобразуются в цикл эффективным образом. Используются те же регистры, что и в вызывающем устройстве, в данном случае System V.
Экосистема
Экосистема очень доступна, если знать английский, сообщество Discourse довольно многолюдно, и люди, занимающиеся библиотеками и языком, почти всегда отвечают людям на форуме. Это очень здорово, так как вы получаете прямой взгляд от людей, которые работают над этим, а не только от непосредственного пользователя языка или библиотеки.