Clojure — это представитель семейства Lisp-подобных языков, разработанный для Java Virtual Machine. Язык идейно гораздо ближе к чистому функциональному программированию, чем его прародитель Common Lisp, но в то же время обладает набором инструментов для работы с состоянием, таких как STM.
Благодаря такому сочетанию технологий в одном языке, разработка программ, предполагающих конкурентное выполнение, значительно упрощается и даже может быть автоматизирована.
(Последующие примеры кода предполагают выполнение в Clojure версии 1.2 и выше)
1; Комментарии начинаются символом ";".
2
3; Код на языке Clojure записывается в виде «форм»,
4; которые представляют собой обычные списки элементов, разделенных пробелами,
5; заключённые в круглые скобки.
6;
7; Clojure Reader (инструмент языка, отвечающий за чтение исходного кода),
8; анализируя форму, предполагает, что первым элементом формы (т.е. списка)
9; является функция или макрос, который следует вызвать, передав ему
10; в качестве аргументов остальные элементы списка-формы.
11
12; Первым вызовом в файле должен быть вызов функции ns,
13; которая отвечает за выбор текущего пространства имен (namespace)
14(ns learnclojure-ru)
15
16; Несколько простых примеров:
17
18; str объединяет в единую строку все свои аргументы
19(str "Hello" " " "World") ; => "Hello World"
20
21; Арифметика тоже выглядит несложно
22(+ 1 1) ; => 2
23(- 2 1) ; => 1
24(* 1 2) ; => 2
25(/ 2 1) ; => 2
26
27; Проверка на равенство (Equality)
28(= 1 1) ; => true
29(= 2 1) ; => false
30
31; Для булевой логики вам может понадобиться not
32(not true) ; => false
33
34; Вложенные формы, конечно же, допустимы и работают вполне предсказуемо
35(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2
36
37; Типы
38;;;;;;;;;;;;;
39
40; Clojure использует типы Java для представления булевых значений,
41; строк и чисел
42; Узнать тип мы можем, использую функцию `class
43(class 1) ; Целочисленные литералы типа по-умолчанию являются java.lang.Long
44(class 1.) ; Числа с плавающей точкой, это java.lang.Double
45(class "") ; Строки всегда заключаются в двойные кавычки
46 ; и представляют собой java.lang.String
47(class false) ; Булевы значения, это экземпляры java.lang.Boolean
48(class nil) ; "Пустое" значение называется "nil"
49
50; Если Вы захотите создать список из чисел, вы можете просто
51; предварить форму списка символом "'", который подскажет Reader`у,
52; что эта форма не требует вычисления
53'(+ 1 2) ; => (+ 1 2)
54; ("'", это краткая запись формы (quote (+ 1 2))
55
56; «Квотированный» список можно вычислить, передав его функции eval
57(eval '(+ 1 2)) ; => 3
58
59; Коллекции и Последовательности
60;;;;;;;;;;;;;;;;;;;
61
62; Списки (Lists) в clojure структурно представляют собой «связанные списки»,
63; тогда как Векторы (Vectors), устроены как массивы.
64; Векторы и Списки тоже являются классами Java!
65(class [1 2 3]); => clojure.lang.PersistentVector
66(class '(1 2 3)); => clojure.lang.PersistentList
67
68; Список может быть записан как (1 2 3), но в этом случае
69; он будет воспринят reader`ом, как вызов функции.
70; Есть два способа этого избежать:
71; '(1 2 3) - квотирование,
72; (list 1 2 3) - явное конструирование списка с помощью функции list.
73
74; «Коллекции» — это некие наборы данных.
75; И списки, и векторы являются коллекциями:
76(coll? '(1 2 3)) ; => true
77(coll? [1 2 3]) ; => true
78
79; «Последовательности» (seqs) — это абстракция над наборами данных,
80; элементы которых "упакованы" последовательно.
81; Списки — последовательности, а векторы — нет.
82(seq? '(1 2 3)) ; => true
83(seq? [1 2 3]) ; => false
84
85; Любая seq предоставляет доступ только к началу последовательности данных,
86; не предоставляя информацию о её длине.
87; При этом последовательности могут быть и бесконечными,
88; т.к. являются ленивыми и предоставляют данные только по требованию!
89(range 4) ; => (0 1 2 3)
90(range) ; => (0 1 2 3 4 ...) (бесконечная последовательность!)
91(take 4 (range)) ; (0 1 2 3)
92
93; Добавить элемент в начало списка или вектора можно с помощью функции cons
94(cons 4 [1 2 3]) ; => (4 1 2 3)
95(cons 4 '(1 2 3)) ; => (4 1 2 3)
96
97; Функция conj добавляет элемент в коллекцию
98; максимально эффективным для неё способом.
99; Для списков эффективно добавление в начло, а для векторов — в конец.
100(conj [1 2 3] 4) ; => [1 2 3 4]
101(conj '(1 2 3) 4) ; => (4 1 2 3)
102
103; Функция concat объединяет несколько списков и векторов в единый список
104(concat [1 2] '(3 4)) ; => (1 2 3 4)
105
106; Работать с коллекциями удобно с помощью функций filter и map
107(map inc [1 2 3]) ; => (2 3 4)
108(filter even? [1 2 3]) ; => (2)
109
110; reduce поможет «свернуть» коллекцию
111(reduce + [1 2 3 4])
112; = (+ (+ (+ 1 2) 3) 4)
113; => 10
114
115; Вызывая reduce, мы можем указать начальное значение
116(reduce conj [] '(3 2 1))
117; = (conj (conj (conj [] 3) 2) 1)
118; => [3 2 1]
119
120; Функции
121;;;;;;;;;;;;;;;;;;;;;
122
123; Функция создается специальной формой fn.
124; «Тело» функции может состоять из нескольких форм,
125; но результатом вызова функции всегда будет результат вычисления
126; последней из них.
127(fn [] "Hello World") ; => fn
128
129; (Вызов функции требует «оборачивания» fn-формы в форму вызова)
130((fn [] "Hello World")) ; => "Hello World"
131
132; Назначить значению имя можно специальной формой def
133(def x 1)
134x ; => 1
135
136; Назначить имя можно любому значению, в т.ч. и функции:
137(def hello-world (fn [] "Hello World"))
138(hello-world) ; => "Hello World"
139
140; Поскольку именование функций — очень частая операция,
141; clojure позволяет, сделать это проще:
142(defn hello-world [] "Hello World")
143
144; Вектор [] в форме описания функции, следующий сразу за именем,
145; описывает параметры функции:
146(defn hello [name]
147 (str "Hello " name))
148(hello "Steve") ; => "Hello Steve"
149
150; Одна функция может иметь сразу несколько наборов аргументов:
151(defn hello3
152 ([] "Hello World")
153 ([name] (str "Hello " name)))
154(hello3 "Jake") ; => "Hello Jake"
155(hello3) ; => "Hello World"
156
157; Также функция может иметь набор аргументов переменной длины
158(defn count-args [& args] ; args будет содержать seq аргументов
159 (str "You passed " (count args) " args: " args))
160(count-args 1 2 3) ; => "You passed 3 args: (1 2 3)"
161
162; Можно комбинировать оба подхода задания аргументов
163(defn hello-count [name & args]
164 (str "Hello " name ", you passed " (count args) " extra args"))
165(hello-count "Finn" 1 2 3)
166; => "Hello Finn, you passed 3 extra args"
167
168; Для создания анонимных функций есть специальный синтаксис:
169; функциональные литералы
170(def hello2 #(str "Hello " %1))
171(hello2 "Fanny") ; => "Hello Fanny"
172
173; такие функциональные литералы удобно использовать с map, filter и reduce
174(map #(* 10 %1) [1 2 3 5]) ; => (10 20 30 50)
175(filter #(> %1 3) [1 2 3 4 5 6 7]) ; => (4 5 6 7)
176(reduce #(str %1 "," %2) [1 2 3 4]) ; => "1,2,3,4"
177
178; Отображения (Maps)
179;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
180
181; Hash maps и array maps имеют одинаковый интерфейс.
182; Hash maps производят поиск по ключу быстрее, но не сохраняют порядок ключей
183(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap
184(class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap
185
186; Array maps автоматически преобразуются в hash maps,
187; как только разрастутся до определенного размера
188
189; Отображения могут использовать в качестве ключей любые хэшируемые значения,
190; однако предпочтительными являются ключи,
191; являющиеся «ключевыми словами» (keywords)
192(class :a) ; => clojure.lang.Keyword
193
194(def stringmap {"a" 1, "b" 2, "c" 3})
195stringmap ; => {"a" 1, "b" 2, "c" 3}
196
197(def keymap {:a 1, :b 2, :c 3})
198keymap ; => {:a 1, :c 3, :b 2}
199
200; Предыдущий пример содержит запятые в коде, однако reader не использует их,
201; при обработке литералов - запятые просто воспринимаются,
202; как "пробельные символы" (whitespaces)
203
204; Отображение может выступать в роли функции, возвращающей значение по ключу
205(stringmap "a") ; => 1
206(keymap :a) ; => 1
207
208; При попытке получить отсутствующее значение, будет возвращён nil
209(stringmap "d") ; => nil
210
211; Иногда бывает удобно указать конкретное значение по-умолчанию:
212({:a 1 :b 2} :c "Oops!") ; => "Oops!"
213
214; Keywords тоже могут использоваться в роли функций!
215(:b keymap) ; => 2
216
217; Однако этот фокус не пройдёт со строками.
218;("a" stringmap)
219; => Exception: java.lang.String cannot be cast to clojure.lang.IFn
220
221; Добавить пару ключ-значение в отображение можно функцией assoc
222(def newkeymap (assoc keymap :d 4))
223newkeymap ; => {:a 1, :b 2, :c 3, :d 4}
224
225; Но всегда следует помнить, что значения в Clojure - неизменяемые!
226keymap ; => {:a 1, :b 2, :c 3} - оригинал не был затронут
227
228; dissoc позволяет исключить значение по ключу
229(dissoc keymap :a :b) ; => {:c 3}
230
231; Множества (Sets)
232;;;;;;;;;;;;;;;;;;
233
234(class #{1 2 3}) ; => clojure.lang.PersistentHashSet
235(set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3}
236
237; Добавляются элементы посредством conj
238(conj #{1 2 3} 4) ; => #{1 2 3 4}
239
240; Исключаются - посредством disj
241(disj #{1 2 3} 1) ; => #{2 3}
242
243; Вызов множества как функции позволяет проверить
244; принадлежность элемента этому множеству:
245(#{1 2 3} 1) ; => 1
246(#{1 2 3} 4) ; => nil
247
248; В пространстве имен clojure.sets
249; содержится множество функций для работы с множествами
250
251; Полезные формы
252;;;;;;;;;;;;;;;;;
253
254; Конструкции ветвления в clojure — это обычные макросы,
255; они подобны своим собратьям в других языках:
256(if false "a" "b") ; => "b"
257(if false "a") ; => nil
258
259; Специальная форма let позволяет присвоить имена значениям локально.
260; При этом все изменения будут видны только вложенным формам:
261(def a 10)
262(let [a 1 b 2]
263 (> a b)) ; => false
264
265; Несколько форм можно объединить в одну форму посредством do.
266; Значением do-формы будет значение последней формы из списка вложенных в неё:
267(do
268 (print "Hello")
269 "World") ; => "World" (prints "Hello")
270
271; Множество макросов содержит внутри себя неявную do-форму.
272; Пример - макрос определения функции:
273(defn print-and-say-hello [name]
274 (print "Saying hello to " name)
275 (str "Hello " name))
276(print-and-say-hello "Jeff") ;=> "Hello Jeff" (prints "Saying hello to Jeff")
277
278; Ещё один пример — let:
279(let [name "Urkel"]
280 (print "Saying hello to " name)
281 (str "Hello " name)) ; => "Hello Urkel" (prints "Saying hello to Urkel")
282
283; Модули
284;;;;;;;;;
285
286; Форма use позволяет добавить в текущее пространство имен
287; все имена (вместе со значениями) из указанного модуля:
288(use 'clojure.set)
289
290; Теперь нам доступны операции над множествами:
291(intersection #{1 2 3} #{2 3 4}) ; => #{2 3}
292(difference #{1 2 3} #{2 3 4}) ; => #{1}
293
294; use позволяет указать, какие конкретно имена
295; должны быть импортированы из модуля:
296(use '[clojure.set :only [intersection]])
297
298; Также модуль может быть импортирован формой require
299(require 'clojure.string)
300
301; После этого модуль становится доступен в текущем пространстве имен,
302; а вызов его функций может быть осуществлен указанием полного имени функции:
303(clojure.string/blank? "") ; => true
304
305; Импортируемому модулю можно назначить короткое имя:
306(require '[clojure.string :as str])
307(str/replace "This is a test." #"[a-o]" str/upper-case) ; => "THIs Is A tEst."
308; (Литерал вида #"" обозначает регулярное выражение)
309
310; Вместо отдельной формы require (и use, хотя это и не приветствуется) можно
311; указать необходимые модули прямо в форме ns:
312(ns test
313 (:require
314 [clojure.string :as str] ; Внимание: при указании внутри формы ns
315 [clojure.set :as set])) ; имена пакетов не квотируются!
316
317; Java
318;;;;;;;
319
320; Стандартная библиотека Java очень богата,
321; и всё это богатство доступно и для Clojure!
322
323; import позволяет импортировать модули Java
324(import java.util.Date)
325
326; В том числе и из ns
327(ns test
328 (:import java.util.Date
329 java.util.Calendar))
330
331; Имя класса, сопровождаемое символом "." позволяет
332; инстанцировать объекты Java-классов:
333(Date.) ; <a date object>
334
335; форма . позволяет вызывать методы:
336(. (Date.) getTime) ; <a timestamp>
337(.getTime (Date.)) ; а можно и так
338
339; Статические методы вызываются как функции модуля:
340(System/currentTimeMillis) ; <a timestamp> (Модуль system всегда доступен!)
341
342; doto позволяет удобно работать с объектами, изменяющими свое состояние
343(import java.util.Calendar)
344(doto (Calendar/getInstance)
345 (.set 2000 1 1 0 0 0)
346 .getTime) ; => A Date. set to 2000-01-01 00:00:00
347
348; Работа с изменяемым сотоянием
349;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
350
351; Clojure предоставляет набор инструментов
352; для работы с изменяемым состоянием: Software Transactional Memory.
353; Структуры STM представлены тремя типами:
354; - атомы (atoms)
355; - агенты (agents)
356; - ссылки (references)
357
358; Самые простые хранители состояния - атомы:
359(def my-atom (atom {})) ; {} - начальное состояние атома
360
361; Обновляется атом посредством swap!.
362; swap! применяет функцию аргумент к текущему значению
363; атома и помещает в атом результат
364(swap! my-atom assoc :a 1) ; Обновляет my-atom, помещая в него (assoc {} :a 1)
365(swap! my-atom assoc :b 2) ; Обновляет my-atom, помещая в него (assoc {:a 1} :b 2)
366
367; Получить значение атома можно посредством '@'
368; (провести так называемую операцию dereference)
369my-atom ;=> Atom<#...> (Возвращает объект типа Atom)
370@my-atom ; => {:a 1 :b 2}
371
372; Пример реализации счётчика на атоме:
373(def counter (atom 0))
374(defn inc-counter []
375 (swap! counter inc))
376
377(inc-counter)
378(inc-counter)
379(inc-counter)
380(inc-counter)
381(inc-counter)
382
383@counter ; => 5
384
385; С другими STM-конструкциями - refs и agents - можно ознакомиться тут:
386; Refs: http://clojure.org/refs
387; Agents: http://clojure.org/agents
Для будущего чтения ¶
Это руководство не претендует на полноту, но мы смеем надеяться, способно вызвать интерес к дальнейшему изучению языка.
Сайт Clojure.org содержит большое количество статей по языку: http://clojure.org/
Clojuredocs.org — сайт документации языка с примерами использования функций: http://clojuredocs.org/quickref/Clojure%20Core
4Clojure — отличный способ закрепить навыки программирования на clojure, решая задачи вместе с коллегами со всего мира: https://4clojure.oxal.org/
Clojure-doc.org (да, именно) неплохой перечень статей для начинающих: http://clojure-doc.org/