Краткий экскурс в синтаксис Clojure, который настолько лаконичен, что вы сможете прочитать этот раздел примерно за 15 минут (или меньше).
Комментарии
;;
две точки с запятой для комментария строки, ;
одна точка с запятой для комментария остальной части строки.
#_
макрос чтения комментариев, чтобы закомментировать следующую форму.
(comment )
форма для комментариев ко всем содержащимся формам.
Clojure записан в формах
Clojure написан в "формах", которые представляют собой списки элементов, заключенных в круглые скобки ()
и разделенных пробелами.
Clojure считает первое значение в форме вызовом функции. Дополнительные значения в форме передаются в качестве аргументов вызываемой функции.
Clojure организован в виде одного или нескольких пространств имен. Пространство имен представляет собой путь к каталогу и имя файла, который содержит код конкретного пространства имен.
;; Define the namespace test
(ns test.code) ;; src/test/code.clj
;; Define a longer namespace
(ns com.company.product.component.core) ;; src/com/company/product/component/core.clj
Манипулирование строками
Функция str
создает новую строку из всех переданных аргументов.
(str "Hello" " " "World")
; => "Hello World"
clojure.string
возвращает строковые значения (другие функции возвращают символы в качестве результата).
Math (математика), Truth (истина) и префиксная нотация
Функции используют префиксную нотацию, поэтому вы можете легко выполнять математические вычисления с несколькими значениями.
(+ 1 2 3 5 7 9 12) ; => 40
(- 24 7 3) ; => 14
(* 1 2) ; => 2
(/ 27 7) ; => 22/7
Математический метод очень точен, нет необходимости в правилах предшествования операторов (так как операторов нет).
Благодаря вложенным формам вычисления очень точны.
(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2
Равенство — это =
(= 1 1) ; => true
(= 2 1) ; => false
;
вам нужно использовать в логике not, так (not true) ; => false
(not= true false) ; => true
Типы
Clojure имеет строгую типизацию, поэтому в Clojure все является типом.
Clojure является динамически типизированным, поэтому сам определяет тип. Тип не нужно указывать в коде, что делает код более простым и лаконичным.
Поскольку Clojure является размещаемым на уже существующей платформе языком, он использует систему типов используемой среды, где это уместно. Например, Clojure под капотом применяет объектные типы Java для булевых данных, строк и чисел.
Для проверки типа кода в Clojure используйте функции class
или type
.
(class 1) ; Integer literals are java.lang.Long by default
(class 1.); Float literals are java.lang.Double
(class ""); Strings always double-quoted, and are java.lang.String
(class false) ; Booleans are java.lang.Boolean
(class nil); The "null" value is called nil
Vectors (векторы) и Lists (списки) — это тоже классы java!
(class [1 2 3]); => clojure.lang.PersistentVector
(class '(1 2 3)); => clojure.lang.PersistentList
Коллекции и последовательности
Наиболее распространенные коллекции данных в Clojure:
(1 2 "three")
или(list 1 2 "three")
— список значений, прочитанный от начала до конца (последовательный доступ).[1 2 "three"]
или(list 1 2 "three")
— вектор значений с индексом (произвольный доступ).{:key "value"}
или(hash-map :key "value")
— хэштаблица (хэшмапа) с нулем или более пар ключ-значение (ассоциативная связь).#{1 2 "three"}
или(set 1 2 "three")
— уникальный набор значений.
Список ()
определяется как вызов функции. Первый элемент списка — имя вызываемой функции, а дополнительные значения являются аргументами функции.
Функция '
quote сообщает читателю Clojure, что список следует рассматривать только как данные.
'(1 2 3)
Списки и векторы — это коллекции.
(coll? '(1 2 3)) ; => true
(coll? [1 2 3]) ; => true
Только списки являются последовательностями.
(seq? '(1 2 3)) ; => true
(seq? [1 2 3]) ; => false
Последовательности — это интерфейс для логических списков, которые могут быть ленивыми. "Ленивая" означает, что последовательность значений не оценивается до тех пор, пока к ней не обратятся.
Ленивая последовательность позволяет использовать большие или даже бесконечные серии, например, как:
(range) ; => (0 1 2 3 4 ...) - an infinite series
(take 4 (range)) ; (0 1 2 3) - lazyily evaluate range and stop when enough values are taken
Используйте cons для добавления элемента в начало списка или вектора.
(cons 4 [1 2 3]) ; => (4 1 2 3)
(cons 4 '(1 2 3)) ; => (4 1 2 3)
Используйте conj, чтобы добавить элемент относительно типа коллекции, в начало списка или в конец вектора.
(conj [1 2 3] 4) ; => [1 2 3 4]
(conj '(1 2 3) 4) ; => (4 1 2 3)
Используйте concat для сложения списков или векторов вместе.
(concat [1 2] '(3 4)) ; => (1 2 3 4)
Используйте filter, map для взаимодействия с коллекциями.
(map inc [1 2 3]) ; => (2 3 4)
(filter even? [1 2 3]) ; => (2)
Используйте reduce для их уменьшения.
(reduce + [1 2 3 4])
; = (+ (+ (+ 1 2) 3) 4)
; => 10
Reduce также может принимать аргумент начального значения.
(reduce conj [] '(3 2 1))
; => [3 2 1]
Эквивалент (conj (conj (conj (conj [] 3) 2) 1)
Функции
Используйте fn
для создания новых функций, определяющих какое-то поведение. fn
называют анонимной функцией, поскольку она не имеет внешнего имени, на которое можно сослаться, и ее нужно вызывать в форме списка.
(fn hello [] "Hello World") ; => hello
Оберните форму (fn ,,,)
в круглые скобки, чтобы вызвать ее и вернуть результат.
((fn hello [] "Hello World")) ; => "Hello World"
Создайте многократно используемую функцию с помощью def
, создав имя, которым будет var
. Поведение функции, определенное в def
, может быть изменено, а выражение переоценено для использования нового поведения.
(defn hello-world []
"Hello World")
;; => "Hello World"
[] — это список аргументов для функции.
(defn hello [name]
(str "Hello " name))
(hello "Steve") ; => "Hello Steve"
Clojure поддерживает мультивариадические функции, позволяя одному определению функции отвечать на вызов функции с разным количеством аргументов.
(defn hello3
([] "Hello World")
([name] (str "Hello " name)))
(hello3 "Jake") ; => "Hello Jake"
(hello3) ; => "Hello World"
Функции могут упаковывать дополнительные аргументы в последовательность.
(defn count-args [& args]
(str "You passed " (count args) " args: " args))
(count-args 1 2 3) ; => "You passed 3 args: (1 2 3)"
Вы можете смешивать обычные и упакованные аргументы.
(defn hello-count [name & args]
(str "Hello " name ", you passed " (count args) " extra args"))
(hello-count "Finn" 1 2 3)
; => "Hello Finn, you passed 3 extra args"
Коллекции хэшмапов (hash-map)
(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap
Ключевые слова подобны строкам с небольшим бонусом в плане эффективности.
(class :a) ; => clojure.lang.Keyword
Мапы могут использовать любой тип в качестве ключа, но как правило, лучше всего подходят ключевые слова.
(def stringmap (hash-map "a" 1, "b" 2, "c" 3))
stringmap ; => {"a" 1, "b" 2, "c" 3}
(def keymap (hash-map :a 1 :b 2 :c 3))
keymap ; => {:a 1, :c 3, :b 2} (order is not guaranteed)
Запятые — это пробелы. Запятые всегда рассматриваются как пробелы и игнорируются Clojure-ридером.
Получение значения из мапы путем вызова ее как функции.
(stringmap "a") ; => 1
(keymap :a) ; => 1
Ключевые слова позволяют извлекать их значение из мапы. Строки так не используются.
(:b keymap) ; => 2
("a" stringmap)
; => Exception: java.lang.String cannot be cast to clojure.lang.IFn
При извлечении значения, которое не было представлено, возвращается nil.
(stringmap "d") ; => nil
Используйте assoc для добавления новых ключей в хэшмапы.
(assoc keymap :d 4) ; => {:a 1, :b 2, :c 3, :d 4}
Но помните, что типы clojure иммутабельны!
keymap ; => {:a 1, :b 2, :c 3}
Используйте dissoc для удаления ключей.
(dissoc keymap :a :b) ; => {:c 3}
Установки
(class #{1 2 3}) ; => clojure.lang.PersistentHashSet
(set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3}
Добавьте элемент с помощью conj.
(conj #{1 2 3} 4) ; => #{1 2 3 4}
Удалите его с помощью disj.
(disj #{1 2 3} 1) ; => #{2 3}
````
Test for existence by using the set as a function:
```clojure
(#{1 2 3} 1) ; => 1
(#{1 2 3} 4) ; => nil
В пространстве имен clojure.sets есть больше функций.
Полезные формы
Логические конструкции в clojure — это просто макросы, и выглядят они так же, как и все остальное.
(if false "a" "b") ; => "b"
(if false "a") ; => nil
Используйте let для создания временных привязок.
(let [a 1 b 2]
(> a b)) ; => false
Группируйте утверждения вместе с помощью do.
(do
(print "Hello")
"World") ; => "World" (prints "Hello")
Функции имеют неявный do.
(defn print-and-say-hello [name]
(print "Saying hello to " name)
(str "Hello " name))
(print-and-say-hello "Jeff") ;=> "Hello Jeff" (prints "Saying hello to Jeff")
Так же как и let.
(let [name "Urkel"]
(print "Saying hello to " name)
(str "Hello " name)) ; => "Hello Urkel" (prints "Saying hello to Urkel")
Пространства имен и библиотеки
Пространства имен используются для того, чтобы организовать код в логические группы. В верхней части каждого файла Clojure есть форма ns
, которая определяет название пространства имен. Доменная часть названия пространства имен обычно является наименованием организации или сообщества (например, GitHub user/organisation).
(ns domain.namespace-name)
Все проекты Practicalli имеют домены пространства имен practicalli
(ns practicalli.service-name)
require
позволяет коду из одного пространства имен быть доступным из другого пространства имен, либо из того же проекта Clojure, либо из библиотеки, добавленной в classpath
(путь к классам) проекта.
Директива :as
в require
используется для указания имени псевдонима, которое сокращенно обозначает полное имя библиотеки.
Или :refer [function-name var-name]
может использоваться для указания конкретных функций и данных (vars), которые доступны напрямую.
Требуемая (required) директива обычно добавляется к форме пространства имен.
(ns practicalli.service-name
(require [clojure.set :as set]))
Функции из clojure.set можно использовать через псевдоним, а не через полное имя, т.е. clojure.set/intersection
(set/intersection #{1 2 3} #{2 3 4}) ; => #{2 3}
(set/difference #{1 2 3} #{2 3 4}) ; => #{1}
Директива :require
может быть использована для включения нескольких пространств имен библиотек.
(ns test
(:require
[clojure.string :as string]
[clojure.set :as set]))
require
может использоваться самостоятельно, обычно в блоке многофункционального кода.
(comment
(require 'clojure.set :as set))
Java
В Java есть огромная и полезная стандартная библиотека, поэтому вы захотите узнать, как с ней работать.
Используйте import для загрузки пакета java.
(import java.util.Date)
Или импорт из имени пакета java.
(ns test
(:import
java.util.Date
java.util.Calendar))
Используйте имя класса с символом "." в конце, чтобы создать новый экземпляр.
(Date.) ; <a date object>
Используйте .
для вызова методов. Или используйте горячую клавишу ".method".
(. (Date.) getTime) ; <a timestamp>
(.getTime (Date.)) ; exactly the same thing.
Используйте /
для вызова статических методов.
(System/currentTimeMillis) ; <a timestamp> (system is always present)
Используйте doto, чтобы сделать ощущения от взаимодействия с (мутабельными [изменяемыми]) классами более толерантными.
(import java.util.Calendar)
(doto (Calendar/getInstance)
(.set 2000 1 1 0 0 0)
.getTime) ; => A Date. set to 2000-01-01 00:00:00
Язык программирования Clojure — это язык программирования общего назначения, на нём можно разрабатывать абсолютно что угодно. Но до недавнего времени разработка скриптов на Clojure была довольно трудной задачей, в основном, из-за медленного старта JVM.
Появление GraalVM позволило обойти это ограничение. Скрипты, написанные на Clojure, стартуют практически мгновенно. При этом в процессе разработки доступен REPL и весь арсенал языка Clojure. На бесплатном вебинаре вы познакомитесь с проектом Babashka и узнаете, как именно эта библиотека помогает разрабатывать скрипты. Приглашаем всех желающих.
Записаться на открытый урок можно на странице "Clojure Developer".