summaryrefslogtreecommitdiffhomepage
path: root/ru-ru/clojure-ru.html.markdown
blob: e1d68e5abb69a1c7066f56b7129464cfddd3b6e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
---
language: clojure
filename: learnclojure-ru.clj
contributors:
    - ["Adam Bard", "http://adambard.com/"]
    - ["Alexey Pirogov", "http://twitter.com/alex_pir"]
lang: ru-ru
---

Clojure, это представитель семейства Lisp-подобных языков, разработанный
для Java Virtual Machine. Язык идейно гораздо ближе к чистому
[функциональному программированию](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) чем его прародитель Common Lisp, но в то же время обладает набором инструментов для работы с состоянием,
таких как [STM](https://ru.wikipedia.org/wiki/Software_transactional_memory).

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

(Последующие примеры кода предполагают выполнение в Clojure версии 1.2 и выше)


```clojure
; Комментарии начинаются символом ";".

; Код на языке Clojure записывается в виде "форм",
; которые представляют собой обычные списки элементов, разделенных пробелами,
; заключённые в круглые скобки
;
; Clojure Reader (инструмент языка, отвечающий за чтение исходного кода),
; анализируя форму, предполагает, что первым элементом формы (т.е. списка)
; является функция или макрос, который следует вызвать, передав ему
; в качестве аргументов остальные элементы списка-формы.

; Первым вызовом в файле должен быть вызов функции ns,
; которая отвечает за выбор текущего пространства имен (namespace)
(ns learnclojure-ru)

; Несколько простых примеров:

; str объединяет в единую строку все свои аргументы
(str "Hello" " " "World") ; => "Hello World"

; Арифметика тоже выглядит несложно
(+ 1 1) ; => 2
(- 2 1) ; => 1
(* 1 2) ; => 2
(/ 2 1) ; => 2

; Проверка на равенство (Equality)
(= 1 1) ; => true
(= 2 1) ; => false

; Для булевой логики вам может понадобиться not
(not true) ; => false

; Вложенные формы, конечно же, допустимы и работают вполне предсказуемо
(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2

; Типы
;;;;;;;;;;;;;

; Clojure использует типы Java для представления булевых значений,
; строк и чисел
; Узнать тип мы можем, использую функцию `class
(class 1)     ; Целочисленные литералы типа по-умолчанию являются java.lang.Long
(class 1.)    ; Числа с плавающей точкой, это java.lang.Double
(class "")    ; Строки всегда заключаются в двойные кавычки
              ; и представляют собой java.lang.String
(class false) ; Булевы значения, это экземпляры java.lang.Boolean
(class nil)   ; "Пустое" значение называется "nil"

; Если Вы захотите создать список из чисел, вы можете просто
; предварить форму списка символом "'", который подскажет Reader`у,
; что эта форма не требует вычисления
'(+ 1 2) ; => (+ 1 2)
; ("'", это краткая запись формы (quote (+ 1 2))

; "Квотированный" список можно вычислить, передав его функции eval
(eval '(+ 1 2)) ; => 3

; Коллекции и Последовательности
;;;;;;;;;;;;;;;;;;;

; Списки (Lists) в clojure структурно представляют собой "связанные списки",
; тогда как Векторы (Vectors), устроены как массивы.
; Векторы и Списки тоже являются классами Java!
(class [1 2 3]); => clojure.lang.PersistentVector
(class '(1 2 3)); => clojure.lang.PersistentList

; Список может быть записан, как (1 2 3), но в этом случае
; он будет воспринят reader`ом, как вызов функции.
; Есть два способа этого избежать:
; '(1 2 3)     - квотирование,
; (list 1 2 3) - явное конструирование списка с помощью функции list.

; "Коллекции", это некие наборы данных
; И списки, и векторы являются коллекциями:
(coll? '(1 2 3)) ; => true
(coll? [1 2 3]) ; => true

; "Последовательности" (seqs), это абстракция над наборами данных,
; элементы которых "упакованы" последовательно.
; Списки - последовательности, а вектора - нет.
(seq? '(1 2 3)) ; => true
(seq? [1 2 3]) ; => false

; Любая seq предоставляет доступ только к началу последовательности данных,
; не предоставляя информацию о её длине.
; При этом последовательности могут быть и бесконечными,
; т.к. являются ленивыми и предоставляют данные только по требованию!
(range 4) ; => (0 1 2 3)
(range) ; => (0 1 2 3 4 ...) (бесконечная последовательность!)
(take 4 (range)) ;  (0 1 2 3)

; Добавить элемент в начало списка или вектора можно с помощью функции 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))
; = (conj (conj (conj [] 3) 2) 1)
; => [3 2 1]

; Функции
;;;;;;;;;;;;;;;;;;;;;

; Функция создается специальной формой fn.
; "Тело"" функции может состоять из нескольких форм,
; но результатом вызова функции всегда будет результат вычисления
; последней из них.
(fn [] "Hello World") ; => fn

; (Вызов функции требует "оборачивания" fn-формы в форму вызова)
((fn [] "Hello World")) ; => "Hello World"

; Назначить значению имя можно специальной формой def
(def x 1)
x ; => 1

; Назначить имя можно любому значению, в т.ч. и функции:
(def hello-world (fn [] "Hello World"))
(hello-world) ; => "Hello World"

; Поскольку именование функций - очень частая операция,
; clojure позволяет, сделать это проще:
(defn hello-world [] "Hello World")

; Вектор [] в форме описания функции, следующий сразу за именем,
; описывает параметры функции:
(defn hello [name]
  (str "Hello " name))
(hello "Steve") ; => "Hello Steve"

; Одна функция может иметь сразу несколько наборов аргументов:
(defn hello3
  ([] "Hello World")
  ([name] (str "Hello " name)))
(hello3 "Jake") ; => "Hello Jake"
(hello3) ; => "Hello World"

; Также функция может иметь набор аргументов переменной длины
(defn count-args [& args] ; args будет содержать seq аргументов
  (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"

; Для создания анонимных функций есть специальный синтаксис:
; функциональные литералы
(def hello2 #(str "Hello " %1))
(hello2 "Fanny") ; => "Hello Fanny"

; такие функциональные литералы удобно использовать с map, filter и reduce
(map #(* 10 %1) [1 2 3 5])          ; => (10 20 30 50)
(filter #(> %1 3) [1 2 3 4 5 6 7])  ; => (4 5 6 7)
(reduce #(str %1 "," %2) [1 2 3 4]) ; => "1,2,3,4"

; Отображения (Maps)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Hash maps и array maps имеют одинаковый интерфейс.
; Hash maps производят поиск по ключу быстрее, но не сохраняют порядок ключей
(class {:a 1 :b 2 :c 3})          ; => clojure.lang.PersistentArrayMap
(class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap

; Array maps автоматически преобразуются в hash maps,
; как только разрастутся до определенного размера

; Отображения могут использовать в качестве ключей любые хэшируемые значения,
; однако предпочтительными являются ключи,
; являющиеся "ключевыми словами" (keywords)
(class :a) ; => clojure.lang.Keyword

(def stringmap {"a" 1, "b" 2, "c" 3})
stringmap  ; => {"a" 1, "b" 2, "c" 3}

(def keymap {:a 1, :b 2, :c 3})
keymap ; => {:a 1, :c 3, :b 2}

; Предыдущий пример содержит запятые в коде, однако reader не использует их,
; при обработке литералов - запятые просто воспринимаются,
; как "пробельные символы" (whitespaces)

; Отображение может выступать в роли функции, возвращающей значение по ключу
(stringmap "a")          ; => 1
(keymap :a)              ; => 1

; При попытке получить отсутствующее значение, будет возвращён nil
(stringmap "d") ; => nil

; Иногда бывает удобно указать конкретное значение по-умолчанию:
({:a 1 :b 2} :c "Oops!") ; => "Oops!"

; Keywords тоже могут использоваться в роли функций!
(:b keymap) ; => 2

; Однако этот фокус не пройдёт со строками.
;("a" stringmap)
; => Exception: java.lang.String cannot be cast to clojure.lang.IFn

; Добавить пару ключ-значение в отображение можно функцией assoc
(def newkeymap (assoc keymap :d 4))
newkeymap ; => {:a 1, :b 2, :c 3, :d 4}

; Но всегда следует помнить, что значения в Clojure - неизменяемые!
keymap ; => {:a 1, :b 2, :c 3} - оригинал не был затронут

; dissoc позволяет исключить значение по ключу
(dissoc keymap :a :b) ; => {:c 3}

; Множества (Sets)
;;;;;;;;;;;;;;;;;;

(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}

; Вызов множества, как функции, позволяет проверить
; принадлежность элемента этому множеству:
(#{1 2 3} 1) ; => 1
(#{1 2 3} 4) ; => nil

; В пространстве имен clojure.sets
; содержится множество функций для работы с множествами

; Полезные формы
;;;;;;;;;;;;;;;;;

; Конструкции ветвления в clojure, это обычные макросы
; и подобны их собратьям в других языках:
(if false "a" "b") ; => "b"
(if false "a") ; => nil

; Специальная форма let позволяет присвоить имена значениям локально.
; При этом все изменения будут видны только вложенным формам:
(def a 10)
(let [a 1 b 2]
  (> a b)) ; => false

; Несколько форм можно объединить в одну форму посредством do
; Значением 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")

; Модули
;;;;;;;;;

; Форма "use" позволяет добавить в текущее пространство имен
; все имена (вместе со значениями) из указанного модуля:
(use 'clojure.set)

; Теперь нам доступны операции над множествами:
(intersection #{1 2 3} #{2 3 4}) ; => #{2 3}
(difference #{1 2 3} #{2 3 4}) ; => #{1}

; use позволяет указать, какие конкретно имена
; должны быть импортированы из модуля:
(use '[clojure.set :only [intersection]])

; Также модуль может быть импортирован формой require
(require 'clojure.string)

; После этого модуль становится доступе в текущем пространстве имен,
; а вызов его функций может быть осуществлен указанием полного имени функции:
(clojure.string/blank? "") ; => true

; Импортируемому модулю можно назначить короткое имя:
(require '[clojure.string :as str])
(str/replace "This is a test." #"[a-o]" str/upper-case) ; => "THIs Is A tEst."
; (Литерал вида #"" обозначает регулярное выражение)

; Вместо отдельной формы require (и use, хотя это и не приветствуется) можно
; указать необходимые модули прямо в форме ns:
(ns test
  (:require
    [clojure.string :as str] ; Внимание: при указании внутри формы ns
    [clojure.set :as set]))  ; имена пакетов не квотируются!

; Java
;;;;;;;

; Стандартная библиотека Java очень богата,
; и всё это богатство доступно и для Clojure!

; import позволяет импортировать модули Java
(import java.util.Date)

; В том числе и из ns
(ns test
  (:import java.util.Date
           java.util.Calendar))

; Имя класса, сопровождаемое символом "." позволяет
; инстанцировать объекты Java-классов:
(Date.) ; <a date object>

; форма . позволяет вызывать методы:
(. (Date.) getTime) ; <a timestamp>
(.getTime (Date.))  ; а можно и так

; Статические методы вызываются как функции модуля:
(System/currentTimeMillis) ; <a timestamp> (Модуль system всегда доступен!)

; 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 предоставляет набор инструментов
; для работы с изменяемым состоянием: Software Transactional Memory.
; Структуры STM представлены тремя типами:
; - атомы (atoms)
; - агенты (agents)
; - ссылки (references)

; Самые простые хранители состояния - атомы:
(def my-atom (atom {})) ; {} - начальное состояние атома

; Обновляется атом посредством swap!.
; swap! применяет функцию аргумент к текущему значению
; атома и помещает в атом результат
(swap! my-atom assoc :a 1) ; Обновляет my-atom, помещая в него (assoc {} :a 1)
(swap! my-atom assoc :b 2) ; Обновляет my-atom, помещая в него (assoc {:a 1} :b 2)

; Получить значение атома можно посредством '@'
; (провести так называемую операцию dereference)
my-atom  ;=> Atom<#...> (Возвращает объект типа Atom)
@my-atom ; => {:a 1 :b 2}

; Пример реализации счётчика на атоме
(def counter (atom 0))
(defn inc-counter []
  (swap! counter inc))

(inc-counter)
(inc-counter)
(inc-counter)
(inc-counter)
(inc-counter)

@counter ; => 5

; С другими STM-конструкциями - refs и agents - можно ознакомиться тут:
; Refs: http://clojure.org/refs
; Agents: http://clojure.org/agents
```

### Для будущего чтения

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

Clojure.org - сайт содержит большое количество статей по языку:
[http://clojure.org/](http://clojure.org/)

Clojuredocs.org - сайт документации языка с примерами использования функций:
[http://clojuredocs.org/quickref/Clojure%20Core](http://clojuredocs.org/quickref/Clojure%20Core)

4Clojure - отличный способ закрепить навыки программирования на clojure, решая задачи вместе с коллегами со всего мира:
[http://www.4clojure.com/](http://www.4clojure.com/)

Clojure-doc.org (да, именно) неплохой перечень статей для начинающих:
[http://clojure-doc.org/](http://clojure-doc.org/)