Skip to content

Instantly share code, notes, and snippets.

@dmitry-osin
Last active December 18, 2024 22:09
Show Gist options
  • Save dmitry-osin/33aa857aa2a78305a929ba69a743aed3 to your computer and use it in GitHub Desktop.
Save dmitry-osin/33aa857aa2a78305a929ba69a743aed3 to your computer and use it in GitHub Desktop.
Шпаргалка по языку Clojure

Шпаргалка по языку Clojure

Clojure - это современный диалект языка Lisp, разработанный для Java Virtual Machine (JVM).

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

Пространства имен в Clojure - это механизм для организации и группировки кода. Они похожи на модули или пакеты в других языках программирования. Вот ключевые аспекты пространств имен в Clojure:

Определение и структура

  • Каждый файл Clojure обычно представляет собой отдельное пространство имен.
  • Пространство имен определяется в начале файла с помощью формы ns:
(ns my-project.core)
  • Имя пространства имен обычно соответствует пути к файлу в структуре проекта.

Импорт и использование

  • Для импорта функций из других пространств имен используется require:
(ns my-namespace
  (:require [other-namespace :as other]))
  • Можно импортировать отдельные функции:
(ns my-namespace
  (:require [other-namespace :refer [specific-function]]))
  • Или импортировать все функции:
(ns my-namespace
  (:require [other-namespace :refer :all]))

Псевдонимы и переименование

  • Пространства имен можно переименовывать для удобства:
(ns my-namespace
  (:require [very-long-namespace-name :as short-name]))

Приватные функции

  • Функции, определенные с defn-, являются приватными и доступны только внутри своего пространства имен.

Организация кода

  • Пространства имен помогают логически группировать связанные функции и данные.
  • Они предотвращают конфликты имен между различными частями программы.

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

Базовый синтаксис

Примитивные типы данных

  • Числа: 42 (целое), -1.5 (с плавающей точкой), 22/7 (рациональное)
  • Строки: "hello"
  • Символы: \a, \newline
  • Ключевые слова: :keyword, :my.namespace/keyword
  • Символы: my-symbol, my.namespace/symbol
  • Булевы значения: true, false
  • Null-значение: nil

Коллекции

  • Векторы: [1 2 3]
  • Списки: '(1 2 3) или (list 1 2 3)
  • Множества: #{1 2 3}
  • Ассоциативные массивы: {:key1 "value1", :key2 "value2"}

Управляющие конструкции

В Clojure существует несколько основных управляющих конструкций:

Условные конструкции

if Простейшая условная конструкция:

(if condition
  then-expression
  else-expression)

Вот несколько примеров использования if в Clojure:

  1. Простое условное выражение:
(if (> 5 3)
  "5 больше 3"
  "5 не больше 3")
; Результат: "5 больше 3"
  1. Использование с функциями:
(defn check-age [age]
  (if (>= age 18)
    "Совершеннолетний"
    "Несовершеннолетний"))

(check-age 20) ; Результат: "Совершеннолетний"
(check-age 15) ; Результат: "Несовершеннолетний"
  1. Вложенные if:
(defn grade [score]
  (if (>= score 90)
    "A"
    (if (>= score 80)
      "B"
      (if (>= score 70)
        "C"
        "F"))))

(grade 85) ; Результат: "B"
  1. Использование с let:
(let [x 10]
  (if (even? x)
    (str x " четное")
    (str x " нечетное")))
; Результат: "10 четное"
  1. if без else:
(if true
  (println "Это выполнится"))
; Выводит: Это выполнится
; Результат: nil

if в Clojure является выражением и всегда возвращает значение. Если ветка else не указана и условие ложно, if возвращает nil.

when Выполняет блок кода, если условие истинно:

(when condition
  expression1
  expression2)

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

(defn greet-user [name]
  (when (not-empty name)
    (println "Hello," name)
    (println "Welcome to our application!")))

(greet-user "Alice")

В этом примере:

  1. Функция greet-user принимает аргумент name.
  2. when проверяет, что name не пустой, используя функцию not-empty.
  3. Если условие истинно, выполняются оба выражения внутри when:
    • Печатается приветствие с именем пользователя
    • Печатается дополнительное сообщение

Если name пустой, ничего не происходит, и функция неявно возвращает nil.

when особенно полезен, когда нужно выполнить несколько выражений при истинном условии, но не требуется обрабатывать ложный случай. В отличие от if, when имеет неявный do и не требует ветки else.

cond Множественное ветвление:

(cond
  condition1 result1
  condition2 result2
  :else default-result)

cond в Clojure используется для множественного ветвления, когда нужно проверить несколько условий последовательно. Вот пример использования cond:

(defn grade-student [score]
  (cond
    (>= score 90) "A"
    (>= score 80) "B"
    (>= score 70) "C"
    (>= score 60) "D"
    :else "F"))

(println (grade-student 85))  ; Выведет "B"
(println (grade-student 92))  ; Выведет "A"
(println (grade-student 45))  ; Выведет "F"

В этом примере:

  1. Функция grade-student принимает числовой аргумент score.
  2. cond последовательно проверяет условия:
    • Если score >= 90, возвращается "A"
    • Если score >= 80, возвращается "B"
    • И так далее
  3. Последнее условие :else служит как "catch-all" и выполняется, если ни одно из предыдущих условий не было истинным.

cond оценивает пары условие-результат сверху вниз и возвращает результат первого истинного условия. Если ни одно условие не истинно и нет :else, cond возвращает nil.

Этот макрос особенно полезен, когда нужно обработать множество различных случаев, и использование вложенных if сделало бы код менее читаемым.

Циклы и итерации

doseq Выполняет тело для каждого элемента последовательности:

(doseq [item collection]
  (println item))

doseq в Clojure используется для итерации по последовательности и выполнения побочных эффектов. Вот пример использования doseq:

(doseq [n [0 1 2]]
  (println n))

Этот код выведет:

0
1
2

В данном примере:

  1. doseq итерирует по вектору [0 1 2]
  2. На каждой итерации значение присваивается переменной n
  3. Тело doseq выполняет println для каждого значения n

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

Важно отметить, что doseq, в отличие от for, не возвращает значение. Он используется именно для выполнения побочных эффектов, а не для создания новых последовательностей.

dotimes Выполняет тело заданное количество раз:

(dotimes [i 5]
  (println i))

dotimes в Clojure используется для выполнения блока кода заданное количество раз. Вот пример использования dotimes:

(dotimes [i 5]
  (println "Iteration" i))

Этот код выведет:

Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4

В этом примере:

  1. dotimes создает локальную переменную i, которая принимает значения от 0 до 4 (всего 5 итераций).
  2. Тело dotimes выполняется для каждого значения i.
  3. На каждой итерации выводится строка "Iteration" и текущее значение i.

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

Важно помнить, что dotimes, как и doseq, используется в основном для выполнения побочных эффектов и не возвращает значение (точнее, всегда возвращает nil).

Последовательное выполнение

do Выполняет несколько выражений последовательно:

(do
  expression1
  expression2
  expression3)

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

(defn perform-actions [x]
  (do
    (println "Starting actions")
    (println "Processing value:" x)
    (let [result (* x 2)]
      (println "Result is:" result)
      result)))

(perform-actions 5)

Этот код выведет:

Starting actions
Processing value: 5
Result is: 10
10

В этом примере:

  1. Функция perform-actions использует do для группировки нескольких выражений.
  2. Внутри do выполняются три действия: два вывода на экран и вычисление результата.
  3. Последнее выражение в do (в данном случае result) становится возвращаемым значением всего блока.

do часто используется в следующих ситуациях:

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

Важно отметить, что многие формы в Clojure (например, let, defn, when) имеют неявный do, поэтому в них не требуется явно использовать do для группировки выражений.

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

Функции в Clojure

В Clojure существует несколько способов определения функций:

Основные способы

1. Использование defn

Это наиболее распространенный способ определения именованных функций:

(defn hello [name]
  (str "Hello " name))

2. Анонимные функции с fn

Для создания безымянных функций используется специальная форма fn:

(fn [x y] (+ x y))

3. Сокращенный синтаксис анонимных функций

Clojure предоставляет краткую форму записи анонимных функций:

#(+ %1 %2)

Дополнительные возможности

Мультиметодные функции

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

(defn hello
  ([] "Hello World")
  ([name] (str "Hello " name)))

Функции с переменным числом аргументов

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

(defn sum [& numbers]
  (apply + numbers))

Деструктуризация в аргументах

При определении функции можно использовать деструктуризацию:

(defn sum [[x y]] (+ x y))

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

Функции -> (thread-first) и ->> (thread-last) в Clojure используются для объединения операций и улучшения читаемости кода. Они позволяют выразить последовательность операций над данными в более линейной форме.

Thread-first (->)

  • Вставляет каждое выражение как первый аргумент следующей формы.
  • Подходит для функций, которые принимают основной аргумент первым.
  • Часто используется с функциями, работающими со структурами данных, такими как assoc, update, dissoc, get.

Пример:

(-> person
    :hair-color
    name
    clojure.string/upper-case)

Thread-last (->>)

  • Вставляет каждое выражение как последний аргумент следующей формы.
  • Идеально подходит для работы с последовательностями и функциями, принимающими коллекцию последним аргументом.
  • Часто используется с функциями map, filter, reduce, into.

Пример:

(->> (range 10)
     (filter odd?)
     (map #(* % %))
     (reduce +))

Выбор между -> и ->>

Выбор макроса зависит от сигнатуры используемых функций:

  • Используйте -> для навигации по вложенным структурам и работы с отдельными значениями.
  • Применяйте ->> для операций с последовательностями и коллекциями.

Преимущества использования

  1. Улучшает читаемость кода, особенно при работе с длинными цепочками преобразований.
  2. Уменьшает вложенность выражений.
  3. Позволяет легко добавлять или удалять шаги в цепочке операций.

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

Деструктуризация в Clojure

В Clojure существует несколько способов определения функций с использованием деструктуризации:

Деструктуризация аргументов функции

1. Деструктуризация векторов и списков:

(defn print-coordinates [[x y]]
  (println "X:" x "Y:" y))

(print-coordinates [10 20])

2. Деструктуризация карт:

(defn greet [{:keys [name location]}]
  (println "Hey there" name ", how's the weather in" location "?"))

(greet {:name "Josie" :location "San Francisco"})

3. Деструктуризация с значениями по умолчанию:

(defn greet [{:keys [name location] :or {location "Unknown"}}]
  (println "Hello" name "from" location))

(greet {:name "John"})

Деструктуризация в теле функции

4. Использование let для деструктуризации:

(defn area [triangle]
  (let [{b :base h :height} triangle]
    (* b h 0.5)))

(area {:base 5 :height 10})

5. Деструктуризация с использованием :as:

(defn process-numbers [numbers]
  (let [[first second & rest :as all] numbers]
    (println "First:" first)
    (println "Second:" second)
    (println "Rest:" rest)
    (println "All:" all)))

(process-numbers [1 2 3 4 5])

6. Деструктуризация вложенных структур:

(defn process-user [{{:keys [street city]} :address :keys [name]}]
  (println name "lives in" city "on" street))

(process-user {:name "Alice" :address {:street "123 Main St" :city "Wonderland"}})

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

Работа с коллекциями

Основные функции:

  • count: подсчет элементов
  • conj: добавление элемента
  • first, second, last: доступ к элементам
  • map, filter, reduce: функции высшего порядка

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

  • Вектор: [1 2 3] или (vector 1 2 3)
  • Список: '(1 2 3) или (list 1 2 3)
  • Множество: #{1 2 3}
  • Ассоциативный массив: {:key1 "value1", :key2 "value2"}

Основные операции

  • count: подсчет элементов
  • conj: добавление элемента
  • first: получение первого элемента
  • rest: получение всех элементов кроме первого
  • nth: получение элемента по индексу
  • get: получение элемента по ключу или индексу

count

Функция count в Clojure используется для подсчета количества элементов в коллекции. Вот несколько примеров использования count:

  1. Подсчет элементов в векторе:
(count [1 2 3 4 5])
; Результат: 5
  1. Подсчет символов в строке:
(count "Hello, World!")
; Результат: 13
  1. Подсчет элементов в списке:
(count '(a b c d))
; Результат: 4
  1. Подсчет элементов в множестве:
(count #{:a :b :c})
; Результат: 3
  1. Подсчет пар ключ-значение в ассоциативном массиве:
(count {:a 1 :b 2 :c 3})
; Результат: 3
  1. Использование с пустыми коллекциями:
(count [])
; Результат: 0
(count "")
; Результат: 0
  1. Подсчет элементов в последовательности:
(count (range 10))
; Результат: 10

Функция count работает эффективно для большинства типов коллекций в Clojure, предоставляя быстрый способ определения размера коллекции.

conj

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

  1. Добавление элемента в вектор (в конец):
(conj [1 2 3] 4)
; Результат: [1 2 3 4]
  1. Добавление элемента в список (в начало):
(conj '(2 3 4) 1)
; Результат: (1 2 3 4)
  1. Добавление элемента в множество:
(conj #{1 2 3} 4)
; Результат: #{1 2 3 4}
  1. Добавление пары ключ-значение в ассоциативный массив:
(conj {:a 1 :b 2} [:c 3])
; Результат: {:a 1, :b 2, :c 3}
  1. Добавление нескольких элементов:
(conj [1 2] 3 4 5)
; Результат: [1 2 3 4 5]
  1. Добавление элементов к пустой коллекции:
(conj [] 1 2 3)
; Результат: [1 2 3]

conj создает новую коллекцию, не изменяя исходную, что соответствует принципам неизменяемости в Clojure.

first

Функция first в Clojure используется для получения первого элемента последовательности или коллекции. Вот несколько примеров использования first:

  1. Получение первого элемента вектора:
(first [1 2 3 4 5])
; Результат: 1
  1. Получение первого элемента списка:
(first '(a b c d))
; Результат: a
  1. Получение первого символа строки:
(first "Hello")
; Результат: \H
  1. Работа с пустыми коллекциями:
(first [])
; Результат: nil
  1. Получение первой пары ключ-значение из ассоциативного массива:
(first {:a 1 :b 2 :c 3})
; Результат: [:a 1]
  1. Использование с бесконечными последовательностями:
(first (range))
; Результат: 0
  1. Комбинирование с другими функциями:
(first (filter odd? [2 4 6 7 8 9]))
; Результат: 7

Функция first часто используется в сочетании с другими функциями для обработки последовательностей и коллекций в функциональном стиле программирования.

rest

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

  1. Получение остатка вектора:
(rest [1 2 3 4 5])
; Результат: (2 3 4 5)
  1. Получение остатка списка:
(rest '(a b c d))
; Результат: (b c d)
  1. Получение остатка строки:
(rest "Hello")
; Результат: (\e \l \l \o)
  1. Работа с пустыми коллекциями:
(rest [])
; Результат: ()
  1. Использование с бесконечными последовательностями:
(take 5 (rest (range)))
; Результат: (1 2 3 4 5)
  1. Комбинирование с другими функциями:
(rest (filter odd? [1 3 5 7 9]))
; Результат: (3 5 7 9)

Важно отметить, что rest всегда возвращает последовательность, даже если исходная коллекция пуста. Это отличает ее от функции next, которая возвращает nil для пустых коллекций.

nth

Функция nth в Clojure используется для получения элемента последовательности по его индексу. Вот несколько примеров использования nth:

  1. Получение элемента вектора по индексу:
(nth [10 20 30 40 50] 2)
; Результат: 30
  1. Получение элемента списка:
(nth '(a b c d e) 3)
; Результат: d
  1. Получение символа из строки:
(nth "Hello" 1)
; Результат: \e
  1. Использование с необязательным значением по умолчанию:
(nth [1 2 3] 5 :not-found)
; Результат: :not-found
  1. Работа с бесконечными последовательностями:
(nth (iterate inc 1) 10)
; Результат: 11
  1. Использование отрицательных индексов (вызовет исключение):
(nth [1 2 3] -1)
; Вызовет IndexOutOfBoundsException

nth эффективно работает с индексированными коллекциями (векторы, строки), но может быть менее эффективным для последовательностей, требующих линейного прохода (например, списки).

get

Функция get в Clojure используется для получения значения по ключу из ассоциативных структур данных или элемента по индексу из индексированных коллекций. Вот несколько примеров использования get:

  1. Получение значения из ассоциативного массива:
(get {:a 1 :b 2 :c 3} :b)
; Результат: 2
  1. Получение элемента вектора по индексу:
(get [10 20 30 40 50] 2)
; Результат: 30
  1. Использование значения по умолчанию:
(get {:a 1 :b 2} :c "not found")
; Результат: "not found"
  1. Работа с вложенными структурами:
(get-in {:user {:name "John" :age 30}} [:user :name])
; Результат: "John"
  1. Использование с множествами:
(get #{:a :b :c} :b)
; Результат: :b
  1. Получение значения из строки по индексу:
(get "Hello" 1)
; Результат: \e

get безопасно работает с nil и возвращает nil, если ключ не найден (если не указано значение по умолчанию).

Преобразование коллекций

  • map: применение функции к каждому элементу
  • filter: фильтрация элементов по предикату
  • remove: удаление элементов по предикату
  • reduce: свертка коллекции
  • into: добавление элементов одной коллекции в другую

map

Функция map в Clojure применяет заданную функцию к каждому элементу последовательности (или нескольких последовательностей) и возвращает новую последовательность результатов. Вот несколько примеров использования map:

  1. Применение функции к одной последовательности:
(map inc [1 2 3 4 5])
; Результат: (2 3 4 5 6)
  1. Использование анонимной функции:
(map #(* % %) [1 2 3 4 5])
; Результат: (1 4 9 16 25)
  1. Применение функции к нескольким последовательностям:
(map + [1 2 3] [4 5 6])
; Результат: (5 7 9)
  1. Работа со строками:
(map clojure.string/upper-case ["a" "b" "c"])
; Результат: ("A" "B" "C")
  1. Использование с функцией, принимающей несколько аргументов:
(map #(format "%s: %d" %1 %2) ["A" "B" "C"] [1 2 3])
; Результат: ("A: 1" "B: 2" "C: 3")
  1. Комбинирование с другими функциями:
(->> [1 2 3 4 5]
     (map #(* % 2))
     (filter even?))
; Результат: (2 4 6 8 10)

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

filter

Функция filter в Clojure используется для создания новой последовательности, содержащей только те элементы исходной последовательности, которые удовлетворяют заданному предикату. Вот несколько примеров использования filter:

  1. Фильтрация четных чисел:
(filter even? [1 2 3 4 5 6])
; Результат: (2 4 6)
  1. Использование анонимной функции:
(filter #(> % 3) [1 2 3 4 5 6])
; Результат: (4 5 6)
  1. Фильтрация строк по длине:
(filter #(> (count %) 3) ["a" "ab" "abc" "abcd" "abcde"])
; Результат: ("abcd" "abcde")
  1. Фильтрация ключей в ассоциативном массиве:
(filter (fn [[k v]] (keyword? k)) 
        {:a 1 :b 2 "c" 3 "d" 4})
; Результат: ([:a 1] [:b 2])
  1. Комбинирование с другими функциями:
(->> [1 2 3 4 5 6 7 8 9 10]
     (filter odd?)
     (map #(* % %)))
; Результат: (1 9 25 49 81)
  1. Фильтрация с использованием предиката на основе индекса:
(filter-indexed #(odd? %1) ["a" "b" "c" "d" "e"])
; Результат: ("b" "d")

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

remove

Функция remove в Clojure используется для создания новой последовательности, исключая элементы, удовлетворяющие заданному предикату. По сути, это противоположность функции filter. Вот несколько примеров использования remove:

  1. Удаление четных чисел:
(remove even? [1 2 3 4 5 6])
; Результат: (1 3 5)
  1. Использование анонимной функции:
(remove #(< % 3) [1 2 3 4 5 6])
; Результат: (3 4 5 6)
  1. Удаление пустых строк:
(remove empty? ["" "a" "" "b" "c" ""])
; Результат: ("a" "b" "c")
  1. Удаление элементов из вектора по условию:
(remove #(= (count %) 3) ["a" "ab" "abc" "abcd"])
; Результат: ("a" "ab" "abcd")
  1. Комбинирование с другими функциями:
(->> (range 1 11)
     (remove even?)
     (map #(* % %)))
; Результат: (1 9 25 49 81)
  1. Удаление ключей из ассоциативного массива:
(into {} (remove (fn [[k v]] (keyword? k)) 
                 {:a 1 :b 2 "c" 3 "d" 4}))
; Результат: {"c" 3, "d" 4}

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

reduce

Функция reduce в Clojure используется для свертки последовательности в одно значение путем последовательного применения функции к элементам. Вот несколько примеров использования reduce:

  1. Суммирование чисел:
(reduce + [1 2 3 4 5])
; Результат: 15
  1. Нахождение максимального значения:
(reduce max [3 7 2 9 1 5])
; Результат: 9
  1. Конкатенация строк:
(reduce str ["Hello" " " "World" "!"])
; Результат: "Hello World!"
  1. Использование начального значения:
(reduce + 10 [1 2 3 4 5])
; Результат: 25 (10 + 1 + 2 + 3 + 4 + 5)
  1. Создание ассоциативного массива:
(reduce (fn [acc [k v]] (assoc acc k (* v v)))
        {}
        {:a 1 :b 2 :c 3})
; Результат: {:a 1, :b 4, :c 9}
  1. Подсчет частоты элементов:
(reduce (fn [acc x] (update acc x (fnil inc 0)))
        {}
        [1 2 3 2 1 3 3 4])
; Результат: {1 2, 2 2, 3 3, 4 1}

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

into

Функция into в Clojure используется для добавления всех элементов из одной коллекции в другую. Вот несколько примеров использования into:

  1. Объединение двух векторов:
(into [1 2 3] [4 5 6])
; Результат: [1 2 3 4 5 6]
  1. Добавление элементов в множество:
(into #{:a :b} [:b :c :d])
; Результат: #{:a :b :c :d}
  1. Преобразование последовательности в map:
(into {} [[:a 1] [:b 2] [:c 3]])
; Результат: {:a 1, :b 2, :c 3}
  1. Объединение двух map'ов:
(into {:a 1 :b 2} {:c 3 :d 4})
; Результат: {:a 1, :b 2, :c 3, :d 4}
  1. Преобразование строки в вектор символов:
(into [] "hello")
; Результат: [\h \e \l \l \o]

Функция into особенно полезна, когда нужно объединить коллекции разных типов или преобразовать один тип коллекции в другой.

Работа с последовательностями

  • take: взять первые n элементов
  • drop: отбросить первые n элементов
  • take-while: брать элементы, пока выполняется условие
  • drop-while: отбрасывать элементы, пока выполняется условие
  • partition: разбиение на подпоследовательности

take

Функция take в Clojure используется для получения первых n элементов из последовательности. Вот несколько примеров использования take:

  1. Получение первых трех элементов из вектора:
(take 3 [1 2 3 4 5])
; Результат: (1 2 3)
  1. Работа с бесконечными последовательностями:
(take 5 (range))
; Результат: (0 1 2 3 4)
  1. Получение элементов из строки:
(take 4 "Hello, World!")
; Результат: (\H \e \l \l)
  1. Использование с пустой коллекцией:
(take 3 [])
; Результат: ()
  1. Получение всех элементов, если n больше длины коллекции:
(take 10 [1 2 3])
; Результат: (1 2 3)
  1. Комбинирование с другими функциями:
(take 3 (filter even? (range)))
; Результат: (0 2 4)

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

drop

Функция drop в Clojure используется для создания последовательности, отбрасывая первые n элементов из исходной последовательности. Вот несколько примеров использования drop:

  1. Отбрасывание первых трех элементов вектора:
(drop 3 [1 2 3 4 5 6])
; Результат: (4 5 6)
  1. Работа с бесконечными последовательностями:
(take 5 (drop 10 (range)))
; Результат: (10 11 12 13 14)
  1. Отбрасывание символов из строки:
(apply str (drop 7 "Hello, World!"))
; Результат: "World!"
  1. Использование с пустой коллекцией:
(drop 3 [])
; Результат: ()
  1. Отбрасывание всех элементов, если n больше длины коллекции:
(drop 10 [1 2 3 4 5])
; Результат: ()
  1. Комбинирование с другими функциями:
(drop-while even? [2 4 6 7 8 9])
; Результат: (7 8 9)

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

take-while

Функция take-while в Clojure используется для создания последовательности, содержащей элементы из начала исходной последовательности, пока предикат возвращает true. Как только предикат возвращает false для какого-либо элемента, take-while прекращает работу. Вот несколько примеров использования take-while:

  1. Получение чисел меньше 5:
(take-while #(< % 5) [1 2 3 4 5 6 7])
; Результат: (1 2 3 4)
  1. Получение положительных чисел:
(take-while pos? [3 2 1 0 -1 -2 3 4])
; Результат: (3 2 1)
  1. Работа со строками:
(take-while #(not= % \space) "Hello World")
; Результат: (\H \e \l \l \o)
  1. Использование с бесконечными последовательностями:
(take-while #(< % 1000) (map #(* % %) (range)))
; Результат: (0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400 441 484 529 576 625 676 729 784 841 900 961)
  1. Комбинирование с другими функциями:
(->> (range)
     (map #(* % 3))
     (take-while #(< % 20)))
; Результат: (0 3 6 9 12 15 18)

take-while особенно полезна, когда нужно получить элементы последовательности, удовлетворяющие определенному условию, до первого элемента, не удовлетворяющего этому условию.

drop-while

Функция drop-while в Clojure используется для создания последовательности, отбрасывая элементы из начала исходной последовательности, пока предикат возвращает true. Как только предикат возвращает false для какого-либо элемента, drop-while возвращает оставшуюся часть последовательности. Вот несколько примеров использования drop-while:

  1. Отбрасывание чисел меньше 5:
(drop-while #(< % 5) [1 2 3 4 5 6 7 4 3])
; Результат: (5 6 7 4 3)
  1. Отбрасывание положительных чисел:
(drop-while pos? [3 2 1 0 -1 -2 3 4])
; Результат: (0 -1 -2 3 4)
  1. Работа со строками:
(apply str (drop-while #(not= % \space) "Hello World"))
; Результат: " World"
  1. Использование с бесконечными последовательностями:
(take 5 (drop-while #(< % 10) (range)))
; Результат: (10 11 12 13 14)
  1. Комбинирование с другими функциями:
(->> (range)
     (map #(* % 2))
     (drop-while #(< % 10))
     (take 5))
; Результат: (10 12 14 16 18)

drop-while особенно полезна, когда нужно пропустить начальные элементы последовательности, удовлетворяющие определенному условию, и начать обработку с первого элемента, не удовлетворяющего этому условию.

partition

Функция partition в Clojure используется для разбиения последовательности на подпоследовательности заданного размера. Вот несколько примеров использования partition:

  1. Разбиение на группы по 3 элемента:
(partition 3 [1 2 3 4 5 6 7 8 9])
; Результат: ((1 2 3) (4 5 6) (7 8 9))
  1. Разбиение с шагом:
(partition 2 3 [1 2 3 4 5 6 7 8 9])
; Результат: ((1 2) (4 5) (7 8))
  1. Использование с заполнителем:
(partition 3 3 [:pad] [1 2 3 4 5 6 7 8 9 10])
; Результат: ((1 2 3) (4 5 6) (7 8 9) (10 :pad :pad))
  1. Работа со строками:
(partition 3 "abcdefghijklm")
; Результат: ((\a \b \c) (\d \e \f) (\g \h \i) (\j \k \l))
  1. Использование с бесконечными последовательностями:
(take 4 (partition 2 (range)))
; Результат: ((0 1) (2 3) (4 5) (6 7))

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

Проверки

  • some: проверка наличия элемента, удовлетворяющего предикату
  • every?: проверка, удовлетворяют ли все элементы предикату
  • empty?: проверка на пустоту
  • distinct?: проверка на уникальность элементов

some

Функция some в Clojure используется для проверки, удовлетворяет ли хотя бы один элемент коллекции заданному предикату. Вот несколько примеров использования some:

  1. Проверка наличия четного числа в коллекции:
(def numbers [1 3 5 7 8 9])

(some even? numbers)

Этот код вернет true, так как в коллекции есть четное число (8).

  1. Поиск первого элемента, удовлетворяющего условию:
(def fruits ["apple" "banana" "cherry" "date"])

(some #(when (.startsWith % "b") %) fruits)

Этот пример вернет "banana", так как это первый элемент, начинающийся с буквы "b".

  1. Проверка наличия элемента в множестве:
(def allowed-colors #{:red :green :blue})

(some allowed-colors [:yellow :green :purple])

Здесь some вернет :green, так как это первый элемент из последовательности, который присутствует в множестве allowed-colors.

some возвращает первое логически истинное (truthy) значение, возвращенное предикатом, или nil, если ни один элемент не удовлетворяет условию. Это делает some более гибким по сравнению с every?, так как он может использоваться не только для проверки условия, но и для поиска конкретных элементов.

every?

Функция every? в Clojure используется для проверки, удовлетворяют ли все элементы коллекции определенному предикату. Вот пример использования every?:

(def numbers [2 4 6 8 10])

(every? even? numbers)

Этот код вернет true, так как все числа в векторе numbers являются четными.

Другой пример:

(def toons ["Daffy Duck" "Bugs Bunny" "Elmer Fudd" "Yosemite Sam"])

(defn two-names? [name]
  (= 2 (count (clojure.string/split name #" "))))

(every? two-names? toons)

Здесь every? проверяет, состоит ли каждое имя персонажа из двух слов. Результат будет true, так как все имена в векторе toons состоят из двух слов.

every? возвращает true только если предикат возвращает true для всех элементов коллекции. Если хотя бы для одного элемента предикат вернет false, то every? вернет false.

empty?

Функция empty? в Clojure используется для проверки, является ли коллекция пустой. Она возвращает true, если коллекция не содержит элементов, и false в противном случае. Вот несколько примеров использования empty?:

  1. Проверка пустого вектора:
(empty? [])  ; Возвращает true
(empty? [1 2 3])  ; Возвращает false
  1. Проверка пустой строки:
(empty? "")  ; Возвращает true
(empty? "hello")  ; Возвращает false
  1. Проверка пустого списка:
(empty? '())  ; Возвращает true
(empty? '(1 2 3))  ; Возвращает false
  1. Проверка пустого множества:
(empty? #{})  ; Возвращает true
(empty? #{1 2 3})  ; Возвращает false
  1. Использование в условных выражениях:
(defn greet [name]
  (if (empty? name)
    "Hello, stranger!"
    (str "Hello, " name "!")))

(greet "")  ; Возвращает "Hello, stranger!"
(greet "Alice")  ; Возвращает "Hello, Alice!"

empty? часто используется для проверки входных данных, управления потоком выполнения программы или в качестве условия в функциях высшего порядка, работающих с коллекциями.

distinct?

Функция distinct? в Clojure используется для проверки, являются ли все переданные аргументы уникальными. Вот несколько примеров использования distinct?:

  1. Проверка уникальности отдельных аргументов:
(distinct? 1 2 3)  ; Возвращает true
(distinct? 1 2 2)  ; Возвращает false
  1. Проверка уникальности элементов в коллекции:
(apply distinct? [1 2 3])  ; Возвращает true
(apply distinct? [1 2 2 3])  ; Возвращает false

Обратите внимание, что при работе с коллекциями необходимо использовать apply, чтобы передать элементы коллекции как отдельные аргументы функции distinct?.

  1. Проверка уникальности символов:
(distinct? 'A 'B 'C)  ; Возвращает true
(distinct? 'A 'A 'C)  ; Возвращает false
  1. Работа с разными типами данных:
(distinct? 1 "a" :keyword)  ; Возвращает true
(distinct? 1 1.0 "1")  ; Возвращает true, так как это разные типы

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

(defn coll-distinct? [coll]
  (= (distinct coll) coll))

(coll-distinct? [1 2 3])  ; Возвращает true
(coll-distinct? [1 2 2 3])  ; Возвращает false

Эта вспомогательная функция сравнивает исходную коллекцию с результатом применения distinct к ней.

Сортировка и группировка

  • sort: сортировка элементов
  • sort-by: сортировка по ключу
  • group-by: группировка элементов по ключу

sort

Функция sort в Clojure используется для сортировки коллекций. Вот несколько способов использования sort:

  1. Простая сортировка коллекции:
(sort [3 1 4 1 5 9 2 6])
; Результат: (1 1 2 3 4 5 6 9)
  1. Сортировка с использованием компаратора:
(sort > [3 1 4 1 5 9 2 6])
; Результат: (9 6 5 4 3 2 1 1)
  1. Сортировка разных типов данных:
(sort [22/7 2.71828 ##-Inf 1 55 3N])
; Результат: (##-Inf 1 2.71828 3N 22/7 55)
  1. Сортировка строк:
(sort ["aardvark" "boo" "a" "Antelope" "bar"])
; Результат: ("Antelope" "a" "aardvark" "bar" "boo")
  1. Сортировка с nil значениями:
(sort [2 6 nil 7 4])
; Результат: (nil 2 4 6 7)

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

sort-by

Функция sort-by в Clojure используется для сортировки коллекции на основе значений, полученных путем применения заданной функции к каждому элементу. Вот несколько примеров использования sort-by:

  1. Сортировка по длине строк:
(sort-by count ["aaa" "c" "bb" "dddd"])
; Результат: ("c" "bb" "aaa" "dddd")
  1. Сортировка структур данных по определенному полю:
(sort-by :age [{:name "Alice" :age 30} {:name "Bob" :age 25} {:name "Charlie" :age 35}])
; Результат: ({:name "Bob", :age 25} {:name "Alice", :age 30} {:name "Charlie", :age 35})
  1. Использование с компаратором:
(sort-by count > ["aaa" "c" "bb" "dddd"])
; Результат: ("dddd" "aaa" "bb" "c")
  1. Сортировка чисел по их абсолютному значению:
(sort-by #(Math/abs %) [-5 -1 3 2 -4])
; Результат: (-1 2 3 -4 -5)
  1. Сортировка по нескольким критериям:
(sort-by (juxt :last-name :first-name)
         [{:first-name "John" :last-name "Doe"}
          {:first-name "Jane" :last-name "Doe"}
          {:first-name "Alice" :last-name "Smith"}])
; Результат: ({:first-name "Jane", :last-name "Doe"}
;             {:first-name "John", :last-name "Doe"}
;             {:first-name "Alice", :last-name "Smith"})

sort-by особенно полезна, когда нужно сортировать сложные структуры данных или когда критерий сортировки не является прямым сравнением элементов.

group-by

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

  1. Группировка чисел по четности:
(group-by even? [1 2 3 4 5 6])
; Результат: {false [1 3 5], true [2 4 6]}
  1. Группировка слов по первой букве:
(group-by #(first %) ["apple" "banana" "cherry" "date" "elderberry"])
; Результат: {\a ["apple"], \b ["banana"], \c ["cherry"], \d ["date"], \e ["elderberry"]}
  1. Группировка структур данных по полю:
(group-by :age [{:name "Alice" :age 25}
                {:name "Bob" :age 30}
                {:name "Charlie" :age 25}
                {:name "David" :age 30}])
; Результат: {25 [{:name "Alice", :age 25} {:name "Charlie", :age 25}],
;             30 [{:name "Bob", :age 30} {:name "David", :age 30}]}
  1. Группировка по остатку от деления:
(group-by #(mod % 3) (range 10))
; Результат: {0 [0 3 6 9], 1 [1 4 7], 2 [2 5 8]}
  1. Использование с более сложной функцией группировки:
(group-by #(cond
             (< % 0) :negative
             (> % 0) :positive
             :else :zero)
          [-2 -1 0 1 2 3 -4])
; Результат: {:negative [-2 -1 -4], :positive [1 2 3], :zero [0]}

group-by особенно полезна для анализа данных, создания индексов или подготовки данных для дальнейшей обработки.

Операции для конкретных типов коллекций

Векторы

  • get: получение элемента по индексу
  • assoc: замена элемента по индексу
  • subvec: получение подвектора
get

Функция get в Clojure может использоваться для получения элемента вектора по его индексу. Вот несколько примеров использования get с векторами:

  1. Получение элемента по индексу:
(get [10 20 30 40 50] 2)
; Результат: 30
  1. Использование с нулевым индексом:
(get ["a" "b" "c"] 0)
; Результат: "a"
  1. Получение последнего элемента:
(get [1 2 3 4 5] 4)
; Результат: 5
  1. Использование значения по умолчанию:
(get [1 2 3] 5 :not-found)
; Результат: :not-found
  1. Работа с пустым вектором:
(get [] 0)
; Результат: nil

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

assoc

Функция assoc в Clojure может использоваться с векторами для замены элемента по указанному индексу. Вот несколько примеров использования assoc с векторами:

  1. Замена элемента по индексу:
(assoc [0 1 2 3 4] 2 :x)
; Результат: [0 1 :x 3 4]
  1. Замена первого элемента:
(assoc ["a" "b" "c"] 0 "z")
; Результат: ["z" "b" "c"]
  1. Замена последнего элемента:
(assoc [1 2 3 4] 3 5)
; Результат: [1 2 3 5]
  1. Добавление нового элемента в конец вектора:
(assoc [1 2 3] 3 4)
; Результат: [1 2 3 4]
  1. Замена нескольких элементов одновременно:
(assoc [0 1 2 3 4] 1 :a 3 :b)
; Результат: [0 :a 2 :b 4]

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

subvec

Функция subvec в Clojure используется для создания подвектора из существующего вектора. Она позволяет извлечь часть вектора, указав начальный и (опционально) конечный индексы. Вот несколько примеров использования subvec:

  1. Получение подвектора с указанием начального и конечного индексов:
(subvec [0 1 2 3 4 5] 2 4)
; Результат: [2 3]
  1. Получение подвектора от указанного индекса до конца:
(subvec [10 20 30 40 50] 3)
; Результат: [40 50]
  1. Получение пустого подвектора:
(subvec [1 2 3] 2 2)
; Результат: []
  1. Использование с отрицательными индексами (вызовет исключение):
(subvec [1 2 3 4 5] -1 3)
; Вызовет IndexOutOfBoundsException
  1. Получение всего вектора:
(subvec [1 2 3 4 5] 0)
; Результат: [1 2 3 4 5]

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

Множества

  • disj: удаление элемента
  • contains?: проверка наличия элемента
disj?

Функция disj в Clojure используется для удаления одного или нескольких элементов из множества. Она возвращает новое множество без указанных элементов. Вот несколько примеров использования disj с множествами:

  1. Удаление одного элемента из множества:
(disj #{1 2 3 4 5} 3)
; Результат: #{1 2 4 5}
  1. Удаление нескольких элементов:
(disj #{:a :b :c :d} :b :c)
; Результат: #{:a :d}
  1. Попытка удаления элемента, которого нет в множестве:
(disj #{1 2 3} 4)
; Результат: #{1 2 3}
  1. Использование с пустым множеством:
(disj #{} 1)
; Результат: #{}
  1. Удаление всех элементов:
(disj #{1 2 3} 1 2 3)
; Результат: #{}

disj создает новое множество, не изменяя исходное. Если указанный для удаления элемент отсутствует в множестве, функция просто возвращает исходное множество без изменений.

contains?

Функция contains? в Clojure может использоваться с множествами для проверки наличия элемента. Вот несколько примеров использования contains? с множествами:

  1. Проверка наличия элемента в множестве:
(contains? #{:a :b :c} :b)
; Результат: true
  1. Проверка отсутствия элемента:
(contains? #{:a :b :c} :d)
; Результат: false
  1. Работа с числовыми множествами:
(contains? #{1 2 3 4 5} 3)
; Результат: true
  1. Проверка с пустым множеством:
(contains? #{} :a)
; Результат: false

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

Функция contains? для множеств фактически проверяет наличие ключа, но поскольку в множествах ключи и значения совпадают, это эквивалентно проверке наличия значения.

Ассоциативные массивы

  • assoc: добавление пары ключ-значение
  • dissoc: удаление пары по ключу
  • select-keys: выборка по ключам
  • merge: объединение нескольких map'ов
assoc

Функция assoc в Clojure используется с ассоциативными массивами для добавления новых пар ключ-значение или обновления существующих. Вот несколько примеров использования assoc с ассоциативными массивами:

  1. Добавление новой пары ключ-значение:
(assoc {:a 1 :b 2} :c 3)
; Результат: {:a 1, :b 2, :c 3}
  1. Обновление существующего значения:
(assoc {:name "John" :age 30} :age 31)
; Результат: {:name "John", :age 31}
  1. Добавление нескольких пар одновременно:
(assoc {:a 1} :b 2 :c 3 :d 4)
; Результат: {:a 1, :b 2, :c 3, :d 4}
  1. Использование с пустым ассоциативным массивом:
(assoc {} :key "value")
; Результат: {:key "value"}
  1. Обновление вложенных структур:
(assoc {:user {:name "Alice" :age 25}}
       :user {:name "Alice" :age 26})
; Результат: {:user {:name "Alice", :age 26}}

assoc создает новый ассоциативный массив, не изменяя исходный. Это соответствует принципу неизменяемости данных в Clojure.

dissoc

Функция dissoc в Clojure используется с ассоциативными массивами для удаления одной или нескольких пар ключ-значение. Она возвращает новый ассоциативный массив без указанных ключей. Вот несколько примеров использования dissoc:

  1. Удаление одного ключа:
(dissoc {:a 1 :b 2 :c 3} :b)
; Результат: {:a 1, :c 3}
  1. Удаление нескольких ключей:
(dissoc {:name "John" :age 30 :city "New York" :country "USA"} :age :country)
; Результат: {:name "John", :city "New York"}
  1. Попытка удаления несуществующего ключа:
(dissoc {:a 1 :b 2} :c)
; Результат: {:a 1, :b 2}
  1. Удаление всех ключей:
(dissoc {:a 1 :b 2} :a :b)
; Результат: {}
  1. Использование с пустым ассоциативным массивом:
(dissoc {} :key)
; Результат: {}

dissoc создает новый ассоциативный массив, не изменяя исходный, что соответствует принципу неизменяемости данных в Clojure.

select-keys

Функция select-keys в Clojure используется для создания нового ассоциативного массива, содержащего только указанные ключи из исходного ассоциативного массива. Вот несколько примеров использования select-keys:

  1. Выбор нескольких ключей:
(select-keys {:a 1 :b 2 :c 3 :d 4} [:a :c])
; Результат: {:a 1, :c 3}
  1. Выбор одного ключа:
(select-keys {:name "John" :age 30 :city "New York"} [:name])
; Результат: {:name "John"}
  1. Выбор несуществующих ключей:
(select-keys {:a 1 :b 2} [:c :d])
; Результат: {}
  1. Выбор всех существующих ключей:
(select-keys {:x 10 :y 20} [:x :y :z])
; Результат: {:x 10, :y 20}
  1. Использование с пустым ассоциативным массивом:
(select-keys {} [:a :b])
; Результат: {}

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

merge

Функция merge в Clojure используется для объединения двух или более ассоциативных массивов в один новый. При этом, если ключи повторяются, значения из последующих массивов перезаписывают значения из предыдущих. Вот несколько примеров использования merge:

  1. Объединение двух ассоциативных массивов:
(merge {:a 1 :b 2} {:c 3 :d 4})
; Результат: {:a 1, :b 2, :c 3, :d 4}
  1. Перезапись значений при конфликте ключей:
(merge {:a 1 :b 2} {:b 3 :c 4})
; Результат: {:a 1, :b 3, :c 4}
  1. Объединение нескольких ассоциативных массивов:
(merge {:x 1} {:y 2} {:z 3})
; Результат: {:x 1, :y 2, :z 3}
  1. Использование с пустым ассоциативным массивом:
(merge {} {:a 1} {:b 2})
; Результат: {:a 1, :b 2}
  1. Объединение с nil:
(merge {:a 1} nil {:b 2})
; Результат: {:a 1, :b 2}

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

Многопоточность и параллелизм

  • future: асинхронное выполнение
  • atom, ref, agent: управление состоянием

Clojure предоставляет несколько механизмов для управления состоянием и конкурентностью: atom, ref, agent и future. Каждый из них имеет свои особенности и применяется в различных сценариях.

Atom

Atom используется для управления одиночным, независимым значением.

(def counter (atom 0))

(swap! counter inc)  ; Увеличение значения на 1
(reset! counter 10)  ; Установка нового значения
@counter             ; Получение текущего значения

Future

Future используется для асинхронного выполнения задач.

(def long-running-task 
  (future 
    (Thread/sleep 5000)
    (println "Task completed")
    42))

(deref long-running-task)  ; Блокирующее ожидание результата
@long-running-task         ; Сокращенная форма deref
(realized? long-running-task)  ; Проверка завершения

Ref

Ref используется для координированных изменений нескольких значений в рамках транзакции.

(def account1 (ref 1000))
(def account2 (ref 500))

(dosync
  (alter account1 - 100)
  (alter account2 + 100))

@account1  ; 900
@account2  ; 600

Agent

Agent используется для асинхронных, независимых изменений состояния.

(def counter (agent 0))

(send counter inc)  ; Асинхронное увеличение
(await counter)     ; Ожидание завершения всех отправленных действий
@counter            ; Получение текущего значения

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

Макросы

(defmacro my-macro [arg]
  `(println ~arg))

Написание макроса в Clojure включает следующие основные шаги:

  1. Определение макроса с помощью специальной формы defmacro:
(defmacro name [params*]
  body)
  1. Использование синтаксиса для построения кода внутри макроса:
  • Обратная кавычка ` для создания шаблона кода
  • Тильда ~ для вставки значений параметров
  • Тильда с собачкой ~@ для развертывания последовательностей
  1. Возврат формы кода, которая будет вставлена на место вызова макроса

Пример простого макроса:

(defmacro my-when [test & body]
  `(if ~test
     (do ~@body)))

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

При написании макросов важно помнить:

  • Макросы выполняются на этапе компиляции
  • Аргументы макроса не вычисляются до передачи в тело макроса
  • Используйте функцию macroexpand-1 для отладки макросов

Макросы - мощный инструмент метапрограммирования в Clojure, позволяющий расширять синтаксис языка и создавать предметно-ориентированные конструкции.

Взаимодействие с Java

(.method object args)
(Class/staticMethod args)
(new ClassName args)

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

Импорт и использование Java-классов

Для использования Java-классов в Clojure их нужно импортировать:

(import java.util.UUID)

(UUID/randomUUID)

Создание экземпляров Java-классов

Создание экземпляров Java-классов в Clojure выглядит так:

(java.util.HashSet. ["1" "2"])

Вызов методов Java-объектов

Методы Java-объектов вызываются с помощью точки:

(.length "hello world")

Реализация Java-интерфейсов

Для реализации Java-интерфейсов используется макрос reify:

(reify java.io.FilenameFilter
  (accept [this dir name]
    true))

Вызов Clojure из Java

Для вызова Clojure-кода из Java используются классы RT и Var:

RT.loadResourceScript("script.clj");
Var fn = RT.var("namespace", "function");
fn.invoke(1, 2);

Типы данных

Clojure использует Java-типы для представления примитивных значений:

(class 1) ; java.lang.Long
(class "string") ; java.lang.String
(class true) ; java.lang.Boolean

Эта тесная интеграция позволяет эффективно использовать существующие Java-библиотеки и инфраструктуру при разработке на Clojure, сочетая мощь JVM с выразительностью функционального программирования.

Ключевые слова в Clojure

В Clojure ключевые слова (keywords) часто используются в функциях для создания более гибких и выразительных интерфейсов. Вот несколько примеров использования ключевых слов в функциях Clojure:

Функции с именованными аргументами

(defn greet [& {:keys [name greeting] :or {greeting "Hello"}}]
  (str greeting ", " (or name "World") "!"))

(greet)                         ; => "Hello, World!"
(greet :name "Alice")           ; => "Hello, Alice!"
(greet :greeting "Hi" :name "Bob") ; => "Hi, Bob!"

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

Ключевые слова как функции

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

(def user {:name "Hubert" :alias "mrhaki"})

(:name user)    ; => "Hubert"
(:alias user)   ; => "mrhaki"
(:city user)    ; => nil

Это позволяет элегантно извлекать значения из карт.

Деструктуризация с ключевыми словами

(defn process-user [{:keys [name location]}]
  (println name "lives in" location))

(process-user {:name "Alice" :location "Wonderland"})
; Выведет: Alice lives in Wonderland

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

Функции с опциональными аргументами

(defn create-user [username & {:keys [email age] :or {age 18}}]
  {:username username
   :email email
   :age age})

(create-user "john_doe" :email "[email protected]")
; => {:username "john_doe", :email "[email protected]", :age 18}

В этом примере функция create-user принимает обязательный аргумент username и опциональные именованные аргументы :email и :age.

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

Работа с исключениями в Clojure

Работа с исключениями в Clojure основана на механизмах Java, но имеет свои особенности:

Основные конструкции

try/catch/finally:

(try
  (/ 1 0)
  (catch ArithmeticException e
    (println "Caught arithmetic exception:" (ex-message e)))
  (catch Exception e
    (println "Caught general exception:" (ex-message e)))
  (finally
    (println "This always executes")))

Множественные catch блоки:

(try
  (+ 1 nil)
  (catch ArithmeticException e
    (println "Arithmetic error"))
  (catch NullPointerException e
    (println "Null pointer error"))
  (catch Throwable e
    (println "Caught any throwable")))

Особенности Clojure

ex-info и ex-data:

(try
  (throw (ex-info "My error" {:type :custom-error}))
  (catch clojure.lang.ExceptionInfo e
    (let [data (ex-data e)]
      (println "Error type:" (:type data)))))

Функции для работы с исключениями:

  • ex-message: получение сообщения исключения
  • ex-cause: получение причины исключения

Библиотеки

slingshot: Предоставляет расширенные возможности для работы с исключениями:

(use '[slingshot.slingshot :only [try+ throw+]])
(try+
  (throw+ {:type :my-error :message "Oops!"})
  (catch [:type :my-error] {:keys [message]}
    (println "Caught error:" message)))

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

(require '[dire.core :refer [with-handler!]])
(defn my-function [x]
  (/ 1 x))
(with-handler! #'my-function
  java.lang.ArithmeticException
  (fn [e & args]
    (println "Cannot divide by zero")))

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

Работа с JDBC в Clojure

Работа с JDBC в Clojure осуществляется с помощью библиотеки clojure.java.jdbc (или ее современного аналога next.jdbc). Вот основные аспекты работы с JDBC в Clojure:

Подключение к базе данных

Для подключения к базе данных используется спецификация в виде карты:

(def db-spec 
  {:dbtype "postgresql"
   :dbname "mydb"
   :user "myuser"
   :password "mypassword"})

Выполнение запросов

Основные функции для работы с базой данных:

  • execute!: выполнение SQL-запросов
  • query: выполнение SELECT-запросов
  • insert!: вставка данных
  • update!: обновление данных
  • delete!: удаление данных

Пример выполнения запроса:

(jdbc/query db-spec ["SELECT * FROM users WHERE id = ?" 1])

Транзакции

Для работы с транзакциями используется макрос with-db-transaction:

(jdbc/with-db-transaction [t-con db-spec]
  (jdbc/insert! t-con :users {:name "Alice"})
  (jdbc/update! t-con :users {:balance 100} ["name = ?" "Alice"]))

Переиспользование соединений

Макрос with-db-connection позволяет переиспользовать соединение:

(jdbc/with-db-connection [conn db-spec]
  (let [users (jdbc/query conn ["SELECT * FROM users"])]
    (jdbc/insert! conn :logs {:action "Users fetched"})))

Подготовленные выражения

Для оптимизации производительности используются подготовленные выражения:

(jdbc/execute! db-spec ["INSERT INTO users (name, email) VALUES (?, ?)" "Bob" "[email protected]"])

Работа с JDBC в Clojure предоставляет удобный и идиоматичный способ взаимодействия с реляционными базами данных, сочетая мощь SQL с выразительностью Clojure.

Работа с JPA в Clojure

Для работы с JPA в Clojure существует несколько подходов:

Использование библиотеки clj-jpa:

clj-jpa - это библиотека, которая предоставляет удобный интерфейс для работы с JPA в Clojure. Чтобы начать использовать clj-jpa, нужно добавить ее в зависимости проекта:

[clj-jpa "0.1.0-SNAPSHOT"]

Также необходимо добавить реализацию JPA, например Hibernate.

Прямое использование Java-интерфейсов JPA:

Clojure позволяет напрямую работать с Java-классами и интерфейсами. Можно использовать стандартные классы JPA, такие как EntityManager, EntityManagerFactory и т.д.:

(import javax.persistence.Persistence)

(def emf (Persistence/createEntityManagerFactory "myPersistenceUnit"))
(def em (.createEntityManager emf))

Использование макросов для упрощения работы с JPA:

Можно создать макросы, которые будут генерировать код для работы с JPA, делая его более идиоматичным для Clojure:

(defmacro with-transaction [& body]
  `(let [tx# (.getTransaction em)]
     (try
       (.begin tx#)
       (let [result# (do ~@body)]
         (.commit tx#)
         result#)
       (catch Exception e#
         (.rollback tx#)
         (throw e#)))))

Использование библиотек более высокого уровня:

Некоторые Clojure-библиотеки для работы с базами данных, такие как Korma или HoneySQL, могут быть интегрированы с JPA для предоставления более функционального и идиоматичного интерфейса.

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

Создание HTTP сервера в Clojure

Для создания HTTP сервера в Clojure обычно используется библиотека Ring, которая предоставляет низкоуровневый API для обработки HTTP-запросов и ответов. Вот основные шаги для создания простого HTTP сервера в Clojure:

Настройка проекта

  1. Добавьте зависимости в файл project.clj:
:dependencies [[org.clojure/clojure "1.10.0"]
               [ring/ring-core "1.9.5"]
               [ring/ring-jetty-adapter "1.9.5"]]
  1. Импортируйте необходимые библиотеки в вашем файле core.clj:
(ns your-project.core
  (:require [ring.adapter.jetty :as jetty]))

Создание обработчика запросов

Определите функцию-обработчик, которая будет принимать запрос и возвращать ответ:

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

Запуск сервера

Используйте функцию run-jetty для запуска сервера:

(defn -main [& args]
  (jetty/run-jetty handler {:port 3000 :join? false}))

Дополнительные возможности

  • Для маршрутизации запросов можно использовать библиотеку Compojure:
(ns your-project.core
  (:require [compojure.core :refer :all]
            [compojure.route :as route]))

(defroutes app
  (GET "/" [] "Hello World")
  (route/not-found "Not Found"))

(defn -main [& args]
  (jetty/run-jetty app {:port 3000 :join? false}))
  • Для асинхронной обработки запросов и поддержки веб-сокетов можно использовать библиотеку http-kit:
(ns your-project.core
  (:require [org.httpkit.server :as server]))

(defn app [req]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    "hello HTTP!"})

(defn -main [& args]
  (server/run-server app {:port 8080}))

Создание HTTP сервера в Clojure с использованием Ring и других библиотек позволяет легко разрабатывать веб-приложения, используя функциональный подход и мощные возможности языка.

Работа с графической библиотекой Swing

Для работы с графическими компонентами Swing из Clojure можно использовать следующие подходы:

Прямое использование Java-классов Swing

Clojure позволяет напрямую импортировать и использовать Java-классы Swing:

(import [javax.swing JFrame JLabel])

(def frame (JFrame. "My Window"))
(def label (JLabel. "Hello Swing!"))
(.add frame label)
(.setSize frame 300 200)
(.setVisible frame true)

Библиотека clj.swing

Библиотека clj.swing предоставляет обертки для многих компонентов и методов из пакетов java.awt и javax.swing:

(ns my-app.core
  (:require [clj.swing :as swing]))

(def frame (swing/frame {:title "My Window"}))
(def label (swing/label "Hello Swing!"))
(swing/add frame label)
(swing/show! frame)

Макросы для упрощения создания интерфейса

Можно создать макросы для более декларативного описания интерфейса:

(defmacro with-swing [& body]
  `(SwingUtilities/invokeLater
     (fn [] ~@body)))

(with-swing
  (let [frame (JFrame. "My App")
        button (JButton. "Click me")]
    (.add frame button)
    (.pack frame)
    (.setVisible frame true)))

Интеграция с существующими Java-приложениями

Clojure можно использовать для расширения существующих Java Swing приложений, добавляя новую функциональность или изменяя поведение компонентов.

При работе со Swing из Clojure важно помнить о многопоточности и использовать SwingUtilities/invokeLater для обновления GUI из других потоков. Также стоит обратить внимание на управление ресурсами и правильное закрытие окон.

DSL на Clojure

Простая реализация DSL для HTML на Clojure

(ns html-dsl.core)

(defn- escape-html [text]
  (clojure.string/escape text
    {\< "&lt;"
     \> "&gt;"
     \& "&amp;"
     \" "&quot;"
     \' "&#39;"}))

(defn- attr-str [attrs]
  (if (empty? attrs)
    ""
    (str " " (clojure.string/join " " 
               (for [[k v] attrs]
                 (str (name k) "=\"" (escape-html (str v)) "\""))))))

(defn tag [name attrs & content]
  (str "<" (clojure.string/lower-case (name name))
       (attr-str attrs)
       (if (empty? content)
         "/>"
         (str ">" (apply str content) "</" (clojure.string/lower-case (name name)) ">"))))

(defmacro html [& content]
  `(str "<!DOCTYPE html>"
        (tag :html {} ~@content)))

(defmacro defelem [name & args]
  `(defmacro ~name ~@args
     (list 'tag '~name ~@args)))

(defelem head [& content])
(defelem body [& content])
(defelem div [& content])
(defelem p [& content])
(defelem a [& content])
(defelem h1 [& content])
(defelem ul [& content])
(defelem li [& content])

Теперь можно использовать этот DSL для создания HTML-структур:

(ns my-app.core
  (:require [html-dsl.core :refer :all]))

(def my-page
  (html
    (head
      (tag :title {} "My Awesome Page"))
    (body
      (h1 {} "Welcome to My Page")
      (p {} "This is a paragraph.")
      (div {:class "content"}
        (ul {}
          (li {} "Item 1")
          (li {} "Item 2")
          (li {} "Item 3"))
        (p {}
          "Check out "
          (a {:href "https://clojure.org"} "Clojure's website"))))))

(println my-page)

Этот код создаст следующий HTML:

<!DOCTYPE html><html><head><title>My Awesome Page</title></head><body><h1>Welcome to My Page</h1><p>This is a paragraph.</p><div class="content"><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><p>Check out <a href="https://clojure.org">Clojure's website</a></p></div></body></html>

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

Простая реализация DSL для CSS на Clojure

(ns css-dsl.core)

(defn css-rule [selector properties]
  (let [props-str (clojure.string/join "; " 
                    (map (fn [[k v]] (str (name k) ": " v)) properties))]
    (str selector " { " props-str " }")))

(defn css [rules]
  (clojure.string/join "\n" rules))

(defmacro nested-css [selector & body]
  `(let [nested-rules# (partition 2 (quote ~body))
         formatted-rules# (map (fn [[sel# props#]]
                                 (css-rule (str ~selector " " sel#) props#))
                               nested-rules#)]
     (clojure.string/join "\n" formatted-rules#)))

(def stylesheet 
  (css
    [(css-rule "body" {:margin "0" :font-family "Arial, sans-serif"})
     (css-rule ".header" {:background-color "#333" :color "white" :padding "10px"})
     (nested-css ".content"
       h1 {:color "#444" :font-size "28px"}
       p {:color "#666" :line-height "1.6"})
     (css-rule ".footer" {:background-color "#222" 
                          :color "white" 
                          :text-align "center" 
                          :padding "10px"})]))

(println stylesheet)

Этот DSL для CSS на Clojure позволяет создавать стили с помощью функций и макросов, что делает код более читаемым и близким к структуре CSS. Вы можете использовать css-rule для создания отдельных правил CSS, css для объединения нескольких правил в таблицу стилей, и nested-css для создания вложенных правил.

Простая реализация DSL для SQL на Clojure

Вот пример реализации простого DSL для SQL на Clojure:

(ns sql-dsl.core
  (:require [clojure.string :as str]))

(defn- format-where [conditions]
  (when (seq conditions)
    (str " WHERE " (str/join " AND " (map (fn [[k v]] (str (name k) " = ?")) conditions)))))

(defn- format-set [values]
  (str/join ", " (map (fn [[k v]] (str (name k) " = ?")) values)))

(defn select [table & {:keys [columns where]}]
  (let [cols (if (seq columns) (str/join ", " (map name columns)) "*")]
    (str "SELECT " cols " FROM " (name table)
         (format-where where))))

(defn insert [table values]
  (let [cols (str/join ", " (map name (keys values)))
        vals (str/join ", " (repeat (count values) "?"))]
    (str "INSERT INTO " (name table) " (" cols ") VALUES (" vals ")")))

(defn update [table values & {:keys [where]}]
  (str "UPDATE " (name table)
       " SET " (format-set values)
       (format-where where)))

(defn delete [table & {:keys [where]}]
  (str "DELETE FROM " (name table)
       (format-where where)))

;; Пример использования
(defn example-queries []
  (println (select :users :columns [:id :name :email] :where {:active true}))
  (println (insert :users {:name "John" :email "[email protected]"}))
  (println (update :users {:email "[email protected]"} :where {:id 1}))
  (println (delete :users :where {:id 1})))

(example-queries)

Этот DSL позволяет создавать базовые SQL-запросы (SELECT, INSERT, UPDATE, DELETE) с помощью функций Clojure. Вот что выведет example-queries:

SELECT id, name, email FROM users WHERE active = ?
INSERT INTO users (name, email) VALUES (?, ?)
UPDATE users SET email = ? WHERE id = ?
DELETE FROM users WHERE id = ?

Этот DSL использует подготовленные выражения (prepared statements) для безопасной работы с параметрами. Вы можете расширить этот DSL, добавив поддержку JOIN, ORDER BY, LIMIT и других SQL-конструкций.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment