Макар Кузьмичев
Назад

Как Макар начал clojure учить часть 3

История о том, как я начал учить clojure часть 3

В предыдущих сериях

  1. Часть 1
  2. Часть 2

Переходим к абстракции коллекций в 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

  1. Проблема преобразований:

    // JS: map возвращает новый массив
    const arr = [1, 2, 3];
    const mapped = arr.map((x) => x * 2); // [2, 4, 6]
    

    В Clojure map возвращает seq, а не вектор. into решает эту проблему.

  2. Добавление элементов:

    // JS: разные методы для разных типов
    arr.push(4); // Добавление в массив
    set.add(4); // Добавление в Set
    

    В Clojure conj универсален для всех коллекций.

  3. Иммутабельность:
    Обе функции (conj и into) возвращают новые коллекции (как в Redux, но на уровне языка).

Паттерны использования

  1. Конвейерная обработка:

    (->> (range 10)
         (map inc)
         (filter even?)
         (into [])) ; Преобразуем обратно в вектор
    
  2. Построение коллекций:

    (reduce conj [] [1 2 3]) ; => [1 2 3]
    
  3. Работа с разными типами:

    (conj (list 1 2) 3)   ; => (3 1 2) ; Добавление в начало списка
    (conj [1 2] 3)        ; => [1 2 3] ; Добавление в конец вектора
    

Производительность и подводные камни

  1. Persistent Data Structures:
    Обе функции используют персистентные структуры данных (возвращают новые версии с общим доступом к неизмененным частям).

  2. Выбор между conj и into:

    • Используй conj для добавления отдельных элементов
    • Используй into для объединения коллекций или преобразования типов
  3. Особенность списков:

    (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

Как работает:

  1. Принимает функцию и часть аргументов
  2. Возвращает новую функцию
  3. При вызове новой функции объединяет аргументы

Реализация через 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

  1. Более глубокая интеграция: В Clojure функции высшего порядка - основа языка
  2. Неизменяемость: Все операции возвращают новые данные
  3. Последовательности: Единый интерфейс для работы с коллекциями
  4. Ленивые вычисления: Автоматическая оптимизация цепочек операций
  5. Макросы: Возможность создавать собственные операторы (вроде ->>)

Советы для эксперта:

  1. Композиция > Наследование: Используй partial и complement для создания специализированных функций
  2. Пайплайны: Цепочки преобразований через -> (thread-first) и ->> (thread-last)
  3. Функции-фабрики: Создавай функции, возвращающие кастомизированные функции
(defn make-filter [key threshold]
  (partial filter #(>= (key %) threshold)))

(def glitter-filter (make-filter :glitter-index 3))
  1. Тестирование: Функции высшего порядка легко тестировать, так как они чистые

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

Больше всякого можно найти тут:Канал в telegram