clojure.md

Личный сайт Go-разработчика из Казани

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/