Как Макар начал clojure учить часть 3
История о том, как я начал учить clojure часть 3
В предыдущих сериях
Переходим к абстракции коллекций в Clojure. Разберем концепции into
и conj
.
Абстракция коллекций vs. Последовательностей
- Последовательности (Sequences): Акцент на поэлементной обработке (
map
,filter
,reduce
). Аналог в JS: итераторы и методы массивов типаmap()
. - Коллекции (Collections): Акцент на структуре данных как целом (операции над всей коллекцией). Аналог в JS: свойства массивов/объектов типа
length
.
Примеры коллекционных функций:
(count [1 2 3]) ; => 3 (аналог JS: array.length)
(empty? []) ; => true (аналог JS: array.length === 0)
Функция into
: Трансформация коллекций
into
преобразует результат последовательных операций обратно в исходный тип коллекции.
(map identity {:a 1}) ; => ([:a 1]) ; Seq, а не Map!
(into {} (map identity {:a 1})) ; => {:a 1} ; Вернули к типу Map
Почему это важно?
Большинство функций последовательностей (map
, filter
) возвращают seq, а не исходный тип; into
конвертирует seq обратно в нужный тип:
(into [] (map inc [1 2 3])) ; => [2 3 4] ; Вектор
(into #{} (map identity [:a :a :b])) ; => #{:a :b} ; Set (уникальные значения)
Расширенное использование
Можно добавлять данные в существующую коллекцию:
(into {:x 0} [[:y 1] [:z 2]]) ; => {:x 0, :y 1, :z 2}
; Аналог JS: Object.assign({x: 0}, [['y', 1], ['z', 2]])
Функция conj
: Добавление элементов
conj
добавляет элементы в коллекцию с сохранением типа.
(conj [1 2] 3) ; => [1 2 3]
(conj #{1 2} 3) ; => #{1 2 3}
Ключевые отличия от into
Особенность | conj |
into |
---|---|---|
Аргументы | Элементы (не коллекция!) | Коллекция элементов |
Поведение | Добавляет элементы по одному | Объединяет коллекции |
Пример | (conj [0] 1) → [0 1] |
(into [0] [1]) → [0 1] |
Ошибка | (conj [0] [1]) → [0 [1]] |
(into [0] [1]) → [0 1] |
Расширенные сценарии
;; Добавление в разные типы коллекций
(conj [:a] :b :c) ; => [:a :b :c]
(conj {:x 0} [:y 1] [:z 2]) ; => {:x 0, :y 1, :z 2}
;; Эквивалентность через into
(defn my-conj [target & additions]
(into target additions))
(my-conj [1] 2 3) ; => [1 2 3]
Смотрим через призму JavaScript
Проблема преобразований:
// JS: map возвращает новый массив const arr = [1, 2, 3]; const mapped = arr.map((x) => x * 2); // [2, 4, 6]
В Clojure
map
возвращает seq, а не вектор.into
решает эту проблему.Добавление элементов:
// JS: разные методы для разных типов arr.push(4); // Добавление в массив set.add(4); // Добавление в Set
В Clojure
conj
универсален для всех коллекций.Иммутабельность:
Обе функции (conj
иinto
) возвращают новые коллекции (как в Redux, но на уровне языка).
Паттерны использования
Конвейерная обработка:
(->> (range 10) (map inc) (filter even?) (into [])) ; Преобразуем обратно в вектор
Построение коллекций:
(reduce conj [] [1 2 3]) ; => [1 2 3]
Работа с разными типами:
(conj (list 1 2) 3) ; => (3 1 2) ; Добавление в начало списка (conj [1 2] 3) ; => [1 2 3] ; Добавление в конец вектора
Производительность и подводные камни
Persistent Data Structures:
Обе функции используют персистентные структуры данных (возвращают новые версии с общим доступом к неизмененным частям).Выбор между
conj
иinto
:- Используй
conj
для добавления отдельных элементов - Используй
into
для объединения коллекций или преобразования типов
- Используй
Особенность списков:
(conj '(1 2) 0) ; => (0 1 2) ; Добавление в начало (into '(1 2) [0]) ; => (0 1 2) ; То же самое
Абстракция коллекций - это операций над целыми структурами данных, а не отдельными элементами
into
- инструмент для:- Преобразования seq → конкретный тип коллекции
- Объединения коллекций
- Работы с гетерогенными типами данных
conj
- единый интерфейс для добавления элементов в любые коллекцииФилософия Clojure:
- Четкое разделение абстракций (последовательности vs коллекции)
- Универсальные функции для разных типов данных
- Иммутабельность как основа
Любая коллекция в Clojure реализует интерфейс
Seqable
, поэтому можно получить последовательность из любой коллекции с помощьюseq
.Для полного соответствия терминологии: в Clojure "sequence" - это не тип данных, а абстракция для поэлементного обхода, а коллекции - это конкретные структуры данных.
Функции высшего порядка: Clojure vs JavaScript
В JavaScript ты знаком с функциями как объектами первого класса. В Clojure это доведено до совершенства:
- Функции могут принимать другие функции как аргументы
- Функции могут возвращать новые функции
- Более лаконичный синтаксис для функциональных операций
Рассмотрим ключевые функции:
1. apply
: "Разворачивание" коллекций
Некоторые функции ожидают отдельные аргументы, а не коллекцию
(max [1 2 3]) ; => [1 2 3] (не то, что нужно!)
apply
преобразует коллекцию в последовательность аргументов
(apply max [1 2 3]) ; => 3
Эквивалентно вызову (max 1 2 3)
Аналогия в JavaScript:
// Без apply
Math.max([1, 2, 3]); // NaN
// С apply
Math.max.apply(null, [1, 2, 3]); // 3
Продвинутое использование:
(defn my-into [target additions]
(apply conj target additions))
(my-into [0] [1 2 3]) ; => [0 1 2 3]
Здесь apply
преобразует вектор [1 2 3]
в три отдельных аргумента для conj
2. partial
: Частичное применение функций
Создание новой функции с предустановленными аргументами
(def add10 (partial + 10))
(add10 3) ; => 13
Как работает:
- Принимает функцию и часть аргументов
- Возвращает новую функцию
- При вызове новой функции объединяет аргументы
Реализация через apply
:
(defn my-partial [f & fixed-args]
(fn [& more-args]
(apply f (concat fixed-args more-args))))
(def add20 (my-partial + 20))
(add20 3) ; => 23
Практическое применение:
(defn log [level message]
(condp = level
:warn (clojure.string/lower-case message)
:emergency (clojure.string/upper-case message)))
(def warn (partial log :warn))
(warn "Danger!") ; => "danger!"
Отличие от JavaScript:
// JavaScript
const add10 = x => 10 + x;
// или
const add10partial = (x => (a, b) => x + b).bind(null, 10);
// Clojure
(def add10 (partial + 10))
3. complement
: Функциональное отрицание
Проблема: Часто нужно инвертировать логику предиката
(filter #(not (vampire? %)) suspects)
Решение: complement
создает инвертированную версию функции
(def not-vampire? (complement vampire?))
(filter not-vampire? suspects)
Реализация:
(defn my-complement [f]
(fn [& args]
(not (apply f args))))
(def my-pos? (my-complement neg?))
(my-pos? 5) ; => true
Аналог в JavaScript:
const complement =
(f) =>
(...args) =>
!f(...args);
const notVampire = complement(vampire);
Практический пример: Анализ вампиров
Разберем финальный пример из книги с полным стеком функциональных операций:
Задача: Анализ CSV с подозреваемыми в вампиризме
1. Подготовка данных:
; Ключи для преобразования
(def vamp-keys [:name :glitter-index])
; Функции преобразования
(def conversions
{:name identity
:glitter-index #(Integer. %)})
2. Парсинг CSV:
(defn parse [csv]
(map #(clojure.string/split % #",")
(clojure.string/split csv #"\n")))
3. Преобразование в мапы:
(defn mapify [rows]
(map (fn [row]
(reduce
(fn [m [k v]] (assoc m k ((conversions k) v)))
{}
(map vector vamp-keys row)))
rows))
4. Фильтрация:
(defn glitter-filter [min records]
(filter #(>= (:glitter-index %) min) records))
Весь пайплайн:
(->> "suspects.csv"
slurp
parse
mapify
(glitter-filter 3))
Аналог в современном JavaScript:
// С использованием lodash/fp
import { split, map, filter, flow } from 'lodash/fp';
const parse = split('\n');
const mapify = map((row) => {
const [name, glitter] = split(',', row);
return { name, glitter: Number(glitter) };
});
const glitterFilter = (min) => filter((item) => item.glitter >= min);
const process = flow(parse, mapify, glitterFilter(3));
process(csvData);
Ключевые отличия от JavaScript
- Более глубокая интеграция: В Clojure функции высшего порядка - основа языка
- Неизменяемость: Все операции возвращают новые данные
- Последовательности: Единый интерфейс для работы с коллекциями
- Ленивые вычисления: Автоматическая оптимизация цепочек операций
- Макросы: Возможность создавать собственные операторы (вроде
->>
)
Советы для эксперта:
- Композиция > Наследование: Используй
partial
иcomplement
для создания специализированных функций - Пайплайны: Цепочки преобразований через
->
(thread-first) и->>
(thread-last) - Функции-фабрики: Создавай функции, возвращающие кастомизированные функции
(defn make-filter [key threshold]
(partial filter #(>= (key %) threshold)))
(def glitter-filter (make-filter :glitter-index 3))
- Тестирование: Функции высшего порядка легко тестировать, так как они чистые
Эти техники позволяют писать выразительный, декларативный код, который легко компоновать и поддерживать. Именно это делает Clojure мощным инструментом для работы с данными!