На каком языке программирования я/мы буду/будем писать завтра?
31 июля 2019
У всех у нас (программистов) есть какой-то набор языков программирования, которые нам нравятся, и которые мы применяем. Конечно, этот набор со временем меняется.
Кому-то интересно запускать бизнесы, кому-то интересно выпускать продукты, а кому-то интересны сами по себе технологии, как таковые (и языки программирования, как частный случай). Постоянно хочется найти, изучить и применить что-то, что лучше и интереснее.
Что мне нравится
Мне нравится функциональное программирование, потому что оно проще, а я люблю простые вещи. Но ФП бывает разное. Есть два больших семейства языков: семейство LISP и семейство ML.
Я попробовал много языков, и у меня сложились определенные предпочтения — больше всего мне нравятся языки семейства ML. Не скажу, что это продуманный, рациональный и глубоко обоснованный выбор, скорее дело вкуса. Хотя при желании его можно рационально обосновать. Концепции, семантика этих языков мне кажутся красивыми и элегантными. (Синтаксис не всегда таким кажется, особенно если речь идет про Scala, но синтаксис вторичен).
В свое время я инвестировал довольно много времени и сил в изучение многих их них. Это было примерно так:
- Haskell
- Standard ML
- еще раз Haskell
- Scala
- еще раз Haskell
- OCaml
- еще раз Haskell
- еще раз Scala
- Elm
- F#
- еще раз OCaml
То есть, я попробовал всех основных игроков: Haskell, Scala, OCaml, F#, и сделал по несколько подходов к каждому из них.
Тут можно было бы порассуждать о каждом языке отдельно. Но это слишком большая тема. Может быть, как-нибудь в другой раз. Сейчас хочется поговорить о всем семействе в целом.
Что такое ML?
Семейство можно определить по происхождению от общего родителя — языка ML (Meta Language), разработаного Робином Милнером в далеком 1973 году. Несмотря на академическое происхождение, сам язык ML и его потомки оказались практичными и удобными, за счет некоторого небольшого набора концепций, лежащих в его основе.
Immutable Programming
Неизменяемые структуры данных уменьшают возможности для появления ошибок. Тем самым урощают жизнь программисту, позволяют ему меньше думать о том, где и как могут возникнуть ошибки.
Они же упрощают многопоточное программирование, исключая ситуации race condition, когда два разных потока одновременно модифицируют некие данные.
Обратная сторона -- операции над этими данными менее производительные, чем операции над мутабельными данными. Считается, что разница в производительности в среднем не такая большая. Но это "в среднем", а в крайних случаях производительность может отличаться сильно.
Полезность концепции подтверждает ее активное проникновение из ФП в мейнстримовые языки.
First-Class Functions (High Order Functions, HOF)
Функции высшего порядка — важный способ абстрагирования кода, лежаший в основе работы с любыми коллекциями (списками, массивами, векторами и т.д.). Они меняют сам подход к такой работе, в сравнении с процедурным программированием.
Циклы и доступ к элементам коллекции по индексам — это операции низкого уровня. Вместо них предлагаются более высокоуровневые операции — map, filter, fold и др. Это тоже уменьшает возможности для появления ошибок.
HOF так же активно проникают из ФП в мейнстримовые языки.
Static Type-Checking
Статическая типизация — это то, что отличает ML от LISP. Большинство ML языков имеют статическую типизацию, а все LISP языки динамически типизированы.
Останавливаться на преимуществах и недостатках того и другого сейчас не будем.
Automatic Type Inference
Автоматический вывод типов — важное дополнение к статической типизации, которое сильно упрощает жизнь программисту. Нет необходимости везде и всюду явно указывать типы, так как во многих случаях компилятор может сам вывести нужный тип.
Это еще одна концепция, которая востребована в мейнстримовых языках.
Algebraic Data Types
Алгебраические типы данных — основной способ моделирования данных. Простой и гибкий, как и все в ФП. Простой, в сравнении с ООП, конечно.
Мейнстримовые языки не особо стремятся это заимствовать. Во-первых, там уже есть ООП, и еще одна штука сбоку просто не нужна. Во-вторых, сами по себе ADT, без Pattern Matching, не так полезны.
Pattern Matching
Киллер-фича ФП. Реализует разбор сложных структур данных на составные части и условные переходы. Обычно делает и то, и другое одновременно, что дает лаконичный и выразительный код.
К сожалению, в мейнстримовые языки проникает с трудом и в урезаном виде.
Parametric Polymorphism
Достигается во многом за счет описаных выше фич: HOF, ADT и PM.
В языке, который я выберу, мне нужны все эти фичи. Поэтому меня не тянет в мейнстримовые языки. Immutable Programming и High Order Function туда проникли, Static Type Checking и Automatic Type Inference там и так были, а вот с Algebraic Data Type и Pattern Matching дела обстоят неважно. Обойтись без ADT и PM я, конечно, могу. Но зачем? Отказаться от удобных фич, чтобы что?
Что у меня есть сейчас
Erlang хорош. Простой, не дает мощных абстракций, а просто делают свою работу.
Да, это ML язык. Но при том, что он поддерживает многие фичи ML, на самом деле Erlang не про ML, и вообще не про ФП. Не про моделирование сложных данных, и не про композиции операций над данными.
Erlang про то, чтобы эффективно направить потоки данных откуда-то куда-то. Это язык водопроводчиков, он про трубы и бассейны, как в школьных задачах: сюда затекает столько-то, отсюда вытекает столько-то, смотри, чтобы бассейн не переполнился. Вернее, у тебя десятки бассейнов и сотни труб, разложены на пять нод (а у кого-то на 50, или на 500 нод). Но суть та же — все должно быстро течь, не засоряться, и не переполняться.
Это нишевый язык под узкие задачи.
Все это интересно, и я с удовольствием занимаюсь этим уже лет 7. Но когда-нибудь мне надоест быть водопроводчиком :) Еще через 7 лет Erlang вряд ли будет моим основным языком.
Elixir лучше и мощнее. Это, очевидно, большой шаг вперед. Много фич, которые действительно улучшают жизнь разработчику, в сравнении с аскетичным Erlang.
Но кое-что мне не нравится. Да, формально Elixir является ФП языком и имеет фичи ML языков. Но авторы языка сделали все как-то не так.
Во-первых, потому, что авторам чужды дух и эстетика ML. (Операции с коллекциями в модуле Enum? Серьезно? Enum -- это не коллекция. Оператор pipe подставляет первый аргумент, а не последний? Да, я понимаю, что авторы не осилили каррирование, но они и не хотели).
Во-вторых, потому, что лучше сделать авторы не могли из-за ограничений своего дизайна. Очевидно, что дизайн языка не был продуман изначально, а сформировался случайным образом.
Сфера применения Elixir шире. Но я не уверен, что его можно считать языком общего назначения. Все-таки, это тоже специализированый язык под конкретные области применения.
Еще у меня есть Python — самый, что ни на есть, мейнстрим. Стабильно в десятке популярных языков по различным рейтингам. Область применения широчайшая. Годится для чего угодно, от мелких скриптов до масштабных проектов.
Однако, мое примерение Python, это больше про мелкие скрипты, чем про масштабные проекты :) Тут задеплоить, тут автоматизировать, тут данные переложить из одной БД в другую. По-мелочи он постоянно нужен, но ничего большого и серьезного я на нем не писал.
Стоит, конечно, изучить язык поглубже. Однако, полноценным Python-разработчиком я быть не хочу. Кажется, что это шаг назад. (Но своего сына я буду учить в первую очередь Python).
На чем я буду писать завтра?
Rust имеет все фичи, присущие семейству ML. Вот всё, что выше перечислено — всё есть. Именно поэтому я обратил внимание на Rust, а не из-за его основных фишек, через которые он рекламируется и продвигается (безопасная работа с памятью и эффективность).
Я попробовал и убедился, что типичный ML код, с ADT, PM, мапами и свертками нормально пишется и вполне канонично выглядит. Да, там move, borrow и lifetime добавляют трудностей. Но трудностей то мы не боимся, если есть ради чего стараться :)
Rust объединяет два мира: ФП программирование и системное программирование.
Где-то рядом с нами, программистами, избалованными GC, обитают суровые мужыки, пишущие код на суровом C и суровом C++. Они знают тайны устройства памяти, управляются с указателями, и умеют писать максимально эффективный код, выжимающий всю возможную производительность из железа.
Я всегда им завидовал. Я прочитал Кернигана и Ричи, прошел 3 курса по C++ на stepic.org, и тоже что-то понял про память и указатели. А еще я понял, что совсем не хочу писать на C++. Зато вот есть Rust, с которым тоже можно погрузиться в этот мир. Вряд ли я буду писать ядро ОС, драйвер сети или базу данных, но и в прикладном программировании эффективный код не лишний.
Итого, два мира в одном языке:
- ФП, такое, как я люблю;
- и системное программирование, такое, которому я всегда завидовал.
Стоит попробовать :)
Ссылки
- Самая лучшая книга по ФП (не важно, что там F#):
Domain Modeling Made Functional. Tackle Software Complexity with Domain-Driven Design and F# by Scott Wlaschin - Rust: A Language for the Next 40 Years - Carol Nichols
- The Rust Programming Language by Steve Klabnik and Carol Nichols, with contributions from the Rust Community