В мире разработки программного обеспечения редко можно увидеть, как зрелый, динамический язык добавляет мощную, статическую систему типов без ущерба для своей философии и существующего кода. 3 июня 2026 года команда Elixir сделала именно это, выпустив версию 1.20. Главное нововведение — постепенная типизация (gradual typing), которая обещает найти в вашем коде «верифицированные баги» (verified bugs — ошибки, гарантированно ведущие к падению в рантайме) без необходимости писать аннотации типов.

Но почему это должно волновать вас, если вы пишете на Python, Java или Go? Команда Elixir поставила перед собой цель, которую многие считают «святым граалем». На практике сочетание всех трёх свойств почти не встречается.

И я считаю, что это отличный повод присмотреться к Elixir, даже если вы никогда не писали на нём ни строчки. Молодой, но зрелый язык, работающий на виртуальной машине Erlang (BEAM) и исповедующий функциональную парадигму. Он известен своей отказоустойчивостью и конкурентностью, но, увы, остается малоизвестным языком.

Оригинальная статья доступна по ссылке.


❯ Динамика или статика?

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

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

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

Статические языки, напротив, предлагают совершенно иной подход. Python, C/C++, Rust, C#, Java и им подобные переносят ответственность за корректность типов на плечи компилятора. Типы в таких языках становятся неким контрактом между разработчиком и языком. Преимущества ощутимые: ошибки в 99% обнаруживаются на этапе компиляции, код становится самодокументируемым (и не нужно догадываться, переменная amount — это int, float или особый Decimal-тип).

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

Разработчики пытались найти ту самую «золотую середину», которая сочетала бы быстрый фидбек от динамической типизации и гарантии статических. Постепенная типизация (gradual typing) казалась идеальным решением: можно аннотировать проект постепенно, получая возможность поначалу быстро прототипировать, а по мере созревания кода добавлять типы.

На практике, конечно, подход сталкивается с серьезными трудностями. Постепенная типизация не гарантирует непротиворечивость (soundness), то есть, если компилятор пропустил код без ошибок типов, не факт, что в рантайме их не будет. А если попытаться одновременно систему типов сделать и непротиворечивой, и постепенной, то теряется третья часть — developer-friendly, аннотации типов могут быть ужасно некрасивыми и запутанными.

И как раз таки в этом контексте Elixir 1.20 выглядит особо примечательным. Команда разработчиков языка заявила (причем они работали над этим уже не первый релиз), что нашла способ решить эту трилемму, предложив подход, который позволит соблюсти все три, беря от них по чуть-чуть. Но прежде чем разбираться в том, как им это удалось, стоит понять, что именно они подразумевают под «святым граалем» типизации и почему сочетание трёх его свойств — непротиворечивости, постепенности и дружественности к разработчику — считается столь сложной задачей.

❯ Трилемма святого грааля

Прежде чем говорить о том, что именно сделала команда Elixir, важно понять, что вообще они пытались решить. В теории систем типов существует трилемма — возможно ли получить систему типов, которая владеет тремя свойствами одновременно: soundness, gradual typing и developer-friendly. Каждое свойство по отдельности не так сложно реализовать, но совместить их в одном языке является крайне нетривиальной задачей.

Вообще, эта трилемма тянется ещё с работ Джереми Сиека, который в 2006 году ввёл понятие gradual typing (постепенная типизация). Он предложил идею постепенной типизации как способ перехода от динамических языков к статическим без резкой смены парадигмы. Однако на практике оказалось, что постепенность — это лишь одно из трёх желаемых свойств, и её достижение часто идёт в ущерб другим. Постепенная типизация позволяет разработчику смешивать статически и динамически типизированный код в рамках одного проекта. В идеале вы можете начать проект без аннотаций типов, быстро прототипируя функциональность, а затем, по мере стабилизации кода, добавлять типы туда, где это даёт наибольшую пользу. При этом переход от динамически типизированных участков к статическим должен быть плавным, без необходимости переписывать уже работающий код. Именно эту идею и предложил Сиек.

Второе свойство — soundness, созвучность, непротиворечивость. Если компилятор вывел или принял некий тип для переменной или выражения, то в рантайме эта переменная или выражение действительно будут иметь значение этого типа. Проще говоря, когда вы передаете числа в функцию, которая ожидает число, и компилятор это проверил, программа не упадёт с ошибкой несоответствия типов. Это свойство дает твердые гарантии, что в продакшене не будет ошибок типов, если тайпчекер не выдал ошибок.

Третье свойство — это developer friendly, его сложно конкретно формализовать, но оно критически важно для принятия системы типов обществом. Дружественная система типов не требует от разработчика писать многословные аннотации для каждой переменной, не заставляет изучать сложную теорию типов и выдаёт понятные, конкретные сообщения об ошибках.

Иногда третьим свойством называют No Wrappers, который заключается в том, что система не должна вставлять рантайм-проверки на границах динамической и статической типизацией. Но, как и команда Elixir, я считаю, что на роль третьего свойства лучше подходит developer friendly, так как мы должны смотреть на язык со стороны программиста, а не со стороны математика-оптимизатора. Оптимизация, безусловно, важна, но без удобства мало кто будет ей пользоваться.

Но эти три свойства очень тяжело сочетать вместе. Soundness и gradual typing порождают сложный синтаксис, полный проверок типов, так как в типизированную функцию может прийти нетипизированные объекты. Developer friendly и soundness заставляют отказаться от gradual typing, так как требуется полная типизация всего для созвучности и дружелюбности. А developer friendly и gradual typing делают код противоречивым. Можно использовать any, постепенно типизировать, но отсутствие ошибок в рантайме не гарантируется.

Соединение всех трех свойств обычно называют «Святым Граалем» системы типов.

Треугольник трилеммы системы типов
Треугольник трилеммы системы типов

❯ Решение Elixir: dynamic() вместо any()

Итак, мы подошли к главному вопросу: как именно команда Elixir предлагает решить трилемму, которая оставалась открытой почти два десятилетия? Ответ кроется в оригинальной интерпретации постепенной типизации и, в частности, в особом типе dynamic(), который принципиально отличается от аналогичных механизмов в других языках.

В большинстве языков с постепенной типизацией существует специальный тип, который сигнализирует системе: «эту часть кода не проверяй, я знаю, что делаю». В TypeScript это any(), в Python — Any, в других языках он может называться иначе. Проблема в том, что такой тип фактически выключает проверки, и система типов перестаёт быть звуковой — вы можете передать куда угодно что угодно, и компилятор промолчит.

В Elixir динамический тип называется dynamic(), но его семантика принципиально иная. Это указание на то, что точный тип будет проверяться непосредственно в рантайме, а не обычный any. При этом компилятор не отбрасывает информацию, а, наоборот, отслеживает диапазон возможных типов, которые может принимать значение. Иными словами, если у переменной тип dynamic(integer() or binary()), это значит, что в рантайме она может быть либо целым числом, либо бинарной строкой, и компилятор будет использовать это знание для проверки всех операций с ней.

Совместимость вместо слепого доверия

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

Рассмотрим простой пример. Предположим, у нас есть переменная value_or_error, имеющая тип dynamic(integer() or binary()). Если мы передадим её в функцию, ожидающую число, например, в оператор деления, компилятор проверит: может ли dynamic(integer() or binary()) быть числом? Да, может, потому что integer() входит в диапазон. Ошибки не будет.

Но если мы попытаемся передать ту же переменную в функцию Map.fetch!/2, которая ожидает объект типа Map, компилятор увидит, что диапазон integer() or binary() и тип map() не пересекаются. В рантайме это гарантированно приведёт к падению, и система выдаст предупреждение. Этот механизм называется совместимостью (compatibility) — система проверяет, входит ли объект в диапазон типов.

Сужение (narrowing): когда компилятор становится умнее

Однако одной совместимости было бы недостаточно для поиска реальных ошибок. Если бы система просто проверяла непересекающиеся диапазоны, многие баги остались бы незамеченными. Здесь как раз и вступает второй ключевой механизм — narrowing, или сужение типов.

Elixir анализирует поток выполнения программы и постепенно уточняет тип переменной по мере того, как она используется. Например, если вы написали условную конструкцию if is_integer(value), то внутри ветки true компилятор будет считать, что value имеет тип integer(), а не dynamic(). Ветка false получит противоположный вывод.

Аналогично работает и с другими конструкциями. В операторе case каждая ветка сужает тип на основе сопоставления с образцом. Если вы пишете:

case System.get_env("PORT") do
  nil -> :not_found
  value -> {:ok, String.to_integer(value)}
end

то компилятор знает, что System.get_env/1 возвращает либо nil, либо бинарную строку. В ветке value -> тип переменной value сужается до binary(), и вызов String.to_integer/1 проходит проверку без ошибок.

Ещё более впечатляюще сужение работает с составными структурами данных. Рассмотрим такой код:

def add_a_and_b(data) do
  data.a + data.b
end

На первый взгляд, data имеет динамический тип. Но компилятор видит обращения к полям :a и :b и оператор сложения. Из этого он выводит, что data должна быть map, содержащей ключи :a и :b, значения которых являются числами. Тип data сужается до %{..., a: number(), b: number()}. Если вызовете эту функцию с map, в которой нет ключа :a, компилятор выдаст ошибку ещё до запуска программы.

Никаких ложных срабатываний

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

Это кардинально отличает Elixir от других языков с постепенной типизацией. В TypeScript использование any() часто воспринимается как «костыль» — вы говорите компилятору замолчать, потому что не можете или не хотите правильно описать типы. В Elixir dynamic() дает компилятору диапазон типов данных. В результате вы получаете созвучную систему типов даже на тех участках кода, где типы не аннотированы явно.

И самое примечательное: всё это работает на существующем коде без единой аннотации типа. Вы просто обновляете версию Elixir, и компилятор начинает находить ошибки, о которых вы могли не подозревать. Именно это сочетание и делает подход Elixir уникальным в решении трилеммы святого грааля. Команда не пытается охватить все три свойства полностью, они идут на компромиссы.

❯ Бесплатная типизация: и помните, ни слова об аннотациях

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

Как именно это работает? Всё дело в том, что Elixir уже содержит множество конструкций, которые неявно несут информацию о типах. Компилятор научился извлекать эту информацию и использовать её для проверки корректности программы.

Guard-выражения как источник типов

В Elixir есть механизм guard-выражений — это условия, которые записываются в заголовке функции после ключевого слова when. Они используются для ограничения областей применения функции и для сопоставления с образцом. Например:

def example(x, y) when is_list(x) and is_integer(y) do
  # тело функции
end

Раньше компилятор просто проверял синтаксическую корректность guard-выражений. Теперь он понимает их семантику: в примере выше он выводит, что переменная x внутри тела функции имеет тип list(), а y — тип integer(). Это позволяет проверять все операции внутри тела на корректность с учётом этих типов.

Guard-выражения могут быть довольно сложными и содержать логические операции. Elixir корректно обрабатывает объединения, пересечения и отрицания типов. Например:

def example({:ok, x} = y) when is_binary(x) or is_integer(x) do
  # ...
end

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

Особенно интересен случай с проверкой наличия ключа в map’е:

def example(x) when is_map_key(x, :foo) do
  x.foo
end

Компилятор понимает, что если выполнено условие is_map_key(x, :foo), то внутри тела функции x гарантированно содержит ключ :foo. Обращение к x.foo будет признано безопасным. И наоборот:

def example(x) when not is_map_key(x, :foo) do
  x.foo
end

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

Сужение в case и условных конструкциях

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

case System.get_env("SOME_VAR") do
  nil -> :not_found
  value -> {:ok, String.upcase(value)}
end

Функция System.get_env/1 возвращает либо nil (если переменная окружения не задана), либо бинарную строку. В первой ветке мы сопоставляем с nil. Компилятор делает логический вывод: раз первая ветка обработала случай nil, то во второй ветке value точно не может быть nil. Значит, это бинарная строка. Вызов String.upcase/1, ожидающий строку, проходит проверку без ошибок.

Этот же механизм работает и для других конструкций — if, cond, а также для сопоставления с образцом в аргументах функций.

Обнаружение мёртвого кода

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

def process(value) when is_integer(value) do
  if value > 0 do
    # что-то делаем с положительным числом
  else
    # обрабатываем отрицательное или ноль
  end
end

Если в другом месте программы компилятор может вывести, что process/1 всегда вызывается только с положительными числами, то ветка else становится ненужной.


Главное чудо — вы получаете преимущества статического анализа без необходимости менять стиль программирования или дописывать аннотации. Это особенно ценно для уже существующих проектов: не нужно тратить месяцы на добавление сигнатур типов, чтобы начать ловить ошибки. Достаточно обновиться до Elixir 1.20, и компилятор начнёт находить проблемы там, где их раньше не видел.

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

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

❯ И что это дает на практике?

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

Рефакторинг становится безопаснее

Одно из самых очевидных применений новой системы типов — это рефакторинг. Раньше, изменяя сигнатуру функции или структуру данных в Elixir, вы полагались на тесты и собственную внимательность. Теперь компилятор стал дополнительным слоем защиты.

Представьте, что вы меняете формат данных, которые передаются между модулями. В одном месте вы обновили код, а в другом забыли — компилятор подсветит это как ошибку ещё до того, как вы запустите тесты. Это особенно важно в больших кодовых базах, где невозможно держать в голове все зависимости между компонентами.

Облегчение входа в проект

Для новичков, которые приходят в Elixir-проект, новая система типов тоже даёт преимущества. Когда вы не знаете, что именно ожидает функция, типы становятся подсказкой. IDE, использующая информацию от компилятора, может показывать ожидаемые типы аргументов и возвращаемых значений, даже если разработчик не написал ни одной аннотации.

Это не отменяет необходимости читать документацию, но снижает когнитивную нагрузку на начальных этапах. Вы видите, что функция String.upcase/1 ожидает строку, потому что компилятор вывел это из guard-выражений, а не потому, что кто-то написал об этом в комментарии.

Уверенность в продакшене

Главное обещание Elixir 1.20 — это поиск «верифицированных багов», то есть ошибок, которые гарантированно проявятся в рантайме. На практике это означает, что часть проблем, которые раньше находили только в процессе тестирования или даже в продакшене, теперь обнаруживается на этапе компиляции.

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

Сравнение с другими языками

Чтобы оценить уникальность подхода Elixir, полезно сравнить его с тем, как работают системы типов в других популярных языках.

В TypeScript есть тип any, который отключает все проверки. Разработчики часто используют его как «костыль» в сложных местах, и это нормальная практика. Но цена этого — потеря soundness. Вы можете написать const x: any = 42; x.someMethod(), и компилятор промолчит, хотя в рантайме будет ошибка. В Elixir dynamic() не отключает проверки, а сужает диапазон возможных типов, поэтому такие ситуации невозможны.

В Go типизация статическая, но в местах, где тип неизвестен, используется interface{}. Чтобы работать с таким значением, нужно делать type assertion вручную, и если вы ошиблись — программа упадёт в рантайме. В Elixir компилятор делает часть этой работы за вас, анализируя, как используется переменная.

В Python с mypy вы получаете постепенную типизацию, но для этого нужно устанавливать отдельный инструмент, настраивать его и писать аннотации. В Elixir всё работает из коробки, без настройки и без аннотаций. Правда, стоит отметить, что mypy активно развивается и сообщество Python тоже движется в сторону улучшения типизации, но на данный момент подход Elixir выглядит более бесшовным.

Компиляция стала быстрее

Помимо типизации, в Elixir 1.20 есть ещё одно заметное улучшение, которое может заинтересовать перфомансофагов. В компилятор добавлена опция :module_definition, которая позволяет указать, должен ли модуль компилироваться или должен интерпретироваться во время компиляции проекта.

На практике это означает, что на машинах с большим количеством ядер компиляция может ускориться. Разработчики сообщают, что в синтетических бенчмарках сборщик проектов Elixir (mix) стал быстрее, чем у других BEAM-языков. Это не революция, но приятный бонус, особенно для CI/CD, где каждая секунда имеет значение.

https://github.com/josevalim/langcompilebench
https://github.com/josevalim/langcompilebench

❯ Заключение

Elixir 1.20 показал нам возможность смены подхода к тому, как мы думаем о типизации в динамических языках. Команда разработчиков во главе с Жозе Валимом, создателем Elixir и одним из ключевых участников сообщества Ruby, не понаслышке знают о проблемах динамических языков в больших проектах. Их опыт работы над Rails и Ruby, а затем над Elixir, позволил взглянуть на проблему типизации с неожиданной стороны. Вместо того чтобы заставлять разработчиков выбирать между гибкостью и надёжностью, они предложили систему, которая даёт и то, и другое, не требуя компромиссов на ранних этапах проекта.

Функциональная природа Elixir сыграла здесь важную роль. В отличие от императивных языков, где типизация часто усложняется из-за мутабельного состояния и побочных эффектов, функциональный подход с неизменяемыми данными и чёткими границами между функциями оказался более предсказуемым для вывода типов. Система типов, построенная на множествах (set-theoretic types), естественным образом вписалась в эту парадигму, позволяя компилятору анализировать код глубже и лучше, чем это возможно в большинстве других языков.

Для тех, кто уже пишет на Elixir, изменения означают одно: ваш код стал надёжнее, а вы об этом даже не просили. Для тех, кто только присматривается к языку, это повод сделать следующий шаг. Конкурентность, отказоустойчивость и теперь ещё и продуманная типизация делают Elixir серьёзным кандидатом для проектов, где важны и скорость разработки, и стабильность в продакшене. И если вы искали язык, в котором функциональная парадигма сочетается с прагматизмом и современным подходом к типизации, — теперь у вас есть ещё одна причина попробовать его.

И да, напоследок: впереди у команды ещё три амбициозные задачи, которые нужно решить, прежде чем мы увидим в Elixir явные сигнатуры типов. Это эффективная работа с рекурсивными типами, параметрическими типами и обход пар ключ-значение в map’ах. Как только эти вызовы будут приняты — а команда уже вовсю над ними работает, — можно будет говорить о следующем этапе эволюции типизации. А пока — наслаждаемся тем, что уже есть, и ловим баги бесплатно.

И да, ещё — в Elixir есть макросы, а значит, система типов должна работать и с ними. Это отдельный вызов, и команда обещает вернуться к этому вопросу в следующих релизах.

Если захотите начать изучать Elixir, то у него есть отличные ресурсы для обучения:


Если эта статья хорошо зайдет, я могу написать больше статей об Elixir, например о Railway Oriented Programming, принципе let-it-crash или обзор экосистемы языка Elixir.

Официальная статья о релизе Elixir


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

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


  1. k12th
    13.07.2026 10:00

    Например, если вы написали условную конструкцию if is_integer(value), то внутри ветки true компилятор будет считать, что value имеет тип integer(), а не dynamic(). Ветка false получит противоположный вывод.

    В TS то же самое, кажется.


    1. iWowik
      13.07.2026 10:00

      | dynamic(integer() or binary())

      в TS можно перечислить допустимые типы, а не только any влепить
      type SettingValue = number | string | boolean;
      даже перечислить допустимые значения
      type Values = 'ok'|'error'|true|false|0|1;


  1. Format-X22
    13.07.2026 10:00

    В какой-то момент создатель Ruby доделает свой новый проект https://github.com/matz/spinel и Elixir с Crystal станут под вопросом о нужности. Elixir функциональный более, так что ещё не факт что совсем будет не нужен, но всё же все плюшки с типизацией, в том числе вывод типа по факту использования, а ещё и компиляция в натив - это ультимативные фичи. Посмотрим к чему приведёт, там есть нюансы с мета-программированием и поэтому Rails из коробки совсем не едет, да и оно только-только пошло в работу. Но через пару лет Ruby Spinel может съесть всех последователей как когда-то JS ES6 и последующий TypeScript съели CoffeeScript, Flow и JSDoc.