Clojure, что это
Clojure VS ClojureScript
В этой небольшой книге вы познакомитесь с языком Clojure в лице его диалекта ClojureScript. И, в отличие от пары совершенно разных языков Java и JavaScript, эти два языка по сути являются одним и отличаются только тонкостями интеграции с конкретной платформой: программы на Clojure запускаются на Java Virtual Machine, а программы на ClojureScript, скомпилированные в JavaScript выполняются в Web-браузере (или, к примеру, на NodeJS).
Тот факт, что языки семейства Clojure очень похожи между собой, означает, что и навыки при изучении вы получаете универсальные: если вам вдруг захочется после ClojureScript попробовать пописать на Clojure, вы будете чувствовать себя как дома! И по этой же причине вы, когда увидите в тексте упоминание Clojure, можете считать, что упоминается всё семейство языков. А вот если будет упомянут именно ClojureScript, то это будет означать, что описывается какая-то особенность этой конкретной реализации.
Clojure — это Lisp
Если вы когда-либо интересовались историей компьютерных наук, вы точно встречались с упоминанием семейства языков Lisp. А если даже история вас не слишком интересует, то само название вы наверняка слышали.
Семейство это старое и влиятельное. Сейчас трудно найти языки, авторы которых не вдохновлялись лиспами в той или иной степени. Тот же JavaScript изначально выглядел как ещё одна реализация Scheme, представителя одной из основных ветвей семейства лиспоподобных языков. И пусть в итоге JavaScript оказался синтаксически похож на Java, но внутри это до сих пор Lisp в овечьей шкуре.
Clojure визуально очень похож на другие лиспы, хотя ни в какие другие семейства не входит. Clojure, как всякий уважающий себя Lisp, имеет
- функции как сущности первого класса
- упрощённый синтаксис, пригодный для программного манипулирования
- систему метапрограммирования, также известную как "макросы"
Особенности Clojure
Автор Clojure Рич Хикки, создавая этот язык, выбрал для себя главной задачей борьбу со сложностью. В его понимании наиболее сложными концепциями — сложными для восприятия человеком и удержания в голове при работе с кодом — являются изменяемое состояние и одновременное исполнение нескольких подпрограмм. Кроме того в язык были заложены богатые средства для работы с данными и инструменты для взаимодействия с платформой. Рассмотрим же все эти составляющие "образа языка".
Изменяемые данные
Чтобы программист реже сталкивался с изменяемым состоянием, в мире Clojure в качестве основного подхода для решения задач используется функциональное программирование. Этот подход характерен использованием неизменяемых структур данных и чистых функций — таких функций, которые при вычислении результата не влияют на другие части программы и сами не зависят от того, в каком состоянии сейчас находится система.
Неизменяемые структуры данных подразумевают то, что когда вы "меняете" что-то в такой структуре, вы не затрагиваете оригинал, а вместо этого получаете новую структуру, содержащую нужные изменения. Чтобы такое поведение не приводило к ненужному перерасходу ресурсов машины, используются специальные персистентные неизменяемые структуры данных, которые в новых версиях данных используют повторно те части структуры, которые не менялись. Clojure, как типичный язык для "в первую очередь функционального программирования", поддерживает такие структуры и умеет работать с ними эффективно.
Одновременное исполнение подпрограмм
Сложность разработки программ, части которых работают одновременно, часто тоже связана с изменяемыми данными. Когда изменяемые данные доступны сразу нескольким потокам (они же сопрограммы, то есть программы, работающие сообща) и в любой момент времени могут быть изменены любым из них, часто практически невозможно предсказать то, как будет себя вести система в целом. Обычно программисты применяют специальные примитивы синхронизации, таких как semaphores и locks, чтобы управлять тем, какая из сопрограмм модифицирует данные в какой момент времени — и получают специфические для такого подхода проблемы вроде блокировок (dead locks) или состояния гонки (race condition).
Clojure наступает на класс проблем, связанных с многопоточностью, с двух фронтов:
- неизменяемые структуры данных позволяют сопрограмме не бояться за сохранность данных, пока она с этими данными работает;
- высокоуровневые средства позволяют и вовсе обойтись без передачи нескольким сопрограммам данных, доступных для прямого изменения. Вместо этого вы связываете взаимодействующие сопрограммы, к примеру, каналом, который доставляет сообщения от одной стороны к другой.
Работа с данными
Лучше иметь 100 функций, работающих с одной структурой данных, чем иметь 10 функций, работающих с 10 структурами данных.
Алан Перлис так писал в своих "Эпиграммах о программировании" (1982). Эта цитата очень хорошо характеризует тот факт, что в языках семейства Lisp (название расшифровывается как "List Processor"!) принято на всё смотреть через призму списков. Список — великий уравнитель, и всё есть список, даже сам код на Lisp. Любые другие структуры данных реализуются с помощью односвязных списков, для работы с которыми лиспы обычно имеют богатейший набор инструментов.
С одной стороны наличие универсальной структуры "под капотом" у большинства других структур позволяет легко писать код, преобразующий одни данные в другие. А нужный уровень абстракции достигается предоставлением функций, при использовании которых вам не нужно знать, что внутри у данных списки списков списков. С другой стороны, как показывает многолетняя практика, некоторый набор структур общего назначения нужен настолько часто, что неплохо бы иметь эти структуры уже реализованными в языке и снабжёнными нужным количеством инструментов для работы с ними.
Получается, что полезно иметь "штук пять структур с сотней функций для каждой"! Недаром говорят:
если нужно оставить только одну структуру данных, выбирай hash map
Вот и Рич Хикки в дополнение к списку заложил в Clojure такие структуры как отображение (map), множество (set) и вектор (vector) и стандартная библиотека языка обладает множеством инструментов для работы с ними. И даже сам код на Clojure строится с использованием этих структур, а не одних лишь круглых скобок как в классических lisp.
Взаимодействие с платформой
Clojure изначально создавался для запуска на Java Virtual Machine во многом потому, что платформа эта существует давно, а значит хорошо оптимизирована и "обросла" изрядным количеством библиотек, написанных для неё на Java и других языках, компилируемых в тот же формат байткода. Возможность легко подключить и использовать в своём коде сторонние библиотеки как правило требует от языка соответствующей поддержки.
Так как JVM исполняет код, который живёт в классах и оперирует объектами этих классов, то работать с классами нужно и со стороны Clojure/JVM. Более того, иногда нужно представить код, написанный на Clojure/JVM, в виде тех самых классов так, чтобы внешний код мог их использовать. Такая интеграция с платформой, работающая в обе стороны, называется interoperability или сокращённо interop.
У Clojure/JVM имеется хороший interop с JVM, а у ClojureScript — хороший interop с Browser API и NodeJS API. Благодаря тому, что использовать interoperability достаточно просто, многие популярные "не родные" библиотеки силами сообщества уже снабжены соответствующими "обёртками" и вам погружаться в interop не нужно.