«1. Введение

Clojure — это функциональный язык программирования, полностью работающий на виртуальной машине Java, аналогично Scala и Kotlin. Clojure считается производным от Lisp и будет знаком всем, кто имеет опыт работы с другими языками Lisp.

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

2. Установка Clojure

Clojure доступен в виде установщиков и удобных сценариев для использования в Linux и macOS. К сожалению, на данном этапе в Windows такого установщика нет.

Однако сценарии Linux могут работать в чем-то вроде Cygwin или Windows Bash. Существует также онлайн-сервис, который можно использовать для тестирования языка, а в более старых версиях есть автономная версия, которую можно использовать.

2.1. Автономная загрузка

Автономный файл JAR можно загрузить с Maven Central. К сожалению, версии новее 1.8.0 больше не работают таким образом, поскольку файл JAR разбит на более мелкие модули.

Как только этот файл JAR загружен, мы можем использовать его как интерактивный REPL, просто рассматривая его как исполняемый файл JAR:

$ java -jar clojure-1.8.0.jar
Clojure 1.8.0
user=>

2.2. Веб-интерфейс к REPL

Веб-интерфейс к Clojure REPL доступен по адресу https://repl.it/languages/clojure, чтобы мы могли попробовать, не загружая ничего. В настоящее время это поддерживает только Clojure 1.8.0, а не более новые версии.

2.3. Установщик на MacOS

Если вы используете macOS и у вас установлен Homebrew, последний выпуск Clojure можно легко установить:

$ brew install clojure

Это будет поддерживать последнюю версию Clojure — 1.10.0 на момент написания. . После установки мы можем загрузить REPL, просто используя команды clojure или clj:

$ clj
Clojure 1.10.0
user=>

2.4. Установщик в Linux

Самоустанавливающийся сценарий оболочки доступен для нас, чтобы установить инструменты в Linux:

$ curl -O https://download.clojure.org/install/linux-install-1.10.0.411.sh
$ chmod +x linux-install-1.10.0.411.sh
$ sudo ./linux-install-1.10.0.411.sh

Как и в случае с установщиком macOS, они будут доступны для самых последних выпусков Clojure и могут быть выполнены с помощью команд clojure или clj.

3. Знакомство с Clojure REPL

Все перечисленные выше варианты дают нам доступ к Clojure REPL. Это прямой эквивалент Clojure инструмента JShell для Java 9 и выше, который позволяет нам вводить код Clojure и сразу же видеть результат. Это отличный способ поэкспериментировать и узнать, как работают определенные функции языка.

Как только REPL загрузится, у нас появится подсказка, в которой можно ввести любой стандартный код Clojure и немедленно выполнить его. Это включает в себя простые конструкции Clojure, а также взаимодействие с другими библиотеками Java, хотя для загрузки они должны быть доступны в пути к классам.

Приглашение REPL указывает на текущее пространство имен, в котором мы работаем. Для большей части нашей работы это пространство имен пользователя, поэтому приглашение будет таким:

user=>

Все остальное этой статьи предполагается, что у нас есть доступ к Clojure REPL, и все они будут работать непосредственно в любом таком инструменте.

4. Основы языка

Язык Clojure сильно отличается от многих других языков, основанных на JVM, и, возможно, поначалу покажется очень необычным. Он считается диалектом Лиспа и имеет очень похожий синтаксис и функциональность на другие языки Лиспа.

Большая часть кода, который мы пишем на Clojure — как и на других диалектах Лиспа — выражается в форме списков. Затем списки могут быть оценены для получения результатов — либо в виде большего количества списков, либо в виде простых значений.

Например:

(+ 1 2) ; = 3

Это список, состоящий из трех элементов. Символ «+» указывает на то, что мы выполняем этот вызов — добавление. Остальные элементы затем используются с этим вызовом. Таким образом, это оценивается как «1 + 2».

Используя здесь синтаксис списка, это можно тривиально расширить. Например, мы можем сделать:

(+ 1 2 3 4 5) ; = 15

И это оценивается как «1 + 2 + 3 + 4 + 5».

Обратите внимание на точку с запятой. Это используется в Clojure для обозначения комментария и не является концом выражения, как в Java.

4.1. Простые типы

«Clojure построен на основе JVM, поэтому у нас есть доступ к тем же стандартным типам, что и в любом другом Java-приложении. Типы обычно выводятся автоматически, и их не нужно указывать явно.

Например:

123 ; Long
1.23 ; Double
"Hello" ; String
true ; Boolean

Мы можем указать и более сложные типы, используя специальные префиксы или суффиксы:

42N ; clojure.lang.BigInt
3.14159M ; java.math.BigDecimal
1/3 ; clojure.lang.Ratio
#"[A-Za-z]+" ; java.util.regex.Pattern

Обратите внимание, что вместо java.math используется тип clojure.lang.BigInt .БольшоеЦелое. Это связано с тем, что тип Clojure имеет некоторые незначительные оптимизации и исправления.

4.2. Ключевые слова и символы

Clojure дает нам концепцию как ключевых слов, так и символов. Ключевые слова относятся только к самим себе и часто используются для таких вещей, как ключи карты. С другой стороны, символы — это имена, используемые для обозначения других вещей. Например, определения переменных и имена функций являются символами.

Мы можем создать ключевые слова, используя имя с префиксом двоеточия:

user=> :kw
:kw
user=> :a
:a

Ключевые слова имеют прямое равенство сами с собой, а не с чем-либо еще:

user=> (= :a :a)
true
user=> (= :a :b)
false
user=> (= :a "a")
false

Большинство других непростых вещей в Clojure значения считаются символами. Они оцениваются ко всему, на что они ссылаются, тогда как ключевое слово всегда оценивается само по себе:

user=> (def a 1)
#'user/a
user=> :a
:a
user=> a
1

4.3. Пространства имен

Язык Clojure имеет концепцию пространств имен для организации нашего кода. Каждый фрагмент кода, который мы пишем, живет в пространстве имен.

По умолчанию REPL запускается в пространстве имен пользователя, как видно из подсказки «user=\u003e».

Мы можем создавать и изменять пространства имен, используя ключевое слово ns:

user=> (ns new.ns)
nil
new.ns=>

После того, как мы изменили пространства имен, все, что определено в старом пространстве имен, больше недоступно для нас, а все, что определено в новом пространстве имен, становится недоступным. теперь доступно.

Мы можем получить доступ к определениям в пространствах имен, полностью уточнив их. Например, пространство имен clojure.string определяет функцию в верхнем регистре.

Если мы находимся в пространстве имен clojure.string, мы можем получить к нему прямой доступ. Если это не так, нам нужно квалифицировать его как clojure.string/upper-case:

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; This is not visible in the "user" namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: upper-case in this context
user=> (ns clojure.string)
nil
clojure.string=> (upper-case "hello") ; This is visible because we're now in the "clojure.string" namespace
"HELLO"

Мы также можем использовать ключевое слово require для более простого доступа к определениям из другого пространства имен. Это можно использовать двумя основными способами: определить пространство имен с более коротким именем, чтобы его было проще использовать, и напрямую обращаться к определениям из другого пространства имен без какого-либо префикса:

clojure.string=> (require '[clojure.string :as str])
nil
clojure.string=> (str/upper-case "Hello")
"HELLO"

user=> (require '[clojure.string :as str :refer [upper-case]])
nil
user=> (upper-case "Hello")
"HELLO"

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

4.4. Переменные

Как только мы узнаем, как определять простые значения, мы можем присвоить их переменным. Мы можем сделать это, используя ключевое слово def:

user=> (def a 123)
#'user/a

После того, как мы это сделали, мы можем использовать символ a везде, где мы хотим представить это значение:

user=> a
123

Определения переменных могут быть как простыми, так и сложно, как мы хотим.

Например, чтобы определить переменную как сумму чисел, мы можем сделать:

user=> (def b (+ 1 2 3 4 5))
#'user/b
user=> b
15

Обратите внимание, что нам никогда не нужно объявлять переменную или указывать ее тип. Clojure автоматически определяет все это для нас.

Если мы попытаемся использовать переменную, которая не была определена, вместо этого мы получим ошибку:

user=> unknown
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: unknown in this context
user=> (def c (+ 1 unknown))
Syntax error compiling at (REPL:1:8).
Unable to resolve symbol: unknown in this context

Обратите внимание, что вывод функции def немного отличается от ввода. Определение переменной a возвращает строку «user/a. Это связано с тем, что результатом является символ, и этот символ определен в текущем пространстве имен.

4.5. Функции

Мы уже видели пару примеров вызова функций в Clojure. Мы создаем список, который начинается с вызываемой функции, а затем со всеми параметрами.

Когда этот список оценивается, мы получаем возвращаемое значение из функции. Например:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]
user=> (java.time.Instant/parse "2019-01-15T07:55:00Z")
#object[java.time.Instant 0x6b8d96d9 "2019-01-15T07:55:00Z"]
user=> (java.time.OffsetDateTime/of 2019 01 15 7 56 0 0 java.time.ZoneOffset/UTC)
#object[java.time.OffsetDateTime 0xf80945f "2019-01-15T07:56Z"]

Мы также можем вкладывать вызовы функций, когда мы хотим передать результат вызова одной функции в качестве параметра другой:

user=> (java.time.OffsetDateTime/of 2018 01 15 7 57 0 0 (java.time.ZoneOffset/ofHours -5))
#object[java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07:57-05:00"]

Также мы можем определить наши функции если мы желаем. Функции создаются с помощью команды fn:

user=> (fn [a b]
  (println "Adding numbers" a "and" b)
  (+ a b)
)
#object[user$eval165$fn__166 0x5644dc81 "[email protected]"]

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

user=> (def add
  (fn [a b]
    (println "Adding numbers" a "and" b)
    (+ a b)
  )
)
#'user/add

«

user=> (add 1 2)
Adding numbers 1 and 2
3

«Теперь, когда мы определили эту функцию, мы можем называть ее так же, как и любую другую функцию:

Для удобства Clojure также позволяет нам использовать defn для определения функции с именем за один раз.

user=> (defn sub [a b]
  (println "Subtracting" b "from" a)
  (- a b)
)
#'user/sub
user=> (sub 5 2)
Subtracting 2 from 5
3

Например:

4.6. Let и локальные переменные

Вызов def определяет символ, который является глобальным для текущего пространства имен. Обычно это не то, что требуется при выполнении кода. Вместо этого Clojure предлагает вызов let для определения переменных, локальных для блока. Это особенно полезно при использовании их внутри функций, когда вы не хотите, чтобы переменные просачивались за пределы функции.


user=> (defn sub [a b]
  (def result (- a b))
  (println "Result: " result)
  result
)
#'user/sub

Например, мы могли бы определить нашу подфункцию:

user=> (sub 1 2)
Result:  -1
-1
user=> result ; Still visible outside of the function
-1

Однако использование этого имеет следующий неожиданный побочный эффект:

user=> (defn sub [a b]
  (let [result (- a b)]
    (println "Result: " result)
    result
  )
)
#'user/sub
user=> (sub 1 2)
Result:  -1
-1
user=> result
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: result in this context

Вместо этого давайте перепишем его, используя let:

~ ~~ На этот раз символ результата не виден за пределами функции. Или, действительно, за пределами блока let, в котором он использовался.

5. Коллекции

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

Вектор — это упорядоченный список значений — любое произвольное значение может быть помещено в вектор, включая другие коллекции. Набор — это неупорядоченный набор значений, который никогда не может содержать одно и то же значение более одного раза. Карта — это простой набор пар ключ/значение. Очень часто в качестве ключей на карте используются ключевые слова, но мы можем использовать любое значение, которое нам нравится, включая другие коллекции. Список очень похож на вектор. Разница такая же, как между ArrayList и LinkedList в Java. Как правило, вектор предпочтительнее, но список лучше, если мы хотим добавлять элементы в начало или если мы хотим получить доступ к элементам только в последовательном порядке.

5.1. Создание коллекций

; Vector
user=> [1 2 3]
[1 2 3]
user=> (vector 1 2 3)
[1 2 3]

; List
user=> '(1 2 3)
(1 2 3)
user=> (list 1 2 3)
(1 2 3)

; Set
user=> #{1 2 3}
#{1 3 2}
user=> (hash-set 1 2 3)
#{1 3 2}

; Map
user=> {:a 1 :b 2}
{:a 1, :b 2}
user=> (hash-map :a 1 :b 2)
{:b 2, :a 1}

Каждую из них можно создать с помощью сокращенной записи или вызова функции:

Обратите внимание, что примеры Set и Map не возвращают значения в одном и том же порядке. Это связано с тем, что эти коллекции по своей природе неупорядочены, и то, что мы видим, зависит от того, как они представлены в памяти.

Мы также можем видеть, что синтаксис для создания списка очень похож на стандартный синтаксис Clojure для выражений. Выражение Clojure — это, по сути, список, который оценивается, тогда как символ апострофа здесь указывает, что нам нужен фактический список значений, а не его вычисление.

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

user=> (seq [1 2 3])
(1 2 3)
user=> (seq #{1 2 3})
(1 3 2)
user=> (seq {:a 1 2 3})
([:a 1] [2 3])

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

5.2. Доступ к коллекциям

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

user=> (my-vector 2) ; [1 2 3]
3

Векторы — единственная коллекция, которая позволяет нам получить любое произвольное значение по индексу. Это делается путем вычисления вектора и индекса как выражения:

user=> (my-map :b)
2

Мы можем сделать то же самое, используя тот же синтаксис, и для карт:

user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)

У нас также есть функции для доступа к векторам и спискам чтобы получить первое значение, последнее значение и остаток списка:

user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)

Карты имеют дополнительные функции для получения всего списка ключей и значений:

Единственный реальный доступ, который у нас есть наборы, чтобы увидеть, является ли конкретный элемент членом.

user=> (my-set 1)
1
user=> (my-set 5)
nil

Это очень похоже на доступ к любой другой коллекции:

5.3. Идентификация коллекций

Мы видели, что способ доступа к коллекции зависит от ее типа. У нас есть набор функций, которые мы можем использовать, чтобы определить это как конкретным, так и более общим образом.

user=> (vector? [1 2 3]) ; A vector is a vector
true
user=> (vector? #{1 2 3}) ; A set is not a vector
false
user=> (list? '(1 2 3)) ; A list is a list
true
user=> (list? [1 2 3]) ; A vector is not a list
false
user=> (map? {:a 1 :b 2}) ; A map is a map
true
user=> (map? #{1 2 3}) ; A set is not a map
false
user=> (seq? '(1 2 3)) ; A list is a seq
true
user=> (seq? [1 2 3]) ; A vector is not a seq
false
user=> (seq? (seq [1 2 3])) ; A vector can be converted into a seq
true
user=> (associative? {:a 1 :b 2}) ; A map is associative
true
user=> (associative? [1 2 3]) ; A vector is associative
true
user=> (associative? '(1 2 3)) ; A list is not associative
false

«Каждая из наших коллекций имеет определенную функцию для определения того, относится ли данное значение к этому типу — list? для списков, установить? для наборов и так далее. Кроме того, есть seq? для определения того, является ли данное значение последовательностью любого вида и ассоциативно? чтобы определить, допускает ли заданное значение ассоциативный доступ любого типа, что означает векторы и карты:

5.4. Изменение коллекций

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

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

user=> (conj [1 2 3] 4) ; Adds to the end
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; Adds to the beginning
(4 1 2 3)
user=> (conj #{1 2 3} 4) ; Unordered
#{1 4 3 2}
user=> (conj #{1 2 3} 3) ; Adding an already present entry does nothing
#{1 3 2}

Добавление новых элементов в вектор, список или набор выполняется с помощью conj. В каждом из этих случаев это работает по-разному, но с одним и тем же основным намерением:

user=> (disj #{1 2 3} 2) ; Removes the entry
#{1 3}
user=> (disj #{1 2 3} 4) ; Does nothing because the entry wasn't present
#{1 3 2}

Мы также можем удалить записи из набора, используя disj. Обратите внимание, что это не работает со списком или вектором, потому что они строго упорядочены:

user=> (assoc {:a 1 :b 2} :c 3) ; Adds a new key
{:a 1, :b 2, :c 3}
user=> (assoc {:a 1 :b 2} :b 3) ; Updates an existing key
{:a 1, :b 3}
user=> (dissoc {:a 1 :b 2} :b) ; Removes an existing key
{:a 1}
user=> (dissoc {:a 1 :b 2} :c) ; Does nothing because the key wasn't present
{:a 1, :b 2}

Добавление новых элементов на карту выполняется с помощью assoc. Мы также можем удалить записи с карты, используя dissoc:

5.5. Конструкции функционального программирования

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

user=> (map inc [1 2 3]) ; Increment every value in the vector
(2 3 4)
user=> (map inc #{1 2 3}) ; Increment every value in the set
(2 4 3)

user=> (filter odd? [1 2 3 4 5]) ; Only return odd values
(1 3 5)
user=> (remove odd? [1 2 3 4 5]) ; Only return non-odd values
(2 4)

user=> (reduce + [1 2 3 4 5]) ; Add all of the values together, returning the sum
15

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

6. Структуры управления

Как и во всех языках общего назначения, В Clojure есть вызовы стандартных управляющих структур, таких как условные операторы и циклы.

6.1. Условные операторы

user=> (if true 1 2)
1
user=> (if false 1 2)
2

Условные операторы обрабатываются оператором if. Он принимает три параметра: тест, блок для выполнения, если тест верен, и блок для выполнения, если тест неверен. Каждое из них может быть простым значением или стандартным списком, который будет оцениваться по запросу:

user=> (if (> 1 2) "True" "False")
"False"

Наш тест может быть чем угодно, что нам нужно — это не обязательно должно быть значение true/false. . Это также может быть блок, который оценивается, чтобы дать нам нужное значение:

user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

Здесь можно использовать все стандартные проверки, включая =, \u003e и \u003c. Существует также набор предикатов, которые можно использовать по разным причинам — некоторые из них мы уже видели при просмотре коллекций, например:

user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"

Тест может вернуть любое значение — он не нужно только быть истинным или ложным. Однако считается истинным, если значение не равно false или nil. Это отличается от того, как работает JavaScript, где есть большой набор значений, которые считаются «истинными», но не истинными:

6.2. Зацикливание

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

user=> (loop [accum [] i 0]
  (if (= i 10)
    accum
    (recur (conj accum i) (inc i))
  ))
[0 1 2 3 4 5 6 7 8 9]

Кроме того, зацикливание выполняется полностью с использованием рекурсии. Мы можем написать рекурсивные функции или использовать ключевые слова loop и recur для написания цикла в рекурсивном стиле:

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

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

7. Резюме

«В этой статье представлено введение в язык программирования Clojure, показано, как работает его синтаксис и что с ним можно делать. Это только вводный уровень, и он не углубляется во все, что можно сделать с языком.