Coffeescript — мой любимый язык программирования (не совсем)
…Недавно меня спросили, какой мой любимый язык программирования. Вообще, конечно, этот вопрос некорректный. Языки отличаются по очень многим вещам, и нет какого-то одного идеального языка, у каждого языка есть свои достинства и недостатки.
[Этот пост не совсем подходит к тематике этого блога, потому что он не совсем про обучение программированию и не про аккуратное программирование. Но тем не менее я про 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