Coffeescript — мой любимый язык программирования (не совсем)

20.09.2018

…Недавно меня спросили, какой мой любимый язык программирования. Вообще, конечно, этот вопрос некорректный. Языки отличаются по очень многим вещам, и нет какого-то одного идеального языка, у каждого языка есть свои достинства и недостатки.

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

Можно обсуждать отдельные аспекты языков. Например, стандартную бибилиотеку — в одних языках стандартная бибилиотека очень богатая и позволяет делать много чего (привет питону с его import antigravity), в других на многие вещи приходится искать внешние библиотеки (привет c++ с его boost’ом). Можно обсуждать базовые конвенции языка — типизированный он или нет, как работают неявные приведения типов (можно ли складывать число со строкой, что кроме bool’ов можно засунуть в if и т.п.), как пишется основной код программы (на самом верхнем уровне, как в паскале и питоне, или в специальной функции main как в c++, или вообще надо создавать целый класс, как в java), и т.д. Можно обсуждать модель памяти, работу с указателями и ссылками, сборку мусора, обработку ошибок, корутины и т.д.

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

Про сам язык

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

Последние несколько лет я относительно много пишу на Coffeescript — почти весь алгопрог, равно как и его предшественники — сводные таблицы для моего курса — написаны на Coffeescript (вот исходники алгопрога).

Создатели Coffeescript’а представляют его как «Javascript без скобочек», потому что язык Javascript печально известен тем, что программы на нем содержат какое-то огромное количество скобок. И Coffeescript показывает, что на самом деле бОльшую часть этих скобочек можно не писать, лишь слегка изменив синтаксис языка. Но я бы характеризовал Coffeescript еще и как «питон без двоеточий» (в плане синтаксиса, конечно). Пописав на Coffeescript, понимаешь, что двоеточия в питоне не нужны, и вообще, если смотреть поверхностно, то небольшие программы на Coffeescript намного больше похожи на питон, чем на Javascript.

Фичи языка

Так вот, что мне нравится в Coffeescript’е. Многое из того, что я пишу ниже, на самом деле присутствует и в других языках, а зачастую и в современном Javascript (вообще, как я понимаю, современный Javascript взял некоторые фичи из первой версии Coffeescript’а), но я не знаю другого языка, который бы включал все это вместе. (Ну разве что Kotlin, про который я только читал пару популярных статей.)

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

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

Во-первых, вложенность конструкций в Coffeescript задается отступами, так же, как и в питоне:

if a > b
    m = a
else
    m = b

Во-вторых, любая функция в Coffeescript’е является лямбда-функцией, т.е. безымянной функцией, просто при необходимости сохраненной в какую-нибудь переменную:

f = (x) ->
    a = x * 2
    return a + 2

Это функция f, которая принимает один параметр x и возвращает 2*x+2.

Словари и объекты — это одно и то же

Теперь собственно к фичам — в некотором несколько случайном порядке. Я буду регулярно сравнивать с питоном (и иногда с c++), потому что это те языки, на которых я пишу больше всего, и поэтому именно на них я зачастую страдаю от отсутствия Coffeescript’овских вещей.

В Javascript, и как следствие в Coffeescript, словари и объекты — это одно и то же. На самом деле это то, чего мне очень часто не хватает в питоне. Часто хочется сделать по месту какую-нибудь простую структурку, которая хранила бы несколько именованных полей. В питоне можно написать так:

user = {"name": "John", "surname": "Doe"}

но тогда обращаться к полям нужно будет через квадратные скобки и кавычки: user["name"]. А если хочется обращаться как user.name, то надо отдельно создавать объект, скорее всего еще и класс сначала, и получается куча лишнего кода. Хотя, если подумать, то в нетипизированных языках объекты и словари очень похожи друг на друга, и потому не очень понятно, зачем вводить два разных понятния.

Вот в Javascript и, соответственно, в Coffeescript не стали различать эти вещи. Объект и словарь — это одно и то же. Можно писать

# еще обратите внимание на отсутствие кавычек у ключей!
user = {name: "John", surname: "Doe"}  
user.admin = true  # создаем новый ключ
fullname = user.name + " " + user.surname

Равным образом можно создать объект какого-нибудь класса и работать с ним как со словарем:

foo = new Foo()
foo["bar"] = "value"

На мой взгляд, это просто прекрасно. Зачем в питоне придумали две разных сущности, я не понимаю.

В Coffeescript еще, в отличие от Javascript, разрешили избавляться от скобочек следующей записью:

user =
    name: "Василий"
    surname: "Пупкин"
    address:
       country: "Россия"
       city: "Тмутаракань"

Наконец, еще прекрасная вещь в контексте словарей — это если у вас значение ключа уже сохранено в переменной, имя которой совпадает с именем ключа, то можно писать так:

name = "John"
surname = "Doe"
user = {name, surname}
# эквивалентно user = {name: name, surname: surname}

На первый взгляд кажется странно, но на самом деле очень удобно, если вы сначала как-то сложно вычисляете поля объекта, а потом просто собираете все воедино. И очень полезно в destructing assignments, про которые я пишу ниже.

Everything is an expression

Практически любое выражение на Coffeescript имеет некоторое возвращаемое значение. Это обозначает, что в правой стороне оператора присваивания может стоять практически любая синтаксическая конструкция. Например, для просто последовательности команд возвращаемое значение есть возвращаемое значение последней команды, а для оператора if — возвращаемое значение той ветки, которая сработала.

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

Например:

max = if a > b
       a
   else
       b

Эту же конструкцию можно записать и в строку, воспользовавшись необязательным then:

max = if a > b then a else b

Ничего не напоминает? Да, это, конечно, всем знакомый тернарный оператор.

Оцените всю красоту конструкции. В большинстве других языков тернарный оператор — это что-то внешнее и выглядящее довольно неопрятно. В C++ (и много где еще) он записывается через ? и :. Но ведь эта конструкция выглядит весьма странно и не похожа ни на какую другую конструкцию языка! Даже само название “тернарный оператор” отражает не суть, а форму (что у оператора три операнда), явно показывая, что он такой один.

В питоне тернарный оператор выглядит вообще очень некрасиво и нечитаемо:

max = a if a > b else b

Как вообще можно было придумать ставить условие между значениями?! И опять, это очень странная конструкция, такой порядок кода в питоне практически нигде больше не встречается.

А в Coffeescript тернарный оператор — это вовсе не тернарный оператор, а просто тот же if, который вы пишете и в остальном коде. Просто everything is an expression, и поэтому отдельный тернарный оператор вам не нужен.

Еще пример — те же лямбда-функции, про которые я писал выше. На самом деле в них не нужен return (потому что everything is an expression). Функция возведения в квадрат записывается так:

f = (x) -> x * x

И опять, это не какая-то необычная конструкция, специально придуманная для коротких функций — нет, это частный случай совершенно общей конструкции. Можно писать и вот так:

# собственно самая обычная функция, ничего особенного
# сравните с примером функции выше
f = (x) ->
    return x * x
    
# но everything is an expression, поэтому можно писать и так:
f = (x) ->
    x * x
    
# ну и на одной строке при желании:
f = (x) -> x * x

# и пример из начала статьи можно переписать так:
f = (x) ->
    a = x * 2
    a + 2

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

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

В C++ лямбды более разумные, но содержат слишком много скобочек даже по меркам C++, а главное — не позволяют опускать return в простых функциях, делая их в простых ситуациях очень громоздкими:

[](int x){ return x * x; }

Destructing assignments

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

Простейший случай очень банален:

[a, b] = [10, 20]

Это записывает значение 10 в a и 20 в b. Это то же, что есть в питоне в форме a, b = 10, 20, и даже в C++ с появлением std::tie. Естественно, как и в питоне и в c++, переменных может быть больше двух, а в правой части может стоять любое выражение, возвращающее массив, в том числе вызов функции и т.п. (массив, потому что в Coffeescript нет отдельного типа для туплов).

Но в Coffeescript можно то же самое делать и со словарями (объектами)!

{name: a, surname: b} = {name: "John", surname: "Doe"}

эквивалентно

a = "John"
b = "Doe"

Ну или, с учетом сокращенной записи словарей, можно писать так:

{name, surname} = {name: "John", surname: "Doe"}

это эквивалентно

name = "John"
surname = "Doe"

И это все работает на любом уровне вложенности, например, можно так:

{address: {country}} =
    name: "Василий"
    surname: "Пупкин"
    address:
       country: "Россия"
       city: "Тмутаракань"

эквивалентно

country = "Россия"

Это, конечно, очень удобно использовать при возвращении значений из функции. В питоне часто принято возвращать из функции тупл из нескольких значений и использовать запись вида foo, bar = foobar(). То же можно делать и в Coffeescript, но возможность использования словарей делает все намного лучше.

Когда вы просто возвращаете тупл из функции, очень легко запутаться, какой элемент тупла что обозначает. В Coffeescript же вы можете вернуть полноценный объект (словарь), и тут же в месте вызова его разобрать!

Аналогично, очень удобно передавать в функции неопределенное количество именованных аргументов. В питоне для такого принято использовать kwargs, в Javascript и Coffeescript аналога kwargs нет, но можно передавать просто отдельный объект, который зачастую называют options. И в начале функции вы пишете:

foo = (a, b, options) ->
    {option1, option2} = options

сравните это с питоновским

def foo(a, b, **kwargs):
    option1 = kwargs["option1"]
    option2 = kwargs["option2"]

Кстати, аналогичный синтаксис работает и для import’ов.

Многоточия

В примере функции с options, приведенном в предыдущем разделе, остался один интересный вопрос — как обеспечить значения по умолчанию. В Coffeescript для этого есть простой синтаксис, позволяющий “слить” два словаря:

dict1 = {a: 1, b: 2}
dict2 = {b: 3, c: 4}
union = {dict1..., dict2...}

дает

union = {a: 1, b: 3, c: 4}

Соответственно, в примере с опциями можно писать просто

foo = (a, b, options) ->
    {option1, option2} = {defaultOptions..., options...}

(Хотя конкретно для случая с значениями по умолчанию есть более простой синтаксис, поэтому пример на самом деле не очень удачный.)

Аналогичный синтаксис есть и для массивов (с понятными отличиями по смыслу).

Отдельно радует, что многоточие всегда ставится после переменной.

Оператор ?

Прекраснейшая вещь (на самом деле, довольно популярная в современных языках) — запись вида

a?.b

Это обозначает: если переменная a определена, не является null или undefined, то взять a.b, иначе undefined. С учетом того, что доступ к отсутствующему полю словаря дает undefined (т.е. если у объекта a нет поля b, то a.b дает undefined, а не падает), такая запись позволяет легко устраивать цепочки доступов к полям:

if getMyData()?.user?.metadata?.admin

если хоть одно поле отсутствует, а также если admin в итоге равен false, то if не сработает.

Аналогично можно писать ?[...] (ну это естественно, ведь словарь и объект — это одно и то же), и ?(...), — это не переходит по полю, а вызывает функцию.

***

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


Мой курс по алгоритмическому программированию (и подготовке к олимпиадам) для школьников, студентов и всех желающих — algoprog.ru