Функциональное программирование - это сейчас ажиотаж, выходящий из академической сферы и помогающий крупным компаниям решать сложные проблемы. Вы можете выполнять функции с объектно-ориентированными языками, такими как C #, или даже с языками с несколькими парадигмами, такими как Javascript, но есть некоторые языки, которые обращаются к функциональной парадигме по дизайну, например Clojure.

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

Все еще здесь? Думаю, вы бросили вызов себе, чтобы выбраться из зоны комфорта! Эта статья покажет вам минимальную теорию и языковые конструкции, которые вам нужно знать, чтобы начать писать простые программы на Clojure, и как (даже не) настроить среду, чтобы начать играть. Наконец, я собираюсь поделиться некоторыми ресурсами, если вы хотите глубоко нырять.

Вступление

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

Что это обозначает? По диалекту LISP (обработка списков) вам просто нужно знать, что программа clojure представлена ​​списками. Да, все является списком, даже вызовом функции. Ты это видишь:

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

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

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

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

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

Однако вы можете изменить «переменные». Но в Clojure переменная просто связывает имя со структурой данных. Поскольку все структуры данных неизменяемы, это имя представляет собой снимок чего-либо в данный момент. Рич Хики, создатель Clojure и доброжелательный диктатор, объясняет это самым простым способом:

В определенный момент телефонный номер Марко был 1234–5678. Но потерял телефон, и у него новый номер телефона: 1111–2222. Старый номер не менял, у него новый. Но мы по-прежнему называем это номером телефона Марко. Вот как работает привязка в clojure.

Говоря языком, восхваляющим чистоту программ и отсутствие побочных эффектов, мы можем сказать, что Clojure затрудняет написание изменяемого, неявного, императивного кода. Но Clojure знает, что программа, предназначенная для решения реальных проблем, должна иметь дело с побочными эффектами, такими как печать для вывода, чтение файловой системы или запись в базу данных. Вот почему в Clojure есть функции, которые создают побочные эффекты, такие как do doseq и dotimes. Это некоторые из немногих стандартных функций библиотеки, которым не нужно что-то возвращать (хотя на самом деле они возвращают nil). Код Clojure очень четко указывает, где размещать побочные эффекты.

Структуры данных

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

Коллекции неизменны, настойчивы и абстрактны. Вы не можете изменить значение коллекции, вместо этого вы возвращаете новую. Но новый разделяет пространство памяти с последним (поэтому он и называется постоянным), так что это очень дешевая операция. И абстрактно, потому что операции Clojure с такими коллекциями, как map filter и reduce, не заботятся о типе коллекции, над которой они работают, поэтому эти методы работают одинаково среди них.

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

Векторы

С clojure.org:

Векторы - это индексированная последовательная структура данных. Векторы обозначаются [ ] следующим образом:
[1 2]

Вы можете думать о векторах, таких как массивы Java / C #. Они проиндексированы, что означает, что вы можете получить элемент по индексу:

Вы можете добавить новый элемент в массив с помощью функции conj. Он добавит элемент в конец массива:

Списки

С clojure.org:

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

Списки представлены ( ), как ("Brazil" "France" "Italy").

Добавление элементов в список:

Вы видите ' перед (".cs" ".js")? Цитата не является оператором оценки. Вы могли заметить, что вызов функции в clojure выполняется с теми же скобками, что и списки. Это потому, что программа clojure на самом деле является диалектом «обработки списков» (LISP), поэтому она представлена ​​кучей списков (и других структур данных, таких как векторы, карты…). Поэтому, когда Clojure видит список, он пытается оценить первый элемент как функцию, а остальные как аргументы функции, как в (inc 1). Это функция с именем inc и уникальным аргументом 1. Он возвращает 2, потому что увеличивает число 1. Чтобы отличить вызовы функций от объявлений функций, то есть (inc 1) от (1 2), нам нужно заключить список в кавычки, поэтому Clojure не пытается вызвать первый элемент списка как функцию.

Получить элементы из списка можно разными способами:

Наборы

Наборы - это коллекции с уникальными значениями. Они представлены #{ }, как в #{1 2 3}. Вот некоторые операции над наборами:

Карты

Карты - это коллекции, которые сопоставляют ключи со значениями. Они представлены чередующимися ключами и значениями в окружении { }, как в {:name "Marco" :job "Software Engineer"}. Вы можете разделить пары ключ / значение запятыми, чтобы улучшить читаемость, как в {:name "Marco", :job "Software Engineer"}, поскольку clojure обрабатывает запятые как пробелы.

Операции на картах очень похожи на те, что вы видели в предыдущих коллекциях. Карты отлично подходят для хранения данных домена. Вы можете думать о них как о литералах объектов Javascript (без прототипа), ассоциативных массивах PHP, а если вы пришли из Java / C #, вы могли бы использовать их как DTO / POCOs / POJOs.

Функции

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

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

Позвольте мне показать, что:

В первой строке мы вызываем функцию if. Он оценивает логический результат своего второго аргумента (= (+ 1 2) 3 ) и, если он истинен, возвращает третий аргумент, строку "Equals 3", или же возвращает четвертый аргумент, "Not equals 3"string.

Давайте углубимся во второй аргумент: (= (+ 1 2) 3 ). Мы вызываем функцию =, которая возвращает истину, когда оба аргумента равны. Первый аргумент - (+ 1 2). Мы вызываем функцию +, которая суммирует все свои аргументы и, следовательно, возвращает 3, поэтому (= (+ 1 2) 3 ) возвращает true, а функция if возвращает значение true, выводя Equals 3.

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

Функция вызывается в clojure в следующей форме:

(function-name param1 param2 paramN)

Функции определяются с помощью формы fn, вектора аргументов и его тела:

(fn [name] (str "Hello, " name))

Мы определили функцию, которая получает единственный аргумент, имя символа, и возвращает конкатенацию Hello, с аргументом имени. Если мы вызовем его с аргументом «Марко», он напечатает Hello, Marco. Мы можем вызвать его, поместив его в список, передав определение функции в качестве первого аргумента и строку в качестве второго аргумента.

((fn [name] (str "Hello, " name)) "Marco")

У этой функции нет имени, поэтому это анонимная функция.

Но давайте дадим этой функции имя, чтобы связать ее с символом и сделать ее вызываемой с помощью функции def:

(def greet [name] (fn (str "Hello, " name))

Для этого есть ярлык, defn форма:

(defn greet [name] (str "Hello, " name))

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

(greet "Marco") ;returns "Hello, Marco"

Также существует ярлык для анонимных функций, использующий синтаксическое указание # перед списком, как в #( ). Аргументы указываются синтаксисом %, и если анонимная функция принимает более одного аргумента, они должны быть пронумерованы, как в #(print %1 %2). Это анонимная функция, которая выводит свои два аргумента.

Наиболее очевидное использование анонимных функций - передача их функциям, которые принимают другие функции в качестве аргументов, например map filter и reduce.

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

(map #(inc %) [1 2 3])

Мы вызвали функцию карты с анонимной функцией в качестве первого аргумента и вектором в качестве второго аргумента. Анонимная функция вызывает функцию inc [rement] для своего уникального аргумента. Функция map выполняет итерацию по коллекции и передает каждый элемент анонимной функции. Следовательно, эта функция возвращает [2 3 4]

Reduce - это еще одна функция, которая принимает функцию в качестве аргумента и коллекцию в качестве второго аргумента. Мы используем эту функцию, чтобы уменьшить коллекцию до одного значения:

(reduce #(+ %1 %2) [1 2 3 4 5])

reduce ожидает, что функция получит аккумулятор в качестве первого аргумента, а текущий элемент коллекции - в качестве второго аргумента. Он применяет результат функции к аккумулятору, и они передают новое накопленное значение функции и следующему элементу сбора. В этом примере мы применяем плюсовую функцию + к накопленному значению, первый аргумент как в %1 и второй аргумент как %2, текущий элемент коллекции:

Первая итерация: применить + к 1 и 2. Накоплено: 3.

Вторая итерация: применить + к накопленным (3) и 3. Накопленные: 6.

Третья итерация: применить + к накопленным (6) и 4. Накопленным: 10;

Четвертая итерация: применить + к накопленным (10) и 5. Конечный результат: 15.

Теперь, когда мы знаем некоторые основные структуры данных, функции и синтаксис clojure, первый код, который вы видели в начале статьи, может быть более знакомым:

В строке for мы объединяем два вектора, в результате получаем [1 2 3 4 5 6]. В строке 3 мы сопоставляем его элементы с помощью анонимных функций, которые умножают каждый элемент на 3, в результате чего получается [3 6 9 12 15 18]. В строке 2 мы фильтруем этот вектор с помощью анонимной функции, предиката, которая возвращает только четные элементы, используя функции = и mod, в результате получаем [6 12 18]. Затем мы сокращаем эту коллекцию до одного значения, содержащего сумму всех его значений, в результате получаем 36.

Видите ли, синтаксиса почти не нужно учить. В этой статье мы: добавляли, умножали, сравнивали равенство, определяли функции, составляли условные операторы с точно таким же синтаксисом: списки. Разве это не потрясающе? Как будто вам почти не нужно изучать Clojure: вы это уже знаете!

Ресурсы для более глубокого погружения

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

Есть отличные бесплатные ресурсы, которые помогут вам глубже начать работу с более точной и полной информацией. Clojure Distilled - один из лучших, чтобы познакомиться с некоторыми концепциями. Официальные страницы website и doc тоже хороши. Если вы такой же видео-парень, как я, то на Youtube-канале ClojureTV много интересных разговоров. Clojurians - отличный канал на Slack, на котором много людей, готовых помочь. Awesome Clojure - это репозиторий Github с некоторыми полезными библиотеками Clojure. В Руководстве для новичков по Clojure есть отличные ресурсы для начала.

Беспроблемный старт с Docker и REPL

Поскольку Clojure работает на JVM, вам потребуется установить Java, JDK и т. Д. Вам также потребуется установить Leiningen, менеджер пакетов clojure. И, возможно, IDE или текстовый редактор. Я обнаружил, что расширение IntelliJ Community + Cursive - отличная среда разработки Clojure.

Но есть способ начать играть с Clojure, не устанавливая ничего из этого. Вы можете использовать Clojure REPL (Read-Eval-Print-Loop) с простой командой docker. REPL оценивает код на ходу, поэтому вам даже не нужно создавать файл. Если у вас установлен докер, просто введите docker run -it clojure lein repl. Он загрузит образ clojure и запустит контейнер с помощью команды repl. Попробуйте набрать (+ 1 2) и посмотрите, что получится!

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