Статически-типизированный Python — всё украдено до нас?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Несколько лет назад были очень популярны статьи о том, „как ввести типы в Питон“, вернее добавить статическую типизацию. Язык Питон очень хорош для написания простых программ с небольшим количеством ветвлений, но в кровавом энтерпрайзе со временем система разрастается, программы усложняются, обрастают if-ами, обработками ошибок, сложным наследованием… И вот тогда отсутствие статической типизации даёт о себе знать. Появляются неожиданные падения, ошибки, программу становится трудно анализировать.


Да, можно использовать „фашистские“ правила кодирования, два-три разных линтера, "import typing“, писать километры тестов, cython, mypy (который, кстати, через запуск показывал предупреждение в одном моём скрипте, чем страшно меня удивил) и т.д. Но все эти меры паллиативны, убивают скорость и простоту разработки.


Почему бы не рассмотреть другие языки? Посмотрим же, что нам нужно от замены: лёгкость среды, те же платформы, где блистает Python, статическая типизация с выводом типов. Хотя кого я обманываю? Всем понятно, что выбранный для проекта язык в первую очередь зависит от компетенций команды, стандартов компании и т.д. Тем не менее, за сравнение вроде бы не бьют?


Давайте сформулируем, что же мы хотим от этого „заменителя Питона“. В качестве обязательных условий возьмём:


  1. Близость семантики к Питону.
  2. Компилируемость. (если уж типы проверяем, почему бы и не докомпилировать?)
  3. Вывод типов. (иначе можно взять cython)
  4. Желательно побыстрее, но это не критично. (где критично, там пишут библиотеки для Python на C).
  5. Хочется не терять „лёгкость“ Python – скорость разработки, быстроту выполнения „Hello world“ов (меня, лично, раздражают скрипты на 3 строки и пол минуты загрузки).
  6. И, конечно, общая приспособленность для „кровавого ынтерпрайза“ — хорошая поддержка разной сложной бизнес-логики и прочего ужаса.

Если мы посмотрим на какой-нибудь рейтинг языков, например TIOBE или PYPL, мы увидим, что из более-менее популярных подходят Rust, Swift, Kotlin. Увы и ах, но Rust не имеет сборщика мусора, Swift не кроссплатформен, Kotlin требует jvm. Мотаем дальше!


Во второй десятке PYPL мы находим Haskell – это уже ближе, хотя Haskell ленивый, чистый, но он со сборщиком мусора, native code, доступен более-менее на тех же платформах, что и Python. Но вот пятый пунктым Haskell не вышел — в его сообществе как-то принято считать, что пользователь всегда может чуть-чуть подождать. Конечно есть Clean с его великолепной эргономикой (0.1 сек на сборку „Hello World“), но сообщества вокруг него, к сожалению, нет. В общем, за медлительность и ленивость отказать, но Хаскель всё-таки стоит записать в маленькую чёрную книжечку.


Условно „промотав дальше“ мы найдём OCaml – вот этот язык уже значительно ближе к Python, чем Haskell. Собственно говоря, OCaml имеет все вышеперечисленные свойства, плюс, в отличие от Haskell, здесь почти нет монад (ну, если „на шкаф не залезать“). По пятому пункту полный зачёт — творцы из Инрии маниакально относятся к производительности компилятора, он получается даже быстрее Клина.


Сравнение языков


Итак, что же у нас есть из возможностей Python, а чего нет? Если просто взглянуть на две программы на этих языка станет очевидно одно – у них катастрофически различается синтаксис, „они похожи друг на друга, как мел и сыр.“ Кроме того, как известно, Ocaml — это функциональный язык, а Python — императивный и, вдобавок, объектно-ориентированный. Фиаско?


К счастью, оба языка не являются „ортодоксальными“ представителями своих семейств, поддерживая и другие парадигмы. В Ocaml'е есть объектно-ориентированная подсистема, и он допускает императивный код, а Python содержит ряд возможностей характерных для функциональных языков (кортежи, анонимные функции, функции высших порядков и т.д.). Посмотрим, достаточно ли этого для полноценной замены.


Так что там с императивностью в „функциональном языке“? Ну, во-первых, в отличие от Haskell в Ocaml есть побочные эффекты и даже оператор „точка с запятой“. Поэтому вполне можно награфоманить


let () =
    begin
        Printf.printf "Hello ";
        Printf.printf "world!\n"
    end

Имеются стандартные циклы for и while. Правда „верблюжий“ for слегка отличается от цикла for в Python. Но это лишь потому, что for в Python „шагает не в ногу“ — это же ведь foreach.


Есть в Ocaml и ссылки — тип ref, позволяющие создавать ну совершенно императивный код, императивнее некуда:


let factorial n =
    let z = ref 1 in
    for i = 1 to n do
       z := !z * i
    done;
    !z

И тут скептически настроенный читатель спросит — что же, теперь везде ставить эти галочки!?! Ну конечно же нет, в Ocaml есть и приветствуется name shadowing или переиспользование имён (в отличие от Haskell). Поэтому, если бы в Python мы хотели написать


def quad_sqr(r):
    r = 2*r
    return r*r

в Ocaml мы напишем


let quad_sqr r =
   let r = 2*r in (* тут мы переиспользуем имя, а не присваиваем! *)
   r*r

То есть, код получается в „псевдоимперативном“ стиле — вроде бы мы и не изменяем ничего, а по смыслу мы как бы и делаем присвоение (знакомые с компиляторами вспомнят про SSA форму, в которую переводится и C++ в clang, и Rust). Причём мы же можем и тип поменять! То есть, поведение при name shadowing получается очень похожим на Python, где мы тоже можем написать


...
x = 1
x = "uno"
...

...
let x = 1 in
let x = "uno" in
...

Кроме того, в Ocaml, как и в Python, нет не только функции Main, но даже и символа Start. Выражения верхнего уровня просто начинают выполняться сверху вниз, как будто интерпретатор их обычным образом читает.


В результате, cтруктура программ на этих языках очень похожа. И там, и там сперва подключаются библиотечные модули, а затем идёт список последовательно вычисляемых выражений. Правда, в Ocaml функции — это тоже значения, поэтому их нельзя вызывать до того, как они вычислены, то есть выше по тексту, а в Python можно. То есть, в Ocaml нужны небольшие указания компилятору, чтобы из функции f вызвать g, если текст g расположен ниже (то есть, функции взаимно рекурсивные).


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


import os

x = 8

def f(z):
   return z + x

print(f(2))

open Unix

let x = 8

let f z = z + x

Printf.printf "%d\n" (f 2)

Как вы уже заметили, типы я нигде не указывал. Это потому, что указывать их и не нужно — система типов позволяет выводить их почти всегда. Не сказать, что это так уж замечательно всегда и везде, но это факт. Кто не верит, может убедиться почитав исходники компилятора, например https://github.com/ocaml/ocaml/blob/trunk/typing/patterns.ml Заодно, кстати, станет понятно, где же у вывода типов тёмная сторона.


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


Зато в Ocaml есть и широко используются модули! Благодаря прекрасному разработчику по имени Alain Frisch они даже могут быть значениями! Почти как в 1ML. Причём модули могут быть параметризованы, то есть, это функции над модулями (в Ocaml они называются функторами). Таким образом, модули слегка дублируют объекты, а значит и в Ocaml тоже можно реализовать огромное количество примеров из книги четырёх.


И да, кровавый энтерпрайз функторы в Ocaml любит точно также, как классы в Python. И тоже злоупотребляет.


Различия


Меня всегда удивляло, что многие знатоки Python относят списковые включения (list comprehensions) к „нефункциональному стилю“, подразумевая под функциональщиной map, filter и прочие функции высших порядков. Всё-таки, даже в ортодоксально функциональных языках вроде Miranda есть и то, и другое.


А вот в Ocaml этих самых list comprehensions нет. Совсем нет, да и не было никогда. Зато там есть функции высших порядков вроде List.map и анонимные функции, а также стащенный из Haskell оператор ($) превратившийся в очки (@@).


Кроме того, в Ocaml есть алгебраические типы и прекрасно разработанный и оптимизированный pattern-matching со всеми прелестями:


match (price, quantity) with
| (None, _)                -> 0.0
| (Just x, v) when x > 0.0 -> x *. v
| (Just x, _)              -> failwith "negative price"

Зато, как вы могли заметить, в этом чудесном отрывке, манящим нас в мир корректной бизнес логики, есть один странный оператор, вот этот — (*.). Очень похоже на умножение, но зачем там ещё точка? Увы и ах, но это результат отсутствия классов типов (traits в Rust) — отдельный оператор умножения чисел с плавающей точкой. То есть, для целых в Ocaml у нас есть обычные +-*/, а для „вещественных“ вот этот ужас: +. -. *. /


Конечно, если нужно писать большое количество формул, то можно просто переопределить операторы +-*/. Но это всё равно неудобно. Увы, такова плата за строгую типизацию без классов типов.


Также без классов типов нет очень важного — простого интерфейса отладочной печати, то есть питоновского „print“. Конечно, есть масса разных реализаций с помощью препроцессоров PPX, но это всё не то. Гораздо проще было бы, позволяй компилятор генерировать функции toString и print. Причём, что очень обидно, компилятор умеет генерировать бинарные представления любых объектов — см. модуль Marshal.


Таким образом, Ocaml всё же неидеальная замена Python. Главных недостатков два: нет классов типов и удобной отладочной печати любых структур.


Экосистема


Экосистема программ вокруг языка сейчас далеко не ограничивается одним компилятором/интерпретатором. Это и поддержка разных операционных систем, редакторов, отладчиков и т.д. Разумеется, поскольку Ocaml — это не язык из первой десятки, можно ожидать, что какие-то части программ экосистемы будут недоделаны, а какие-то вообще отсутствовать. Поэтому пробежимся по ним.


Компиляторы и интерпретаторы


Начнём, разумеется, с главного, с того, что вообще позволяет работать с языком не только в тетрадке — с компиляторов/интерпретаторов. Для Python'а существует масса интерпретаторов, включая интерпретаторы с JIT, все они на слуху, но основной, конечно, это cpython. В Ocaml всё значительно беднее, собственно компилятор и интерпретатор существуют в единственном экземпляре, github:/ocaml/ocaml.git, продукция Научно Исследовательского Института Inria.


Есть разные вариации этого компилятора, подточенные под разные ситуации (фирмы Lexifi, OcamlPRO, Facebook и д.р.). Основные целевые архитектуры компилятора — x86, ARM и POWER. Относительно недавно вместо MIPS добавлена архитектура RISC-V.


Вместе с компилятором в поставке Ocaml идёт интерпретатор байткода, ocamlrun. Скорость интерпретации примерно такая же, как у интерпретатора Python без JIT. Зато, ровно также как и Python интерпретатор Ocaml может работать на всём, на чём можно скомпилировать C-шный код.


Таким образом, компилятор в машинный код у Ocaml есть примерно на тех же плаформах, где у Python есть интерпретаторы с поддержкой JIT. А простой интерпретатор работает более-менее везде, хотя и с ограничениями. Самой-самой-самой основной целевой платформой Ocaml, как для Python'а, является Linux/x86-64. Таким образом, тут мы можем констатировать идеальную замену.


Библиотеки


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


Так для работы с JSON есть yojson, для асинхронного кода — lwt или async, для YML — yml, для тестирования — OUnit/OUnit2/QCheck и т.д. Если чего-то не хватает, но есть Cшная библиотека, можно сделать обёртку для подключения с помощью FFI (Foreign Function Interface), затратив примерно столько же времени, сколько стоит обёртка для Python'а.


Кроме того, есть же JaneStreet, давно работающая с Ocaml в совершенно разных областях с совершенно разными задачами. Поэтому можно воспользоваться библиотеками этой конторы, правда они сильно связаны друг с другом.


Отдельное слово стоит сказать про стандартную библиотеку. Она традиционна весьма бедна и консервативна. Тем не менее, за последнее десятилетие она подросла до более-менее разумного охвата. Разумеется, есть альтернативные стандартные библиотеки, в частности JaneStreet Core, batteries, ExtLib.


Посмотреть на доступные библиотеки можно тут — https://opam.ocaml.org/packages/


Системы сборки


Следующий важный инструмент — это система сборки. И тут, конечно, у Ocaml царит жуткий зоопарк. Складывается ощущение, что система сборки — это, что каждый программист на Ocaml обязан написать. К счастью, на данный момент вроде бы нашёлся „победитель“ — это система Dune. Несмотря на ряд недостатков, обусловленных происхождением из кровавого энтерпрайза с Wall Street, она имеет и ряд достоинств — потрясающую скорость, чудовищную масштабируемость, (в частности умение собирать сразу тучу проектов максимально быстрым образом), постоянные обновления для поддержки всего нового и прогрессивного.


Поэтому в большинстве проектов используется именно Dune, но (помним про кровавый энтерпрайз) с обвязкой в виде простого Makefile для удобства (например https://github.com/ocaml-community/sedlex/blob/master/Makefile ).


У Python, говорят, тоже есть какие-то сборки. Но любим мы его ведь не за это? Поэтому перейдём к следующему акту Марлезонского балета.


Пакетные менеджеры


И у Python, и у Ocaml есть пакетные менеджеры. У Python это pip, а у Ocaml — это OPAM (http://opam.ocaml.org). Оба менеджера предоставляют примерно одинаковые базовые возможности, но pip позволяет создавать пакеты wheel, а OPAM имеет механизм switch (позволяет использовать разные компиляторы с разными наборами установленных пакетов, для проверки под разными версиями экосистем).


Конечно, напрямую механизм wheel копировать в OPAM нет необходимости — можно ведь распространять бинарные скомпилированные программы (с байткодом, увы, это не получится — интерпретатор читает магическое число у программ и не запускает те, что были собраны с компилятором другой версии). Но, надо отметить, что очень не хватает в репозитории OPAM срезов длительной поддержки (для знатоков экосистемы Haskell, OPAM реализует hackage, но не реализует stackage). Отсутствие LTS срезов мешает делать автоматическое пакетирование программ и библиотек Ocaml в дистрибутивах Linux. Будем надеятся, что его всё-таки реализуют.


Линтеры и авто-форматирование, поддержка в IDE и прочее


Тут просто — доведённых до ума линтеров в Ocaml нет, а ocamlformat есть. Поскольку в сообществе особого орднунга нет, в природе наблюдаются несколько разных более-менее стандартных настроек этого ocamlformat. Поскольку язык компилируемый, всё очевидно и просто.


Вопрос с линтерами не закрыт отчасти потому, что они не особо и нужны. С одной стороны, язык компилируемый, поэтому на неиспользуемые переменные укажет компилятор. С другой стороны, язык значительно проще, чем С++, поэтому из предупреждений PVS-studio релевантны для Ocaml ну дай бог 5% (всякое связанное с булевскими операциями и одинаковыми ветками match/if).


Поддержка Ocaml в VSCode есть, причём достаточно хорошая. Дело в том, что ещё до появления стандарта LSP для языка Ocaml были разработаны плагины для Vim и Emacs, реализующие, грубо говоря, Intellisense. Поэтому существующая пара реализаций LSP для Ocaml умеют показывать выведенный тип переменных и выражений, компенсируя тёмную сторону Хиндли-Милнера.


Подсветка синтаксиса Ocaml есть, в целом, везде, где есть, скажем Cшная (кроме Хабра, конечно).


Документация генерируется с помощью двух утилит — старой ocamldoc и модной молодёжной odoc. Они совместимы по языку разметки комментариев, немного напоминая Doxygen, с которого вроде бы все и пошли. Или от чего-то другого?


В минимальной поставке компилятора включены базовые инструменты для генерации парсеров — Ocamllex и Ocamlyacc. Это, творчески сделанные порты lex и yacc (bison). В принципе, если вы по какой-то причине собираетесь сделать парсер для C/С++ на flex/bison, настоятельно рекомендую сделать сперва прототип на Ocamllex/Ocamlyacc, а потом его портировать на пару flex/bison. Такой подход убережёт от огромного количества проблем, т.к. Ocamllex/Ocamlyacc генерируют полностью типизированный код, который проверяется компилятором от и до.


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


Развитие и люди


По сравнению с Python, Ocaml развивается значительно расслабленнее, сдержаннее и совместимее с собой. Программы 90х, использующие только Stdlib, скорее всего придётся совсем совсем слегка подработать напильником, но программы десятилетней давности будут работать как есть. Как правило, место приложения напильника и угол подпилки очевидны из выданных компилятором ошибок.


Неторопливость проявляется, например, в том, что очень интересная оптимизация TRMC (Tail Recursion Modulo Constructor) добралась до кода через 6 (шесть) лет после того, как её предложили. С другой стороны, вот вот уже появится „локальный термояд“ — близко завершение проекта Ocaml Multicore, реализующего „нормальную“ поддержку многопоточности. В общем, движение есть, прогресса… тоже есть.


С людьми в Ocaml всё обстоит сильно иначе, чем в Python'е. Толп нет, но народ присутствует. Как правило, программисты на Ocaml умеют писать на C и догадываются о наличии других языков программирования. То есть, относительно квалифицированы. :-)


В ряде мест Ocaml и родственный ему SML используются для обучения студентов программированию. В принципе, семантика Ocaml значительно интуитивнее, чем семантика Python — ядро языков семейства ML изложено с примерами на буквально 40 страницах „Введения в Стандартный ML“ Р. Харпера. Полная сводка синтаксиса — https://ocamlpro.github.io/ocaml-cheat-sheets/ocaml-lang.pdf


Ocaml считается наиболее удобным языком для построения компиляторов, поэтому он популярен среди компиляторщиков, особенно из Франции. Слово программа в переводе на французский пишется logiciel, и это не просто так: Ocaml широко применяется для разработки разных инструментов верификации, доказательства и т.д.


Другая часть сообщества — это любители Web, уставшие от JavaScript'а. Мне трудно судить, насколько это направление перспективно, но люди есть и что-то делают. Ключевые слова — bucklescript (транспайлер из Ocaml в JavaScript, генерирующий высококачественный код) и ReasonML — альтернативный синтаксис для Ocaml.


Третья часть — это Jane Street, где Ocaml пытаются воткнуть буквально везде. И хотя они открыли достаточно большое кол-во крайне полезного кода, их библиотеки — это немножко „кубик-рубик-монолит“. Просто по структуре и целям „кровавого энтерпрайза“ как-то само собой получается, что все библиотеки стремятся слепиться в единый ком. Разумеется, их можно упорядочить и разделить, но это отдельная деятельность, которой, как правило, занимаются когда уж совсем допекло.


Основное место общения людей из разных областей сейчас — https://discuss.ocaml.org/
и Github.


Основной сайт языка — https://ocaml.org
Сборник документации — https://ocamlverse.github.io/


Заключение


Таким образом, Ocaml вполне подходит для замены Python в разработках программ со сложной бизнес-логикой несмотря на некоторые недостатки.


Решающий плюс — развитая система алгебраических типов данных, включающая GADT и Polymorphic Variants (алгебраические типы без объявления), и отлично проработанный быстрый pattern-matching.


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


Серьёзные минусы Ocaml — отсутствие классов типов вообще и класса Show в частности. Да, есть возможность используя разные PPX реализовать относительно лёгкую отладочную печать, но это неудобно. С моей точки зрения, даже в Haskell нужно вставить обязательную генерацию какого-нибудь DebugShow для всех объектов вообще. А про Ocaml тут и говорить нечего.


Поэтому, конечно, не надо рассматривать эту статью как призыв бежать и менять Python на Ocaml везде и всюду. Да, статическая типизация в больших системах помогает, но есть ситуации, когда нам лучше, чтобы программа даже не была корректной, а уже как-то запускалась. Характерный пример — Wolfram или Jupyter notebooks. Согласитесь, если бы для вычисления какого-то выражения в них требовалась корректность всех ячеек, это была бы просто боль.


Тем не менее, когда разрабатываемая система содержит огромное количество ветвлений, её функциональность нельзя покрыть тестами из-за комбинаторного взрыва. Да и тесты далеко не бесплатны (бесстыдно занимаюсь самоцитированием — https://habr.com/ru/post/560370/ ). И тогда возникает время переходить к статической типизации.


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

Источник: https://habr.com/ru/post/594337/


Интересные статьи

Интересные статьи

При выполнении инженерно-геологических изысканий может возникнуть задача, связанная с сопоставлением данных полевых и лабораторных исследований на одних и тех же грунтах,...
На сайте nalog.ru есть очень удобный сервис, который «покрывает» такие страхи владельца бизнеса как увод компании из под контроля без участия самого владельца. Отчасти естественно «покр...
Привет, Хаброжители! Python — это динамический язык программирования, используемый в самых разных предметных областях. Хотя писать код на Python просто, гораздо сложнее сделать этот код ...
Добрый день уважаемые читатели! Данная статья является продолжением публикации "Повторяем когортный анализ, выполненный в Power BI, силами Python" (ссылка). Настоятельно ...
Заключительная статья из серии как вызывать C/C++ из Python3, перебрал все известные способы как можно это сделать. На этот раз добрался до boost. Что из этого вышло читаем ниже. ...