Введение в Clojure

Цель этой статьи — краткое введение в основы LISP с особым вниманием к Clojure. Мы поговорим об основном синтаксисе, основных структурах данных и в целом познакомимся с написанием функций на LISP.

Предпосылки

Для начала вам понадобится Clojure REPL. Вы можете зайти на сайт Clojure и начать там, или вы можете зайти на tryclj.com и использовать онлайн REPL (который может работать или не работать — у меня были некоторые трудности).

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

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

(defn sayhello [name] (println (str "Hello, " name)))

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

Вызов функции

В самой простой форме все, что заключено в круглые скобки, является вызовом функции. Попробуйте это в REPL:

> (println "Hello World!")
"Hello World!"
nil
> 

Примечание. Всякий раз, когда строка начинается с>, это означает, что вы просматриваете ввод REPL.

Он напечатает «Hello World!» затем nil. Первый текст («Hello World!») — это то, что было выведено на консоль в результате вызова println. nil — это возвращаемое значение функции println. Если это сбивает с толку, давайте попробуем что-нибудь еще:

> (str "Hello" "World")
"HelloWorld"
>

Итак, здесь он выдает "HelloWorld", который является результатом объединения Hello и World. Это был вызов функции str, которая объединяет строки. Выведенное "HelloWorld" является результатом (возвращаемым значением) функции.

Итак, это базовый вызов функции. Прежде чем мы перейдем к более сложным вещам, давайте рассмотрим основные примитивы, которые поддерживает Clojure.

Примитивные выражения

Clojure поддерживает то, что вы ожидаете:

Целые числа:

> 54
54

И с плавающей запятой:

> 33.3
33.3

Четные дроби:

> 4/12
1/3

Строки:

> "Hello"
"Hello"

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

> \a
\a

Так, например:

> (first "Hello")
\H

Здесь он вызывает функцию first для строки «Hello», которая возвращает символ «H», который печатается как символьный литерал \H.

Все комментарии начинаются с ; и идут до конца строки:

; this is my function. Call it like this: (my-function)
(defn my-function (println "meee"))

У нас также есть ключевые слова:

> :iamkeyword
:iamkeyword

И символы:

> println
#<core$println clojure.core$println@31024864>

Ключевые слова — это идентификаторы, которые оцениваются сами по себе (обычно используются для ключей в картах), а символы — это идентификаторы, которые разрешаются во что-то еще (например, в функцию типа println). В приведенных выше примерах мы видим, что :iamkeyword вычисляется сама по себе, а println оценивается настолько бессвязно, что это означает, что это функция println. Я не буду вдаваться в подробности, кроме как показать, как определять символы:

> (def zero 0)
#'user/zero

Здесь мы создали Var (не важно, какой он сейчас) и привязываем его к числу 0. На него можно сослаться, используя символ zero.

> zero
0
> (* 5 zero)
0

Здесь * идентифицирует функцию умножения (да, функции могут иметь такие имена!), а ноль идентифицирует 0. Ключевые слова и символы — это еще не все, но пока все, что вам нужно знать, это то, что символы работают более или менее так же, как идентификаторы в любом другом языке.

Списки

Списки очень важны в любом LISP. Это базовая структура, и она уже будет вам знакома. Список объявляется следующим образом:

(1 2 3)

Это представляет собой список, содержащий 1, 2 и 3. «Но подождите!» Вы скажете: «Разве это не вызовет функцию?» И ответ - да! И тут вы натыкаетесь на секрет ЛИСПа — вы пишете программы в виде списков! Итак, когда вы вызываете функцию println:

> (println "Hello World")
"Hello World"

Вы создаете список с элементами println и "Hello World", а затем «запускаете» программу, указанную в этом списке. Это то, что имеется в виду, когда мы говорим, что код LISP являетсяданными. Принципиальной разницы между кодом и данными в LISP нет.

Следующий вопрос должен звучать так: «Но как составить список, не «прогоняя» его?». Один из способов — вызвать функцию list:

> (list 1 2 3)
(1 2 3)

Функция списка оценивает каждый из своих параметров и создает список из значений:

> (list 1 (+ 1 1) 3)
(1 2 3)

Другой способ создать список — вызвать функцию цитаты:

> (quote (1 2 3))
(1 2 3)

Цитирование создает список, но не «оценивает» его. Так, например, если мы используем функцию first из предыдущего:

> (first (quote (1 2 3)))
1

Он ведет себя так, как ожидалось.

Поскольку это очень распространено в LISP, для цитирования есть сокращение:

> '(1 2 3)
(1 2 3)

Здесь '(1 2 3) это то же самое, что и (quote (1 2 3)).

Обратите внимание, что ничего внутри списка в кавычках не будет оцениваться:

> (quote (1 (+ 1 1) 3))
(1 (+ 1 1) 3)

Здесь мы создали список, содержащий первый элемент 1, второй элемент — список, содержащий символ + и число 1 дважды, а третий элемент — 3. Есть способы обойти это поведение, но я не буду вдаваться в подробности здесь.

Работа со списками

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

> (rest '(1 2 3))
(2 3)

Сначала это может показаться немного непонятным, но помните, как работают связанные списки. Связанный список — это, по сути, структура, состоящая из двух частей данных: значения (называемого головой) и указателя на остальную часть списка (называемого концом). . Если хвост ноль, то элементов не осталось.

Еще одна вещь, которую следует отметить, это то, что когда у нас есть список, все, что у нас есть, это первый маленький блок (на диаграмме блок, содержащий 1 и стрелку к следующему). В LISP это называется cons-пара. Их можно создать непосредственно в LISP, вызвав функцию cons:

> (cons 99 nil)
(99)

Здесь мы видим, что если мы создадим пару с первым элементом, являющимся числом 99, и вторым элементом, равным nil, результатом будет список с одним элементом. Мы можем создавать большие списки:

> (cons 10 (cons 20 nil))
(10 20)

Как и ожидалось, если мы создадим пару с первым элементом, равным 10, и вторым элементом, представляющим собой список с первым элементом 20 и вторым элементом nil, результатом будет список с двумя элементами!

Также помните, что списки (по крайней мере, в Clojure) неизменяемы. Они не могут измениться. После создания пары минусов вы не можете изменить голову или хвост. Что вы можете сделать, так это создать новую пару минусов, хвост которой указывает на уже существующий список. Таким образом, вы можете вставлять элементы в начало списка:

Give me some Clojure:
> (def mylist '(1 2 3))
#'user/mylist
> mylist
(1 2 3)
> (def zerofirst (cons 0 mylist))
#'user/zerofirst
> zerofirst
(0 1 2 3)
> mylist
(1 2 3)
>

Вот, собственно, что здесь произошло:

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

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

> (nth '(1 2 3 4) 2)
3

Это получает элемент с индексом 2 (начиная с нуля) из списка (1 2 3 4). Итак, результат 3. Помните, что для этого ему нужно пройти по списку, поэтому, хотя он работает быстро для маленьких списков, он становится медленнее для больших списков.

У нас также есть функция take:

> (take 3 '(1 2 3 4 5 6 7 8))
(1 2 3)

Это возвращает ряд элементов из начала списка.

count возвращает количество элементов:

> (count '(1 2 3))
3

Векторы и карты

Списки реализованы как односвязные списки. чуть позже мы рассмотрим значение этого. Односвязные списки не поддерживают произвольный доступ (во всяком случае, эффективно), поэтому у нас есть векторы:

> [1 2 3]
[1 2 3]

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

И затем есть карты:

> {:one "one" :two "two"}
{:one "one", :two "two"}

Карты — это списки пар ключ-значение, в которых ключи сопоставляются со значениями. И ключи, и значения могут быть выражениями любого типа (которые оцениваются при создании карты). Однако ключи обычно являются ключевыми словами.

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

> (get {:a 1 :b 2} :b)
2

Также есть ярлык:

> (:b {:a 1 :b 2})
2

Вызов ключевого слова как функции на карте вернет значение этого ключевого слова на карте. Довольно прикольно!

Если ключевое слово не найдено, оно вернет nil:

> (:c {:a 1 :b 2})
nil

Функции

Ранее мы рассмотрели, как определить материал:

> (def hi "Hello there")
#'user/hi
> hi
"Hello there"
>

Существует еще одна встроенная функция для определения функций:

> (defn sayhi [] (println "Hello there"))
#'user/sayhi
> (sayhi)
Hello there
nil

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

> (defn sayhi [name] (println (str "Hi, " name)))
#'user/sayhi
> (sayhi "Charles")
Hi, Charles
nil

И функции могут иметь документацию:

> (defn sayhi
      "says hello"
      [name]
      (println (str "Hi, " name)))
> (sayhi "Charles")
Hi, Charles
nil

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

Здесь мы определяем функции, используя defn, но есть и другой способ, используя только def:

> (def sayhi (fn [name] (println (str "Hi, " name))))
#'user/sayhi
> (sayhi "John")
Hi, John
nil

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

Помните, как первым элементом в списке всегда было имя функции? Это также может быть сама функция:

> ((fn [name] (println (str "Hi, " name))) "Charles")
Hi, Charles
nil

Это указывает на тот факт, что Clojure (и все LISP) являются функциональными языками, в которых функции рассматриваются как примитивы, такие же, как числа и строки. Они могут быть привязаны к символам (на другом языке мы бы сказали «назначены переменным») и передаваться в качестве аргументов, а анонимное функциональное выражение может быть вызвано непосредственно как функция.

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

> #(println (str "Hi, " %))

Это эквивалентно:

(fn [arg] (println (str "Hi, " arg)))

% рассматривается как аргумент. Итак, если мы используем наш предыдущий пример, мы должны сделать это:

> (#(println (str "Hi, " %)) "Charles")
Hi, Charles
nil

Вы также можете иметь несколько аргументов в этой сокращенной форме:

#(println (str "Hi, " %1 " and " %2))

Эквивалентно:

(fn [a1 a2] (println (str "Hi, " a1 " and " a2)))

Контроль

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

Основное отличие в этом случае состоит в том, что они возвращают значения. Например, давайте посмотрим на выражение if:

> (if (= 1 2) "first" "second")
"second"

Здесь мы сказали: «Если 1 равно 2, вернуть "first", иначе "seconds"). Если бы мы изменили его:

> (if (= 5 5) "first" "second")
"first"

Он возвращает "first". Обратите внимание, что функция = возвращает логическое значение, поэтому мы могли бы сказать:

> (if false "first" "second")
"second"
> (if true "first" "second")
"first"

Также есть выражение cond, допускающее несколько условий:

(defn pos-neg-or-zero
  [n]
  (cond
    (< n 0) "negative"
    (> n 0) "positive"
    :else "zero"))

Здесь мы определяем функцию, которая будет возвращать “positive” “negative” или “zero” в зависимости от значения аргумента n.

> (pos-neg-or-zero 5)
"positive"
> (pos-neg-or-zero -5)
"negative"
> (pos-neg-or-zero 0)
"zero

У вас может быть любое количество условий:

(cond
   condition1 result1
   condition2 result2
   ...
   conditionn resultn
   :else resultotherwise)

А если cond для вас слишком многословно, есть case:

> (case (read-line)
    "a" "you type a"
    "b" "you type b"
    "c" "you type c"
    "you typed neither a, b, or c")

Как вы можете догадаться, он принимает вид:

(case expression
   value1 result1
   value2 result2
   ...
   valuen resultn
   defaultresult)

Логика

У нас есть логические выражения, которые мы ожидаем. И:

> (and true true false)
false
> (and true true)
true

Or:

> (or true true false)
true
> (or false false)
false

Примечание. В Clojure nil считается ложным, а не-nil — истинным, а функции and и или возвращают первый аргумент, когда известен результат:

> (and true "yes")
"yes"
> (and true nil)
nil
> (or 1 2 3)
1
> (if nil "a" "b")
"b"

Они также не будут оценивать выражения, в которых нет необходимости:

> (and
    (do (println "a") true)
    (do (println "b") true)
    (do (println "c") false)
    (do (println "d") false))
a
b
c
false

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

Пусть выражения

Одного все еще не хватает. Как вы объявляете переменные?

Во-первых, у вас нет «переменных», так как вы не можете изменить значения. Но вы можете объявить «привязки», которые можно использовать в ограниченном контексте. Введите выражение let.

> (let [a 5
        b 3]
    (+ a b))
8

Здесь мы привязали значение 5 к a, а значение 3 к b, а затем вычислили выражение a + b, которое, как и ожидалось, вернуло 8. Таким образом, выражение let принимает следующую форму:

(let [name1 value1
      name2 value2
      ...
      namen valuen]
   expression)

Теперь мы можем написать кое-что интересное:

> (do
     (println "What is your name?")
     (let [name (read-line)
           hi-statement (str "Hi, " name)]
       (println hi-statement)))
What is your name?
Charles
Hi, Charles
nil
>

Рекурсия

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

Clojure выполняет хвостовые вызовы, которые не используют пространство стека, используя выражение recur:

> (defn sum-list [list carry]
    (if (empty? list)
      carry
      (recur
        (rest list)
        (+ carry (first list)))))
> (sum-list '(1 2 3 4 5) 0)
15

Давайте пройдемся по этой функции. Он имеет два аргумента: list и carry. carry присваивается начальное значение 0, а list — это list, содержимое которого мы хотим суммировать. Сначала он проверяет, пусто ли list. Если это так, он возвращает carry без каких-либо действий. Если list не пусто, вызывается recur с некоторыми параметрами. Здесь вызов recur был бы аналогичен вызову sum-list, но с хвостовой рекурсией. Другими словами, он не займет лишнего места в стеке. Ограничение состоит в том, что он должен быть последним в функции (помните, что его аргументы вычисляются до, поэтому в данном случае он последний). Двигаясь дальше, он рекурсивно вызывает сам себя, передавая list, кроме первого элемента, в качестве первого аргумента и сумму carry и первого элемента list в качестве второго элемента. Таким образом, функция будет вызываться, суммируя каждый элемент в list, пока не будет достигнут конец.

Это все на данный момент. Я надеюсь, что это было полезное знакомство с LISP, Clojure и некоторыми его базовыми управляющими структурами. Есть некоторые важные вещи, которые мы не рассмотрели (например, макросы). А пока, если вам нужна дополнительная информация, посмотрите Документацию Clojure или просто поищите на YouTube.

Отзывы будут оценены!