Программирование на Clojure
Это перевод книги «Programming Clojure Second Edition». The Pragmatic Programmers, Программирование на Clojure Вторая Редакция. Авторы: Стюарт Хэлловэй (Stuart Halloway), Аарон Бедра (Aaron Bedra), Предисловие Рича Хики (Rich Hickey) — создателя Clojure. Редакция Макла Свэйна (Michael Swaine).
Что читатели говорят о
Программирование на Clojure, Вторая Редакция
Clojure — это, на данный момент, один из самых интересных языков программирования и лучший путь изучения Clojure стал лучше. Вторая редакция Программирования на Clojure включила в себя обновлённую информацию, стало больше практических примеров и тонны полезной информации для обучения, работы и углублённого изучения Clojure.
- Ола Бини (Ola Bini)
- Создатель языка программирования Ioke, разработчик, ThoughtWorks
Пугаетесь Clojure? После прочтения этой книги Вы не будете его бояться. Книга написана в ясном и приятном стиле, обучение языку преподаётся в очень простом виде и занимает немного времени.
- Тим Берглюнд (Tim Berglund)
- Основатель и директор, August Technology Group
Авторы создали лёгкий путь изучения Clojure с помощью этой хорошо организованной и лёгкой для чтения книгой. У них есть умение создания простых и эффективных примеров наглядно демонстрирующих сочетания уникальных особенностей этого языка.
- Крис Хаусер (Chris Houser)
- Главный разработчик Clojure и автор библиотеки
Clojure — это красивый, элегантный и очень мощный язык на JVM. Он подобен собору: вы можете бродить по нему в одиночку, или можете ходить в компании знающего экскурсовода, который объяснит вам всё со своей точки зрения и поможет вам понять и оценить красоту архитектуры. В этой книге вы можете насладиться компанией и получить помощь не одного, а двух опытных разработчиков, действительно глубоко владеющих этим языком.
- Доктор Венкат Субраманиам (Dr. Venkat Subramaniam)
- Автор, имеющий награды и основатель Agile Developer, Inc.
Pragmatic Bookshelf
Многие обозначения используемые производителями и продавцами для однозначного обозначения своих продуктов являются торговыми марками. Когда эти обозначения появляются в этой книге, то они печатаются с первыми заглавными буквами или всеми буквами. The Pragmatic Starter Kit, The Pragmatic Programmer, Pragmatic Programming, Pragmatic Bookshelf, PragProg и связанное с ними g устройство являются торговой маркой The Pragmatic Programmers, LLC.
При подготовке этой книги были предприняты все меры предосторожности. Тем не менее, издатель не несёт ответственности за ошибки, упущения или причинения вреда в результате использования информации (включая тексты программ) содержащейся здесь.
Pragmatic проводит курсы, семинары и другие продукты могущие помочь вам и вашей команде создавать более лучшее программное обеспечение и получать от этого удовольствие. За более подробной информацией, пожалуйста посетите http://pragprog.com.
Команда, которая подготовила эту книгу:
- Майкл Свэйн (Michael Swaine) - редактор
- Potomac Indexing, LLC — индексатор
- Ким Вимпсетт (Kim Wimpsett) — редактор копии
- Дэвид J Келли (David J Kelly) — наборщик
- Джанет Фюрлоу (Janet Furlow) — продюсер
- Джулиет Бенда (Juliet Benda) — права
- Элли Кэллэхен (Ellie Callahan) — поддержка
Все права защищены. Copyright © 2012 The Pragmatic Programmers, LLC.
Памяти моего отца и наставника,
Крейга Бедра (Craig Bedra), который научил меня ценить
обучение исследованием и показал что нет
такой вещи как магия. — Аарон (Aaron)
Содержание
- 1 Предисловие к Второму Изданию
- 2 Предисловие к Первому Изданию
- 3 Благодарности
- 4 Введение
- 5 Глава 1. Приступая к Работе
- 6 Глава 2. Изучение Clojure
- 7 Глава 3. Объединение Данных с Последовательностями
- 8 Глава 4. Функциональное Программирование
Предисловие к Второму Изданию
Многое изменилось после выхода первой редакции книги. Да, произошли некоторые улучшения в языке, например протоколы (protocols) и записи (records). Однако важнее всего, это то, что Clojure получил применение в большом количестве областей. Люди создают стартапы, коммуникации, анализируют большие наборы данных, работают с финансами, интернетом и базами данных с помощью Clojure. Вокруг Clojure выросло большое сообщество, а вместе с ним и тонны библиотек. Библиотеки являются интересными сами по себе, вне зависимости от предоставляемых ими возможностей. Лучшие из них вобрали в себя механизмы и подход Clojure и в результате достигли нового уровня простоты и совместимости.
В этом втором издании Стьюарт (Stuart) и Аарон (Aaron) охватили усовершенствования языка, кроме этого разобрали внутреннее устройство некоторых библиотек сообщества, заботясь об освещении подхода их работы. Книга является интересным введением в Clojure и я надеюсь, она вдохновит вас к присоединению к сообществу что в конечном итоге поможет развитию экосистемы библиотек.
—Рич Хики (Rich Hickey) Создатель Clojure
Предисловие к Первому Изданию
Мы тонем в сложности. Большая часть усложнений — случайны и возникли они от способа решения проблем, а не от самих проблем. Объектно-ориентированное программирование кажется лёгким, но программы часто порождают сложную паутину из связанных между собой изменяемых объектов. Единственный вызов метода на одном объекте может быть причиной каскада изменений проходящих через весь граф объекта. В результате становится очень сложным понять что и когда случилось, как эти объекты получили своё состояние и как вернуть их в предыдущее состояние для исправления ошибки. Добавьте сюда ещё и параллельные вычисления и вся эта смесь быстро станет не управляемой. Начинается использование фиктивных объектов и наборов тестов, но слишком часто мы не в состоянии подвергнуть сомнению наши инструменты и нашу модель программирования.
Функциональное программирование предлагает альтернативу. Делая упор на чистые функции, использующие и возвращающие неизменяемые значения, мы можем получить побочные эффекты, но это скорее исключение чем правило. Это становится важным тогда, когда мы сталкиваемся с увеличением параллелизма в многоядерных архитектурах. Clojure спроектирован так, чтобы функциональное программирование было доступным и практичным для разработчиков коммерческого программного обеспечения. Он признаёт необходимость и огромную практичность работы в доверенных инфраструктурах, например JVM и поддерживает существующие наработки в Java фреймворке и библиотеках.
Примечательность этой книги в том, что Стьюарт «использует» Clojure с точки зрения профессионального разработчика. Очевидно что он (Стьюарт) достаточно опытен и знает, кроме плюсов, минусы Clojure. Кроме того, он излагает прагматичный подход к Clojure. Эта книга — это тур от энтузиаста по ключевым особенностям Clojure, с обоснованием в виде программ, кроме того, содержит введение в те вещи, которые могут стать новой концепцией. Я надеюсь она вдохновит вас к написанию программ на Clojure, а когда вы бросите взгляд на проделанный путь, вы скажете «Я не просто сделал работу, а сделал её лёгким и простым путём а процесс программирование был очень приятным!»
—Рич Хики (Rich Hickey)
Создатель Clojure
Благодарности
Множество людей внесли свой вклад в создание этой книги. Они помогли решить проблемы и ошибки с которыми в одиночку справиться было бы трудно.
Спасибо замечательной команде Relevance and Clojure/core за создание атмосферы в которой выросли и расцвели хорошие идеи.
Спасибо людям принявшим участие в списке рассылки Clojure за всю их помощь и поддержку.
Спасибо всем в Pragmatic Bookshelf. Выражаю особую благодарность нашему редактору Майклу Свэйну, который несмотря на очень жёсткий график помогал своими хорошими советами. Спасибо Дейву Томасу и Энди Ханту за создание удобной платформы написания технических книг и поддержку авторов.
Спасибо всем людям, которые присылали предложения по улучшению книги.
Спасибо нашим рецензентам за все их комментарии и полезные предложения, одни из рецензентов: Кевин Бим, Шон Кофилд, Фред Даод, Стивен Хьювиг, Тибор Симич, Дэвид Слеттен, Венкат Субраманиам и Стефан Туралски.
Выражаю особую благодарность Дэвиду Либке, за его труд в создании уникальной Главы 6, Протоколы и Типы данных, страница 143. Благодаря его усилиям появилось чудесное руководство о новых идеях и эта книга не была бы полной без его вклада.
Спасибо Ричу Хики за замечательный язык Clojure и укрепление сообщества вокруг него.
Спасибо моей жене, Джои и моим дочерям Хэтти, Харпер и Мейбл Фэйри. Вы все подобны лучику солнца. — Стьюарт
Спасибо моей жене, Эрин, за её бесконечную любовь и поддержку. — Аарон
Введение
Clojure — это динамический язык программирования для Виртуальной Машины Java (Java Virtual Machine — JVM), с привлекательным сочетанием функций:
- Clojure элегантен. Дизайн языка Clojure является аккуратным и чистым, что в свою очередь позволяет вам писать программы предназначенные для решаемой вами задачи без лишнего беспорядка и не отвлекаясь на другие детали.
- Clojure — это диалект Lisp’а. Clojure обладает мощью присущей Lisp’у, но не ограничивается его особенностями.
- Clojure — это функциональный язык. Структуры данных не изменяемы и большинство функций свободны от побочных эффектов. Эта особенность позволяет легко писать правильные программы и конструировать из маленьких программ большие.
- Clojure упрощает параллельное программирование. Многие языки строят параллельную модель вокруг блокировки, что усложняет их корректное использование. Clojure предоставляет несколько альтернатив блокировке: программную транзакционную память, агенты, атомы и динамические переменные.
- Clojure включает в себя Java. Вызовы из Clojure к Java происходят непосредственно и быстро, без промежуточного слоя.
- В отличие от многих популярных динамических языков, Clojure быстрый. Clojure написан с расчётом использования оптимизации современных JVM.
Существует много других языков которые обладают некоторыми перечисленными свойствами. Но Clojure стоит особняком. Мы расскажем об этих свойствах и даже больше в Главе 1, Приступая к Работе, на странице 1.
Для кого предназначена эта книга
Clojure — это мощный язык программирования универсального назначения. Как следствие, эта книга предназначена для опытных программистов, которым нужен мощный и элегантный язык программирования. Эта книга будет полезна людям знакомым с такими современными языками программирования как C#, Java, Python или Ruby.
Clojure построен на базе Java Virtual Machine и поэтому он быстр. Эта книга будет особенно интересна Java программистам, которым нужна выразительность динамического языка без потери производительности.
Clojure позволяет легко создавать новые функции нужные вам. Если вы программируете на Lisp, используете функциональные языки как Haskell или пишете программы параллельных вычислений, то вы будете получать удовольствие от Clojure. Clojure сочетает в себе идеи Lisp, функционального программирования и параллельного программирования, и кроме того, позволяет программистам легко применять их на практике.
Clojure является частью более крупного явления. Такие языки как Erlang, F#, Haskell и Scala в недавнее время получили внимание за счёт поддержки функционального программировани или модели параллелизма. Энтузиасты этих языков найдут много общего с Clojure.
Что содержится в этой книге
Глава 1, Приступая к Работе, на 1-й странице демонстрирует элегантность и уникальность Clojure как языка общего назначения с функциональным стилем и моделью поддержки параллельного программирования. Также будет рассказано об установке Clojure и интерактивной разработке программ с помощью REPL.
Глава 2, Изучение Clojure, на странице 21 дан общий обзор всех конструкций ядра Clojure. После изучения этой главы вы сможете читать большинство кода Clojure.
Следующие две главы посвящены функциональному программированию. Глава 3, Объединение Данных с Последовательностями, на странице 55 показывает как все данные могут объединяться в мощную метафору последовательности.
Глава 4, Функциональное Программирование, на странице 85 показывает как писать функциональный код и как в том же стиле используется библиотека последовательности.
Глава 5, Состояние, на странице 113 производится углубление в параллельную модель Clojure. Clojure предоставляет четыре мощные модели для работы с параллельными вычислениями, плюс всё хорошее что предоставляют библиотеки параллельных вычислений Java.
Глава 6, Протоколы и Типы Данных, на странице 143 происходит знакомство с записями, типами и протоколами в Clojure. Эта концепция появилась в Clojure 1.2.0 и расширена в 1.3.0.
Глава 7, Макросы, на странице 165 изучается эта особенность присущая Lisp. Макросы возникают как следствие того, что код написанный на Clojure одновременно является данными, в результате обеспечивается способность метапрограммирования, присущая языку Lisp, но невозможная или трудная в реализации в других языках.
Глава 8, Мультиметоды, на странице 187 даны ответы Clojure полиморфизму. Полиморфизм обычно означает следующее «взять класс первого аргумента и управлять методом основанным на нём». Мультиметоды Clojure позволяют вам выбрать функцию для всех аргументов и управлять методом основанным на ней.
Глава 9, Спуск к Java и грязи, на странице 203 показывает как вызывать Java из Clojure и вызывать Clojure из Java. Вы увидите как Clojure получает прямой доступ к железу и производительности Java уровня.
И наконец, Глава 10, Сборка Приложений, на странице 227 дан обзор полного рабочего процесса Clojure. Вы будете собирать приложение с нуля, решая различные проблемы и думая над простотой и качеством. Вы научитесь использовать полезные библиотеки Clojure для создания и развёртывания web приложений.
Приложение 1, Поддержка Редактора, на странице 253 дан список редакторов, поддерживающих Clojure, со ссылками на установку и настройку каждого из них.
Как читать эту книгу
Всем читателям настоятельно рекомендуется прочитать первые две главы в том порядке, в каком они идут. Обратите особое внимание на Раздел 1.1, Почему Именно Clojure?, на странице 2, где содержится обзор преимуществ Clojure.
Постоянно экспериментируйте. Clojure предоставляет интерактивную среду, в которой вы можете сразу получить результат ваших действий; прочитайте Использование REPL на странице 12 за более детальной информацией.
После прочтения первых двух глав, можете читать книгу как вам заблагорассудится. Но прочтите Главу 3, Объединение Данных с Последовательностями, на странице 55 до Главы 5, Состояние, на странице 113. Эти главы проведут вас через неизменные структуры данных к мощной модели, используемой в написании программ параллельных вычислений.
Прежде чем приступать к углублению в примеры кода в следующих главах, убедитесь, в том, что вы используете редактор поддерживающий работу с Clojure. Приложение 1, Поддержка Редактора, на странице 253 разъяснит вам общие сведения о редакторе. Если возможно, то попробуйте использовать редактор, поддерживающий балансировку скобок, например Emacs’овский paredit режим или плагин CounterClockWise для eclipse. Использование этих редакторов будет огромным подспорьем в изучении программирования на Clojure.
Для Программистов на Функциональных Языках
- Clojure соблюдает баланс между академической чистотой и работой современных поколений JVM. Внимательно прочитайте Главу 4, Функциональное Программирование на странице 85 для того, чтобы понять разницу между Clojure и такими языками как Haskell.
- Модель параллельного программирования Clojure (Глава 5, Состояние, на странице 113) предоставляет несколько явных путей для решения проблемы побочных эффектов и состояния, что делает функциональное программирование более привлекательным широкой публике.
Для Программистов Java/C#
- Внимательно прочитайте Главу 2, Изучение Clojure, страницу 21. Clojure имеет очень несложный синтаксис (по сравнению с Java или C#) и мы довольно быстро рассмотрим основные правила.
- Обратите внимание на макросы, рассматриваемые в Главе 7, Макросы, страница 165. Эта черта Clojure очень сильно отличается от Java или C#.
Для Lisp программистов
- Некоторое содержимое Главы 2, Изучение Clojure, страница 21 будет для вас просто повторением пройденного, но всё же прочтите эту главу. Clojure сохраняет ключевые особенности Lisp, но в то же время отличается от Lisp в некоторых местах, всё это содержится в Главе 2.
- Уделите внимание ленивым последовательностям в Главе 4, Функциональное Программирование, страница 85.
- Установите и настройте режим Emacs для Clojure, это позволит вам получать удовольствие при изучении примеров кода в следующих главах.
Для Perl/Python/Ruby программистов
- Внимательно прочитайте Главу 5, Состояние, страница 113. В Clojure очень важна параллельность между процессами.
- Поймите макросы (Глава 7, Макросы, страница 165). Но не думайте что вы сможете легко перенести идиомы метапрограммирования с вашего языка на макросы. Всегда помните, что макросы исполняются во время чтения, а не во время исполнения.
Условные обозначения
Ниже описаны условные обозначения применяемые в этой книге. Пример текста программы обозначается следующим шрифтом:
(+ 2 2)
Результат выполненного кода обозначается стрелкой ->.
(+ 2 2) -> 4
Перед сообщением консоли, в тех случаях, когда сообщения консоли трудно отличить от кода и результатов, предшествует символ трубы (|).
(println "hello") | hello -> nil
При первом знакомстве с Clojure мы будем показывать грамматику форм в следующем виде:
(example-fn required-arg) (example-fn optional-arg?) (example-fn zero-or-more-arg*) (example-fn one-or-more-arg+) (example-fn & collection-of-variable-args)
Грамматика является не формальной и использует символы ?, *, + и & для документирования различных стилей использования аргументов так, как показано выше.
Код Clojure организован в виде libs (libraries — библиотеки). В тех случаях, когда примеры приведённые в книге зависят от библиотек не являющихся частью ядра Clojure, будет использована форма use или require:
(use '[lib-name :only (var-names+)]) (require '[lib-name :as alias])
Эта форма use выполняет только ввод имени (name) в var-names, а require создаёт псевдоним (alias) для ясного представления происхождения каждой функции. Например, наиболее часто используемая функция — это file из библиотеки clojure.java.io:
(use '[clojure.java.io :only (file)]) (file "hello.txt") -> #<File hello.txt>
или аналог на основе require:
(require '[clojure.java.io :as io]) (io/file "hello.txt") -> #<File hello.txt>
Clojure возвращает nil как результат правильно выполненного use. Для краткости он не будет указываться в кодах примеров. При чтении книги, вы будете вводить код в интерактивную оболочку называемую REPL. Приглашение REPL выглядит так:
user=>
Обозначение user в данном примере является обозначением активного пространства имён. Для большинства примеров в этой книге пространство имён не играет никакой роли. В тех примерах, где не важно пространство имён мы будем использовать следующий синтаксис интерактивного REPL:
(+ 2 2) ; ввод строки без приглашения с пространством имён -> 4 ; возвращаемое значение
В тех немногих случаях, когда будет важно текущее пространство имён, мы будем использовать следующее:
user=> (+ 2 2) ; ввод строки с приглашением содержащим пространство имён -> 4 ; возвращаемое значение
Web-ресурсы и обратная связь
Официальная домашняя страница «Программирования на Clojure» расположена на web-сайте Pragmatic Bookshelf. Здесь вы можете заказать бумажные или электронные копии книги или скачать примеры демонстрационного кода. Также вы можете связаться через форму обратной связи, по исправлению и улучшению книги или воспользоваться форумом книги.
Скачивание Примеров Кода
Примеры кода из книги доступны из двух источников:
- Ссылка на домашнюю страницу Программирования на Clojure содержит официальную копию примеров кода, которые обновляются с каждым выпуском книги.
- Git хранилище Программирования на Clojure обновляется в режиме реального времени. Эти примеры являются наиболее новыми, лучшими и временами могут опережать примеры кода описанного в книге.
Примеры находятся в директории examples, в случае если не написано что-то другое. Во всей книге текст примеров кода приводится с путями, пути отделяются от кода другим цветом фона. На пример, следующий код находится по пути src/examples/preface.clj: src/examples/preface.clj
(println "hello")
Если вы читаете книгу в PDF формате, вы можете кликнуть по маленькому прямоугольнику с названием файла, который находится до текста кода и непосредственно скачать код.
С кодом примера в руках вы можете начать работу. Мы начнём знакомство с Clojure через комбинацию тех особенностей, которыми Clojure отличается от остальных языков.
Глава 1. Приступая к Работе
Много факторов поспособствовало быстрому развитию Clojure. Поверхностный поиск в интернете о Clojure даст нам следующее:
- это функциональный язык,
- это реализация Lisp для JVM и
- имеет специальные механизмы для работы с параллельными вычислениями.
Все эти вещи очень важны, но ни одна из них не является ключевым моментом в Clojure. По нашему мнению есть два ключевых момента управляющих всем, что есть в Clojure: простота и мощь.
Простота имеет несколько значений, которые являются актуальными в программном обеспечении, но наше определение простоты следующее: вещь является простой в том случае, когда оне не сложная. Простые компоненты дают системе возможность выполнять те вещи, которые заложены разработчиком, и не дают выполнять задачи не предусмотренные для этих компонентов. Судя по нашему опыту можно сказать следующее: неоправданная сложность быстро перерастает в опасную сложность.
Мощь также имеет много значений. Одно из значений мощи употребляемое здесь — это возможность решения наших задач. Мощь в отношении к программисту — это постройка программ на такой базе, которая способна к решению поставленной задачи и широко используется, например, JVM. Тогда ваши инструменты дадут вам полный, ничем не ограниченный доступ к мощи. Мощь — часто является неотъемлемой частью получения полной отдачи от используемой платформы.
Как программисты, мы долгие годы терпели причудливо-сложные инструменты, которые были единственным способом получения нужной нам мощи или мирились с ограниченной мощью для программирования с приемлемой простотой. Некоторые компромиссы являются фундаментальными, но противостояние мощи против простоты — не одно из этих компромиссов. Clojure показывает, что мощь и простота могут идти вместе плечом к плечу.
Почему Clojure?
Все отличительные черты Clojure предназначены для обеспечения простоты, мощи или их обоих (простота и мощь). Вот несколько примеров:
- Простота функционального программирования в том, что оно позволяет изолировать вычисления от состояния и идентичности. Плюсы: функциональную программу легче понять, писать, тестировать, оптимизировать и распараллелить.
- Взаимодействие Clojure с формами Java является очень мощным подспорьем, дающим вам прямой доступ к семантике Java платформы. Плюсы: вы получаете производительность и семантику эквивалентную Java. Самое главное: вам никогда не понадобится «спускаться» в низко-уровневый язык для получения большей мощи.
- Простота Lisp’а выражается в двух направлениях: он разделяет чтение от вычисления и синтаксис языка состоит из маленького количества ортогональных частей. Плюсы: синтаксическая абстракция захватывает модели дизайна и S-выражения подобные XML, JSON и SQL получаются такими, какие они есть.
- Также мощь Lisp’а выражается в доступности компилятора и системы макросов во время выполнения. Плюсы: Lisp обладает ленивым принятием решений и возможностью простого создания проблемно-ориентированных языков (DSL).
- Временная модель Clojure проста, разделение значений, идентичности, состояния и времени. Плюсы: программы могут принимать и запоминать информацию, не опасаясь изменения в прошлом.
- Протоколы также просты, отделяют полиморфизм от происхождения. Плюсы: вы получаете безопасное, специальное расширение типа и абстракций, без путаницы шаблонов дизайна или хрупкого monkey исправления (monkey patch или партизанский патч).
Этот список особенностей выступает в роли дорожной карты или указателя, вам не обязательно следовать каждому указателю. Каждая особенность будет освещена ниже в отдельных главах.
Давайте посмотрим некоторые из этих особенностей в деле через постройку маленького приложения. Попутно вы узнаете как загружать и исполнять остальные примеры в книге.
Clojure элегантен
Clojure высокоуровневый язык, с малым количеством шума. В результате, программы написанные на Clojure становятся короткими. Короткие программы менее сложны в построении, менее сложны в внедрении и менее сложны в обслуживании (В хорошей книге Software Estimation: Demystifying the Black Art [McC06] описано почему маленькие программы проще в обслуживании). Частично верно, то, что программы должны быть короткими, но не очень короткими. В качестве примера, рассмотрим следующий Java код, из Apache Commons:
public class StringUtils { public static boolean isBlank(String str) { int strLen; if (str == null || (strLen = str.length()) == 0) { return true; } for (int i = 0; i < strLen; i++) { if ((Character.isWhitespace(str.charAt(i)) == false)) { return false; } } return true; } }
Метод isBlank() проверяет строку на пустоту: является ли строка действительно пустой или состоит из пробелов. Вот подобная реализация в Clojure:
(defn blank? [str] (every? #(Character/isWhitespace %) str))
Clojure версия короче. Но что гораздо важнее, она проще: она не имеет переменных, нет изменяемых состояний и нет ветвлений. Это стало возможным благодаря функциям высшего порядка. Функции высшего порядка — это функции принимающие другие функции в виде аргументов и/или возвращающие функции в качестве результатов. В этом примере применяется функция every? и коллекция в качестве аргумента и возвращается true если функция every? возвращает true для каждого элемента коллекции.
Поскольку Clojure версия не имеет ветвлений, она проще для чтения и тестирования. Эти преимущества особенно хорошо проявляются в более сложных программах. Кроме того, пока код краток, он более читабельный. На самом деле Clojure программа читается как определение пустоты: строка пуста, если каждый символ в нём является пробелом. Этот определение значительно лучше чем метод Commons, который скрывает определение пустоты за деталями реализации из определений loop и if.
В качестве другого примера рассмотрим определение тривиального класса Person в Java:
public class Person { private String firstName; private String lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
В Clojure вы определите Person одной строкой:
(defrecord Person [first-name last-name])
и работа с записями будет осуществляться примерно так:
(def foo (->Person "Aaron" "Bedra")) -> #'user/foo foo -> #:user.Person{:first-name "Aaron", :last-name "Bedra"}
defrecord и остальные связанные с ней функции описаны в Разделе 6.3, Протоколы, на странице 147.
Кроме того, что этот способ на порядок короче, подход языка Clojure отличается тем, что запись Person является не изменяемой. Не изменяемые структуры данных являются потокобезопасными и возможность их обновления может осуществляться через Clojure’овские ссылки, агенты и атомы, описанные в Главе 5, Состояние, на странице 113. Поскольку записи не изменяемы, Clojure автоматически предоставляет корректную реализацию hashCode() и equals().
Clojure основан на Lisp’е.
Clojure — это Lisp. На протяжении десятилетий сторонники Lisp рассказывали о достоинствах Lisp’а. И в то же время, захват мирового господства Lisp’ом продвигается медленно. Также как и Lisp, Clojure сталкивается с двумя проблемами:
- Clojure должен добиться успеха Lisp показав Lisp программистам что он (Clojure) охватывает важные части Lisp.
- В тоже время Clojure должен добиться успеха там, где в прошлом не добились признания различные реализации Lisp через получение поддержки большого сообщества программистов.
Clojure решает эти задачи через предоставление метапрограммирования аналогичного Lisp’у и в то же время синтаксическими улучшениями, делающими Clojure более дружественным к не-Lisp программистам.
Почему Lisp?
Реализации Lisp имеют очень маленькое ядро языка, почти без синтаксиса и мощную особенность: макросы. Благодаря этим особенностям вы можете преобразовать Lisp под ваши нужды, а не подлаживаться под язык программирования. Для сравнения рассмотрим следующий фрагмент Java кода:
public class Person { private String firstName; public String getFirstName() { // и т.д.
В этом коде, getFirstName() является методом. Методы полиморфичны и могут преобразовываться под ваши нужды. Но интерпретация остальных слов в примере ограничена языком. Иногда вам срочно может понадобиться изменить значение этих слов. Так, например, вы можете сделать следующее:
- Переопределить private для того, чтобы оно означало «private для кода, предназначенного для непосредственного использования, но public для сериализации и юнит тестов.»
- Переопределить класс для автоматической генерации методов получения и установки для private полей, за исключением тех случаев, когда указано что-либо другое.
- Создать подкласс, предоставляющий захват обратных вызовов для событий жизненного цикла программы. Например, жизненный цикл для класса — запускать событие при создании экземпляра класса.
Мы увидели пример программы в которой нужны все эти особенности. Без них программисты прибегают к обходным путям, приводящим к появлению повторяющихся ошибок. В результате написаны миллионы строк кода обходных путей для реализации отсутствующих особенностей в языках программирования.
В большинстве языков программирования, для реализации особенностей упомянутых раньше, вам придётся обратиться к реализации языка. В Clojure вы можете добавить свои собственные особенности языка с помощью макросов (Глава 7, Макросы, страница 165). Clojure сам построен из макросов, подобных defrecord:
(defrecord name [arg1 arg2 arg3])
Если вам нужна другая семантика, напишите свой собственный макрос. Если вы хотите другой вариант записей со строгой типизацией и конфигурируемой проверкой на null для всех полей, вы можете создать свой собственный макрос defrecord, который будет использоваться примерно так:
(defrecord name [Type :arg1 Type :arg2 Type :arg3] :allow-nulls false)
Эта способность перепрограммирования языка в самом языке — уникальное преимущество Lisp. Вы можете увидеть аспекты этой идеи описанной различными путями:
- Lisp гомоиконен. Это означает что Lisp код — это Lisp данные. Такой подход облегчает написание программ из других программ.
- Весь язык доступен здесь и сейчас. Эссе Пола Грэма «Ярость Нердов» описывает почему это так хорошо.
Также синтаксис Lisp’а устраняет правила приоритета и ассоциативности операторов. В этой книге вы не найдёте таблицу содержащую приоритеты и ассоциативность операторов. Для выражений, находящихся в скобках эти вещи не имеют никакой двусмысленности.
Оборотной стороной простого синтаксиса Lisp’а, для новичка, становится концентрация скобок и списков как основных типов данных. Clojure предлагает интересную комбинацию различных особенностей, которые будут более понятны для не-Lisp программистов.
Lisp, с меньшим количеством скобок
Clojure обеспечивает значительные преимущества для программистов, ранее работавших с Lisp:
- Clojure обобщает Lisp’овские списки в абстракцию под названием последовательность. Таким образом сохраняется мощь списков, плюс расширение мощи за счёт других структур данных.
- Clojure позволяет использовать всё богатство JVM.
- Подход Clojure к разрешению использования символов и использованию синтаксиса цитирования упрощает написание многих общих макросов.
Многие Clojure программисты — новички в Lisp и, возможно, они слышали не хорошие вещи о всех этих скобках. Clojure сохраняет скобки (и мощь Lisp!), но улучшает традиционный Lisp синтаксис несколькими способами:
- Clojure предоставляет удобный синтаксис для широкого круга структур данных за пределами списков: регулярные выражения, map, наборы, векторы и метаданные. Благодаря этим особенностям код написанный на Clojure не состоит из такого количества «списков» как большинство реализаций Lisp. Например, параметры функций определены как вектор: [] вместо списка: ().
(defn hello-world [username] (println (format "Hello, %s" username)))
Вектор более наглядно отображает список аргументов и облегчает чтение описания Clojure функций.
- В Clojure, в отличие от многих реализаций Lisp, запятая является разделителем.
; создание вектора подобно созданию массива в остальных языках [1, 2, 3, 4] -> [1 2 3 4]
- Идиоматика Clojure не содержит больше скобок чем нужно. Рассмотрим макрос cond, содержащийся и в Common Lisp и в Clojure. cond вычисляет набор пар тест/результат и возвращает первый результат для каждой истинной тестовой формы. Каждая пара тест/результат сгруппирована скобками, например:
; cond в Common Lisp (cond ((= x 10) "equal") ((> x 10) "more"))
Clojure избегает лишних скобок:
; cond в Clojure (cond (= x 10) "equal" (> x 10) "more")
Это эстетическое решение и у обоих подходов есть свои сторонники. Важный принцип заключается в том, что Clojure пытается быть менее Lisp’овым без ущерба мощи Lisp.
По сути Clojure — это замечательный Lisp как для экспертов в Lisp, так и для новичков.
Clojure — это функциональный язык.
Clojure не чисто функциональный язык, как например Haskell. Функциональные языки обладают следующими свойствами:
- Функции — это объекты первого класса. То есть, функции могут быть созданы во время выполнения, переданы в другое место, возвращены и вообще могут использоваться как другие типы данных.
- Данные не изменяются.
- Функции являются чистыми, то есть они не имеют побочных эффектов.
Для многих задач функциональные программы проще для понимания, в них труднее сделать ошибку и они значительно просты для повторного использования. Например, следующая короткая программа производит поиск в базе данных композиций по каждому композитору, который написал композицию под названием «Requiem»:
(for [c compositions :when (= "Requiem" (:name c))] (:composer c)) -> ("W. A. Mozart" "Giuseppe Verdi")
Название for не порождает цикл, но создаёт список. Предыдущий код следует читать как «For each c in compositions, where the name of c is „Requiem“, yield the composer of c.» («Для каждого c в композициях, где имя c равно „Requiem“, показать композитора c»). Понятие списков более подробно охвачено в Преобразовании Последовательностей, на странице 66.
Этот пример охватывает четыре желаемых свойства:
- Этот пример прост; он не содержит циклов, переменных или изменяемых состояний.
- Эта нить безопасна; она не требует блокировок.
- Этот пример поддаётся распараллеливанию; для создания параллельных нитей вам не придётся изменять исходный код.
- Этот пример имеет общий вид; композиции могут быть в виде простого набора или XML или результатом выборки базы данных.
Контраст между функциональными и императивными программами в том, что явное определение изменяет состояние программы. Большинство объектно-ориентированных программ написаны в императивном стиле и не имеют ни одного из свойств, перечисленных выше; они излишне сложны, нить процесса не безопасна, они не параллелизуемы и не универсальны. (Для подробного сравнения функционального и императивного стилей, перейдите с Разделу 2.7, Где оператор My для Loop?, на странице 48.)
Люди давно знают о преимуществах функциональных языков. Но всё же чистые функциональные языки как Haskell не получили всемирного использования, поскольку не всё можно легко представить в функциональном виде.
Есть четыре причины, по которым Clojure заслуживает большего внимания теперь чем функциональные языки в прошлом:
- Функциональное программирование становится всё более актуальным, чем раньше. Массовое многоядерное оборудование становится всё более распространённым и функциональное программирование позволяет легко использовать их (многоядерного оборудования) преимущества. Функциональное программирование рассматривается в Главе 4, Функциональное Программирование, на странице 85.
- Чистые функциональные языки могут создать значительные сложности при изменении состояния модели. Clojure предоставляет структурированный механизм для работы с изменяемым состоянием через программную транзакционную память и ссылки (страница 115), агенты (страница 123) и динамическое связывание (на странице 127).
- Многие функциональные языки являются статически типизированными. Clojure обладает динамической типизацией, что делает его более доступным для программистов изучающих функциональной программирование.
- Подход Clojure в вызове Java не является функциональным. Когда вы вызываете Java, вы входите в знакомый, изменяемый мир. Такой подход обеспечивает удобные условия для новичков изучающих функциональное программирование и, когда это необходимо, прагматичную альтернативу функциональному стилю. Вызов Java рассматривается в Главе 9, Спуск к Java и грязи, на странице 203.
Подход Clojure к изменению состояния делает возможным использование параллелизма без явной блокировки и дополняет функциональное ядро Clojure.
Clojure Упрощает Параллельное Программирование
Поддержка функционального программирования в Clojure облегчает написание поточно-безопасного кода. Поскольку структуры данных не изменяются, то исключается повреждение данных со стороны других активных потоков.
Однако, Clojure не ограничивается поддержкой параллельных вычислений одним только функциональным программированием. Когда вы хотите сослаться на изменяемые данные, Clojure защищает их через программную транзакционную память (Software Transactional Memory — STM). STM — это более высоко — уровневой подход к безопасности нити чем механизм блокировок Java. Защита общего состояния с помощью транзакций — это более лучший метод чем использование хрупкой, подверженной ошибкам стратегии блокировок. Этот подход более продуктивен, поскольку многие программисты хорошо понимают механизм транзакций на примере баз данных.
На пример, следующий код создаёт в памяти работающую, потоко-безопасную базу данных, содержащую данные о счетах:
(def accounts (ref #{})) (defrecord Account [id balance])
Функция ref создаёт транзакционно — защищённую ссылку на текущее состояние базы данных. Обновление базы тривиально. Следующий код добавляет новый счёт в базу данных:
(dosync (alter accounts conj (->Account "CLJ" 1000.00)))
Dosync вызывает обновление счетов, выполняемое внутри транзакции. Так осуществляется гарантия безопасности нити и этот подход легче в использовании, чем блокировка. При использовании транзакций вам не надо думать заблокирован ли объект или в каком порядке произведена блокировка. Транзакционный подход будет работать лучше под некоторыми общими сценариями, поскольку (например) читатель никогда не будет заблокирован.
Несмотря на то, что пример, показанный здесь, очень прост, технология, применённая здесь, является общей и работает с проблемами, существующими в реальном мире. Для дополнительных сведений о параллелизме и STM в Clojure читайте Главу 5, Состояние, на странице 113.
Clojure охватывает Виртуальную Машину Java (Java Virtual Machine — JVM)
Clojure даёт чистый, простой, прямой доступ к Java. Вызов любого Java API совершается прямо:
(System/getProperties) -> {java.runtime.name=Java(TM) SE Runtime Environment ... many more ...
В Clojure есть некоторый синтаксический сахар для вызова Java. Здесь мы не будем углубляться в детали (читайте Секцию 2.5, на странице 43), но имейте в виду, что следующий Clojure код содержит меньше точек и меньше скобок чем его Java версия:
// Java "hello".getClass().getProtectionDomain()
; Clojure (.. "hello" getClass getProtectionDomain)
Clojure предоставляет простые функции для реализации Java интерфейсов и подклассов для Java классов. Кроме того, все Clojure функции поддерживают Вызываемость (Callable) и Запускаемость (Runnable). Таким образом задача передачи следующей анонимной функции в конструктор Java Нити становится тривиальной.
(.start (new Thread (fn [] (println "Hello" (Thread/currentThread))))) -> Hello #<Thread Thread[Thread-0,5,main]>
Этот забавный вывод — это способ Clojure печати Java экземпляра. Thread — это имя класса экземпляра, а Thread[Thread-0,5,main] — это toString представление экземпляра. (Учтите, что в предыдущем примере новая нить будет работать до конца, но её вывод может в случайном порядке смешиваться с приглашением REPL. Это не проблема Clojure, но просто результат работы более чем одной нити записывающей данные в поток вывода.)
Поскольку синтаксис вызова Java в Clojure чист и прост, то идиоматически лучше использовать Java напрямую, чем скрывать Java за Lisp подобными обёртками.
Ну а теперь, когда вы узнали несколько причин в пользу использования Clojure, пришло время писать код.
Быстрый Старт Кодирования в Clojure
Для запуска Clojure и кода из этой книги, вам нужны две вещи:
- Java runtime. Скачайте и установите Java 5 версии или выше. Java 6-й версии имеет значительные улучшения и улучшенный отчёт о исключениях, поэтому эта версия предпочтительней.
- Leiningen. Leiningen — это инструмент управления зависимостями и запуск задач от вашего кода. Это наиболее широко используемый инструмент для таких задач в пространстве Clojure.
Вы будете использовать Leiningen для установки Clojure и всех зависимостей для демонстрационных текстов программ из этой книги. Если вы уже установили Leiningen, то вы уже должны были ознакомиться с его основами. Если вы не устанавливали Leiningen, то вам следует прочитать быстрый курс по Leiningen на его странице в GitHub, там вы найдёте базовые инструкции по установке. Не беспокойтесь об изучении всего прямо сейчас, на протяжении этой книги вы найдёте сведения, которые понадобятся вам.
При работе с книгой используйте версию Clojure соответствующую примерам в этой книге. После прочтения книги, вы можете следовать инструкциям в Самостоятельной Сборке Clojure, на странице 12 для сборки проектов, соответствующих последней, на данной минуте, версии Clojure.
Если вы хотите найти инструкции по скачиванию текстов кодов Прочитайте Секцию 6, Скачивание Примеров Кода. Когда вы загрузите пример кода, вам понадобится использовать Leiningen для получения зависимостей. Перейдите в директорию где расположен пример кода и выполните от пользователя root следующее:
lein deps
Зависимости будут автоматически скачаны и размещены в соответствующие места. Вы можете протестировать ваше установленное программное обеспечение с помощью перехода в место, где расположен демонстрационный код и запустить Clojure’овский read-eval-print loop (REPL). Leiningen содержит сценарий, запускающий REPL, который в свою очередь загружает Clojure с нужными зависимостями, именно этот способ мы будем использовать в книге.
lein repl
При благополучной загрузке REPL появится приглашение в виде user=>:
Clojure
user=>
Теперь вы готовы для «Hello World.»
Самостоятельная Сборка Clojure.
Вам может понадобиться собрать Clojure из исходных кодов для получения доступа к новой функциональности и исправлений ошибок. Вот как это делается:
git clone git://github.com/clojure/clojure.git cd clojure mvn package
Этот код регулярно синхронизируется с текущей разрабатываемой версией Clojure. Для того, чтобы узнать с какими версиями был совместима эта версия Clojure, прочитайте версию ревизии в файле README.
Использование REPL
Начнём изучение методов использования REPL, давайте создадим несколько вариантов «Hello World.» Первое, напечатаем(println "hello world")
user=> (println "hello world") -> hello world
Вторая строка, hello world — это консольный вывод того, что вы запросили.
Далее, инкапсулируем ваш «Hello World» в функцию, которая обратится к человеку по имени:
(defn hello [name] (str "Hello, " name)) -> #'user/hello
Давайте разберём, то что мы написали по порядку:
- defn определяет функцию.
- hello — это имя функции.
- hello получает один аргумент — name.
- str — это вызов функции, которая объединяет произвольный список аргументов в строку.
- defn, hello, name и str — это всё символы, имена которых относятся к определённым вещам. Доступные символы описаны в Символах, на странице 25.
Взгляните на возвращённое значение, #'user/hello. Префикс #' обозначает что функция была сохранена в переменной Clojure, а user — это пространство имён функций. (Пространство имён user устанавливается по умолчанию в REPL, подобно пакету по умолчанию в Java.) На данный момент вам не надо переживать о переменных и пространствах имён; они охвачены в Разделе 2.4, Переменные, Привязки и Пространства Имён, на странице 36.
Теперь вы можете вызвать hello и передать ваше имя:
user=> (hello "Stu") -> "Hello, Stu"
Если вы перевели ваш REPL в состояние, которое ваш смущает, то самое простое решение — это убить REPL с помощью CTRL+C в Windows или CTRL+D в *nix, а затем запустить ещё один.
Специальные Переменные
REPL включает в себя несколько полезных специальных переменных. Когда вы работаете в REPL, результат последних трёх вычислений сохраняется в специальные переменные: *1, *2 и *3 соответственно. Использование этих переменных сильно упрощает работу. Выполните hello с несколькими различными именами:
user=> (hello "Stu") -> "Hello, Stu" user=> (hello "Clojure") -> "Hello, Clojure"
Теперь вы можете использовать специальные переменные для комбинирования результатов последних вычислений:
(str *1 " and " *2) -> "Hello, Clojure and Hello, Stu"
Если вы допустили ошибку в REPL, вы увидите Java исключение. Детали будут часто пропускаться для краткости. На пример, деление на ноль будет следующим:
user=> (/ 1 0) -> ArithmeticException Divide by zero clojure.lang.Numbers.divide
Эта проблема очевидна, но иногда проблема бывает более тонкой и вам может понадобится детализированная трассировка стека. Специальная переменная *e хранит в себе последнее исключение. Поскольку исключения Clojure — это Java исключения, то вы можете запросить stacktrace через вызов pst (print stacktrace — напечатать stacktrace). (pst доступен только в Clojure версии 1.3.0 и выше.)
user=> (pst) -> ArithmeticException Divide by zero | clojure.lang.Numbers.divide | sun.reflect.NativeMethodAccessorImpl.invoke0 | sun.reflect.NativeMethodAccessorImpl.invoke | sun.reflect.DelegatingMethodAccessorImpl.invoke | java.lang.reflect.Method.invoke | clojure.lang.Reflector.invokeMatchingMethod | clojure.lang.Reflector.invokeStaticMethod | user/eval1677 | clojure.lang.Compiler.eval | clojure.lang.Compiler.eval | clojure.core/eval
Взаимодействие с Java рассматривается в Главе 9, Спуск к Java и грязи, на странице 203.
Если у вас есть блок кода и этот код слишком большой для ручного ввода в REPL, сохраните этот код в файл и затем загрузите этот файл через REPL. Вы можете использовать абсолютные пути или относительные пути, в зависимости от того, где вы запустили REPL.:
; сохранить некоторый код в temp.clj и затем ... user=> (load-file "temp.clj")
REPL — это потрясающая среда для испытания идей и получения непосредственного результата. Для более лучших результатов, держите REPL открытым всё время при чтении этой книги.
Добавление общего состояния
Функция hello в предыдущем разделе является чистой; то есть она не имеет побочных эффектов. Чистые функции удобно разрабатывать, тестировать, понимать, и вы должны отдавать им предпочтение для для использования их во многих задачах.
Тем не менее, многие программы имеют общие состояния и используют не чистые функции для управления этими общими состояниями. Давайте расширим функцию hello так, чтобы можно было следить за последними посетителями. Первое что нам понадобится, это структура данных для отслеживания посетителей. Выполним следующий приём:
#{} -> #{}
#{} — это литерал для пустого набора. Далее вам понадобится conj:
(conj coll item)
conj — это сокращение от conjoin (соединить), в результате создаётся новая коллекция с добавленным элементом. Выполним conj для элемента и набора и посмотрим какой набор у нас получился в результате:
(conj #{} "Stu") -> #{"Stu"}
Теперь, когда вы можете создавать новые наборы, вам нужен способ отслеживания текущего набора посетителей. Для этих целей Clojure предоставляет несколько ссылочных типов (refs). Наиболее базовый тип ссылки — это атом:
(atom initial-state)
Для того, чтобы назвать ваш атом, вы можете использовать def:
(def symbol initial-value?)
def похож на defn, но носит более общий характер. def может определять функции или данные. Используйте atom для создания атома и используйте def для связывания атома с именем visitors:
(def visitors (atom #{})) -> #'user/visitors
Для обновления ссылки, вы должны использовать функцию подобную swap!:
(swap! r update-fn & args)
swap! применяет update-fn к ссылке r, с необязательными аргументами args, используемыми при необходимости. Примените swap! к посетителю в visitors, используя conj в качестве функции обновления:
(swap! visitors conj "Stu") -> #{"Stu"}
atom — это один из нескольких типов ссылки в Clojure. Выбирайте тип ссылки, соответствующий вашим потребностям (обсуждено в Главе 5, Состояние, на странице 113).
Вы можете в любой момент заглянуть внутрь ссылки с помощью функции deref или с помощью сокращения @:
(deref visitors) -> #{"Stu"} @visitors -> #{"Stu"}
Теперь вы готовы к созданию новой, более сложной версии hello:
(defn hello "Выводит в *out* сообщение hello. Называет вас по имени пользователя И знает вас если вы здесь были раньше." [username] (swap! visitors conj username) (str "Hello, " username))
Далее, надо проверить корректность отображения посетителей в памяти:
(hello "Rich") -> "Hello, Rich" @visitors -> #{"Aaron" "Stu" "Rich"}
По всей вероятности, ваш список посетителей будет отличаться от показанного здесь. Это проблема с состоянием! Ваши результаты будут меняться в зависимости от происходящих вещей. Можно рассуждать о известной функции, выполняющие последовательные локальные действия. Но рассуждения о состоянии требует полного знания её истории.
Избегайте состояний где только это возможно. Но если без них не обойтись, разумно реализуйте состояния и делайте их управляемыми через использование таких ссылок, как атомы. Атомы (и все остальные типы ссылок Clojure) безопасны для много поточных приложений и процессоров. Более того, эта безопасность осуществляется без использования блокировок, которые, как известно, сложны в использовании.
К этому моменту вы уже должны чувствовать себя уверенно в REPL и можете вводить небольшие фрагменты кода. Работа с более крупными кусками кода не отличается от работы с маленьким кодом; вы можете также загружать и запускать Clojure библиотеки из REPL. Давайте посмотрим как это делается.
Изучение библиотек Clojure
Код Clojure упакован в библиотеки. Каждая Clojure библиотека принадлежит пространству имени, что является аналогом Java пакета. Вы можете загружать Clojure библиотеку с помощью функции require:
(require quoted-namespace-symbol)
Когда вам требуется библиотека под именем clojure.java.io, Clojure производит поиск файла с именем clojure/java/io.clj в CLASSPATH. Попробуйте выполнить следующие команды:
user=> (require 'clojure.java.io) -> nil
Наличие одинарной кавычки (') в начале обязательно. Эта кавычка закавычивает имя библиотеки (закавычивание рассмотрено в Разделе 2.2, Считыватель Макроса, на странице 30). В случае благополучного выполнения операции возвращается nil. Находясь в этой библиотеке, попробуйте загрузить демонстрационный код для этой главы, examples.introduction:
user=> (require 'examples.introduction) -> nil
Библиотека examples.introduction включает реализацию чисел Фибоначчи, которая является традиционной «Hello World» программой для функциональных языков программирования. Более подробно мы рассмотрим числа Фибоначчи в Разделе 4.2, Как Быть Ленивым, на странице 90. На данный момент вам надо просто убедиться, что вы можете выполнить демонстрационную функцию fibs. Для получения первых десяти чисел Фибоначчи введите в REPL следующие строки:
(take 10 examples.introduction/fibs) -> (0 1 1 2 3 5 8 13 21 34)
Если вы удачно установили примеры книги, то вы увидите первые десять чисел Фибоначчи.
Все модули, представленные в примерах из книги были протестированы с помощью тестов, расположенных в директории examples/test. Сами тесты для примеров в этой книге не охвачиваются, но вам они могут пригодиться в качестве справки. Вы можете самостоятельно запустить тестирование модуля с помощью lein test.
Require и Use
Когда вы выполняете require к библиотеке Clojure, то вы должны ссылаться на элемент библиотеки через имя, указывающее на пространство имени. При использовании fibs вы должны сказать: examples.introduction/fibs. Убедитесь в этом, запустив новый экземпляр REPL (Создание нового REPL предотвращает конфликты между одноимёнными функциями в предыдущей работе и функциями из демонстрационного кода. Эта проблема не будет актуальной в разработке настоящего приложения, мы перейдём к этому вопросу в Пространстве имён, на странице 40.) и попробуйте выполнить следующее:
(require 'examples.introduction) -> nil (take 10 examples.introduction/fibs) -> (0 1 1 2 3 5 8 13 21 34)
Полностью определённые имена быстро устаревают. Вы можете выполнить функцию ссылки (refer) на пространство имён, создающее отображение всех имён на вашем текущем пространстве имён:
(refer quoted-namespace-symbol)
Вызовите refer на examples.introduction и убедитесь что возможен прямой вызов fibs:
(refer 'examples.introduction) -> nil (take 10 fibs) -> (0 1 1 2 3 5 8 13 21 34)
Для удобства, функция use выполнит require и refer на библиотеку в одном шаге:
(use quoted-namespace-symbol)
В новом REPL вы можете выполнить следующие шаги:
(use 'examples.introduction) -> nil (take 10 fibs) -> (0 1 1 2 3 5 8 13 21 34)
При работе с примерами из книги, вы можете вызывать require или use с флагом :reload для принудительной перезагрузки библиотеки:
(use :reload 'examples.introduction) -> nil
Флаг :reload полезен при внесении изменений и просмотре результатов без перезагрузки REPL.
Поиск Документации
Довольно часто вы сможете найти документацию прямо в REPL. Основная вспомогательная функция (на самом деле doc является макросом) — это doc:
(doc name)
Используйте doc для вывода на экран документации о str:
user=> (doc str) ------------------------- clojure.core/str ([] [x] [x & ys]) With no args, returns the empty string. With one arg x, returns x.toString(). (str nil) returns the empty string. With more than one arg, returns the concatenation of the str values of the args.
Первая строка вывода от doc содержит полное имя функции. Следующая строка содержит список возможных аргументов, генерируемых непосредственно из кода. (Некоторые общие имена аргументов и их использование изложены в Соглашении об Именах Параметров, на странице 19.) И наконец, следующие строки содержат строку документации функции, если функция их содержит, конечно.
Вы можете добавить строку документации к вашим собственным функциям, разместив строку сразу после имени функции: src/examples/introduction.clj
(defn hello "Writes hello message to *out*. Calls you by username" [username] (println (str "Hello, " username)))
Иногда вы не будете знать по какому имени искать документацию. Функция find-doc выводит все данные о встречаемом регулярном выражении или строке, введённой вами, в выводе doc:
(find-doc s)
Используйте find-doc для нахождения сокращений в Clojure:
user=> (find-doc "reduce") ------------------------- clojure/areduce ([a idx ret init expr]) Macro ... details elided ... ------------------------- clojure/reduce ([f coll] [f val coll]) ... details elided ...
reduce выполняет сокращение Clojure коллекций, подробно об этом рассказано в Трансформировании Последовательностей, на странице 66. areduce используется для взаимодействия с Java массивами, подробное описание вы можете найти в Использование Java Коллекций, на странице 216.
Большая часть Clojure была написана в Clojure и поэтому рекомендуется читать исходный код Clojure. Вы можете взглянуть на исходный код функций Clojure с помощью repl библиотеки.
(clojure.repl/source a-symbol)
Попытайтесь посмотреть на исходный код простой функции identity:
(use 'clojure.repl) (source identity) -> (defn identity "Returns its argument." {:added "1.0" :static true} [x] x)
Конечно, вы можете использовать Java Reflection API. Вы можете использовать такие методы как class, ancestors и instance? для отображения нижележащей модели Java объекта и таким образом, сказать, что коллекции Clojure также являются коллекциями Java:
-> #{clojure.lang.ILookup clojure.lang.Sequential java.lang.Object clojure.lang.Indexed java.lang.Iterable clojure.lang.IObj clojure.lang.IPersistentCollection clojure.lang.IPersistentVector clojure.lang.AFn java.lang.Comparable java.util.RandomAccess clojure.lang.Associative clojure.lang.APersistentVector clojure.lang.Counted clojure.lang.Reversible clojure.lang.IPersistentStack java.util.List clojure.lang.IEditableCollection clojure.lang.IFn clojure.lang.Seqable java.util.Collection java.util.concurrent.Callable clojure.lang.IMeta java.io.Serializable java.lang.Runnable}
Полное API Clojure задокументировано он-лайн по адресу http://clojure.github.com/clojure. Правая панель содержит упорядоченные по имени ссылки на все функции и макросы, а левая панель содержит ссылки на обзоры статей по различным особенностям Clojure.
Соглашения о Параметрах Имён
Строки документации для reduce и areduce содержат несколько кратких имён параметров. Ниже перечислены некоторые имена параметров и как они расшифровываются:
Параметр | Обозначение |
---|---|
a | Java массив (Java array) |
agt | Агент (Agent) |
coll | Коллекция (Collection) |
expr | Выражение (Expression) |
f | Функция (Function) |
idx | Индекс (Index) |
r | Ссылка (ref) |
v | Вектор (Vector) |
val | Значение (Val) |
Эти имена могут показаться короткими, но на это есть весомая причина: основанием для «хороших названий» часто служат Clojure функции! Название параметров именами, одинаковыми с именами функций, не является нарушением, но является плохим стилем: параметр будет перекрывать одноимённую функцию, которая будет не доступна в пределах параметра. Таким образом, не называйте ваши ссылки как ref, ваших агентов как agent и счётчики как count. Эти имена предназначены для функций.
Заключение
Вы только что прошли беглый курс по Clojure. Вы увидели выразительный синтаксис Clojure, изучили подход Clojure к языку Lisp и поняли насколько легко вызвать Java код из Clojure.
Вы запустили Clojure в своей среде и написали короткую программу в REPL, демонстрирующую функциональное программирование и примерную модель работы с состоянием. Теперь пришло время изучить оставшуюся часть языка.
Глава 2. Изучение Clojure
Clojure предлагает вам большую мощь функционального стиля, поддержку многопоточности и чистое взаимодействия с Java. Но прежде чем вы начнёте изучение этих особенностей, вам следует ознакомиться с базой языка. В этой главе, вам предлагается краткий тур по языку Clojure, включающий в себя следующие вещи:
- Формы
- Считывающие макросы
- Функции
- Привязки и пространства имён
- Управление выполнением программы
- Метаданные
Если вы привыкли к императивным языкам программирования, то вам покажется что в этом туре не хватает некоторых ключевых конструкций языка, таких как переменные и for в циклах. Раздел 2.7, Где Мой for в цикле?, на странице 48 покажет вам как улучшить жизнь от for в циклах и переменных.
Clojure очень выразительный язык, но в этой главе мы быстро пробежимся по концепциям этого языка. Если вы не поймёте какие-то детали, то не стоит беспокоиться; мы более подробно рассмотрим эту тему в последующих главах. Если есть возможность, перейдите в REPL и поэкспериментируйте с примерами, приведёнными в тексте.
Формы
Clojure гомоиконен, это означает что код Clojure состоит из данных Clojure. Когда вы запускаете программу, написанную на Clojure, часть Clojure называемая как считыватель (reader) считывает текст программы в частях, называемых формами и транслирует их в структуры данных Clojure. Далее происходит компиляция и выполнение этих структур данных.
Рассмотренные в этой книге формы Clojure собраны в Таблице № 1, Формы Clojure. Для того, чтобы посмотреть формы в действии, запустите любую простую форму, поддерживающую числовые типы.
Таблица № 1 — Формы Clojure
Форма | Пример(ы) | Соответствующий раздел |
---|---|---|
Булево значение (Boolean) | true, false | Булевы значения и nil, на странице 27 |
Символ (Character) | \a | Строки и Символы, на странице 25 |
Ключевое слово (Keyword) | :tag, :doc | Отображения, Ключевые Слова и Записи, на странице 28 |
Список (List) | (1 2 3), (println "foo") | Глава 3, Объединение Данных с Последовательностями, на странице 55 |
Отображение (Map) | {:name "Bill", :age 42} | Отображения, Ключевые Слова и Записи, на странице 28 |
Nil | nil | Булевы значения и nil, на странице 27 |
Число (Number) | 1, 4.2 | Использование числовых типов, на странице 22 |
Набор (Set) | #{:snap :crackle :pop} | Глава 3, Объединение Данных с Последовательностями, на странице 55 |
Строка (String) | "hello" | Строки и Символы, на странице 25 |
Символ (Symbol) | user/foo, java.lang.String | Символы, на странице 25 |
Вектор (Vector) | [1 2 3] | Глава 3, Объединение Данных с Последовательностями, на странице 55 |
Использование Числовых Типов
Числовые литералы являются формами. Числа просто вычисляются в самих себя. Если вы введёте число REPL вернёт его вам:
42 -> 42
Вектор из чисел это ещё один вид формы. Создать вектор из чисел 1, 2 и 3:
[1 2 3] -> [1 2 3]
Список это ещё один вид формы. Список — это «всего лишь данные», он (список) также используется для вызова функций. Создайте список в котором первый элемент является функцией Clojure, например символ +:
(+ 1 2) -> 3
Как вы можете видеть, Clojure вычисляет список как вызов функции. Стиль, в котором первым элементом располагается функция, называется префиксной нотацией (В частности, она называется Кембриджской польской нотацией.), в отличие от более привычной инфиксной нотации 1 + 2 = 3. Конечно, префиксная нотация замечательно подходит для функций, имена которых состоят из слов. На пример, большинство программистов сразу разберутся в следующем выражении с concat во главе:
(concat [1 2] [3 4]) -> (1 2 3 4)
В Clojure так же просто можно выразить и другие математические операции, функции также располагаются первыми.
Практическим превосходством префиксной нотации является тот факт, что вы можете легко расширить её для произвольного числа аргументов:
(+ 1 2 3) -> 6
Даже в случае отсутствия аргументов префиксная нотация будет работать так, как и ожидалось, возвращая ноль. Такой подход помогает в устранении хрупкой, специальной логики для граничных состояний:
(+) -> 0
Многие математические операторы и операторы сравнения имеют те же имена и ту же семантику, что и в других языках программирования. Сложение, вычитание, умножение, сравнение и равенство работают также как вы можете ожидать:
(- 10 5) -> 5 (* 3 10 10) -> 300 (> 5 2) -> true (>= 5 5) -> true (< 5 2) -> false (= 5 2) -> false
Деление может удивить вас:
(/ 22 7) -> 22/7
Как вы можете видеть, Clojure имеет встроенный тип Ratio (Соотношение):
(class (/ 22 7)) -> clojure.lang.Ratio
Если вы хотите получить десятичное деление, используйте число с плавающей точкой в делимом:
(/ 22.0 7) -> 3.142857142857143
Если вам нужны целые числа, то вы можете получить целое частное и остаток с помощью quot и rem:
(quot 22 7) -> 3 (rem 22 7) -> 1
Если вам нужно число произвольной точности с плавающей точкой, добавьте M к числу, для создания BigDecimal литерала:
(+ 1 (/ 0.00001 1000000000000000000)) -> 1.0 (+ 1 (/ 0.00001M 1000000000000000000)) -> 1.00000000000000000000001M
Для целых чисел произвольной точности, вы можете добавить N, что приведёт к созданию BigInt литерала:
(* 1000N 1000 1000 1000 1000 1000 1000) -> 1000000000000000000000N
Учтите, что необходим литерал BigInt и это повлияет на всё вычисление.
Символы
Такие формы как +, concat, java.lang.String называются символами и используются для именования вещей. На пример, + — это имя функции, которая складывает вещи. Символами называются все вещи в Clojure:
- Функции вроде str и concat
- «Операторы», родственные + и -, которые на деле являются всего лишь функциями
- Java классы, подобные java.lang.String и java.util.RandomJava
- Пространства имён, подобные clojure.core, и Java пакеты, подобные java.lang
- Структуры данных и ссылки
Символы не могут начинаться с цифры, но могут состоять из алфавитно-числовых символов, плюс +, -, *, /, !, ?, ., и _. Список допустимых символов — это минимальный набор, который должен поддерживаться в Clojure. В своём коде вы должны придерживаться этих символов, но не думайте, что этот список является исчерпывающим. Для внутренних нужд Clojure может использовать другие, не задокументированные знаки в символах и в дальнейшем список разрешённых символов может быть расширен. Обновления списков разрешённых символов доступны в он-лайн документации Clojure.
Clojure по особому трактует символы / и . в контексте поддержки пространства имён; за деталями читайте Пространство имён, на странице 40.
Строки и Символы
Строки — это другой считыватель формы. Строки Clojure — это Java строки. Они заключены в двойные кавычки и могут занимать несколько строк:
"This is a\nmultiline string" -> "This is a\nmultiline string" "This is also a multiline string" -> "This is also\na multiline string"
Как вы уже заметили, REPL всегда показывает строковые литералы с экранированным символом новой строки. Если же вы напечатали много строчную строку, то на выходе вы получите много строчную строку:
(println "another\nmultiline\nstring") | another | multiline | string -> nil
Clojure не использует обвёртки для большинства строковых функций Java. Взамен вы можете прямо вызывать их через форму взаимодействия с Java:
(.toUpperCase "hello") -> "HELLO"
Точка перед toUpperCase говорит Clojure о том, что следующее имя следует воспринимать как метод Java, а не как функцию Clojure.
Есть одна строковая функция, для которой Clojure применяет обвёртку, эта функция — toString. Вам нет нужды вызывать toString напрямую. Взамен вызова toString используйте функцию Clojure str:
(str & args)
Есть две вещи, которыми str отличается от toString. Она объединяет несколько раздельных аргументов и пропускает nil без ошибок:
(str 1 2 nil 3) -> "123"
Clojure символы являются Java символами. Их буквальный синтаксис следующий: \{letter}, где letter может быть литерой или именем символа: backspace, formfeed, newline, return, space или tab:
(str \h \e \y \space \y \o \u) -> "hey you"
Как и в случае со строками, Clojure не использует обвёртку для символьных функций Java. Взамен вы можете использовать форму взаимодействия с Java, например Character/toUpperCase:
(Character/toUpperCase \s) -> \S
Формы взаимодействия с Java рассматриваются в Разделе 2.5, Вызов Java, на странице 43. Для более подробной информации о классе Java символов, обращайтесь к документации API по адресу http://tinyurl.com/java-character .
Строки — это последовательности символов. При применении функций Clojure для работы с последовательностями к строке, вы получите последовательность символов. Представьте, что вам нужно скрыть секретное послание через чередование его со вторым, безобидным посланием. Вы можете использовать interleave для комбинирования двух сообщений:
(interleave "Attack at midnight" "The purple elephant chortled") -> (\A \T \t \h \t \e \a \space \c \p \k \u \space \r \a \p \t \l \space \e \m \space \i \e \d \l \n \e \i \p \g \h \h \a \t \n)
Такой подход работает, но, возможно, вам понадобится получить конечную строку для последующей отправки. Первой мыслью будет использование str для упаковки символов в строку, но результат будет не тот, что мы ожидали:
(str (interleave "Attack at midnight" "The purple elephant chortled")) -> "clojure.lang.LazySeq@d4ea9f36"
Проблема в том, что str работает с несколькими аргументами, но вы передали единственный аргумент, который содержит список аргументов. Решением будет использование apply:
(apply f args* argseq)
apply принимает функцию f, некоторые необязательные аргументы args и последовательность аргументов названную как argseq. Далее происходит вызов f, с разворачиванием аргументов args и argseq в список аргументов. Используйте (apply str …) для сборки строки из последовательности символов:
(apply str (interleave "Attack at midnight" "The purple elephant chortled")) -> "ATthtea cpku raptl em iedlneipghhatn"
Вы можете использовать (apply str …) для расшифровки сообщения:
(apply str (take-nth 2 "ATthtea cpku raptl em iedlneipghhatn")) -> "Attack at midnight"
Вызов (take-nth 2 …) получает каждый второй элемент последовательности, расшифровывая зашифрованную строку.
Булевы значения и nil
Правила Clojure для булевых значений просты для понимания:
- true — это true и false — это false.
- В дополнение к false, nil также вычисляется в false при использовании его в булевом контексте.
- Кроме false и nil, в булевом контексте, всё остальное вычисляется в true.
Предупреждение для Lisp программистов: в Clojure пустой список не считается ложным:
; (if part) (else part) (if () "We are in Clojure!" "We are in Common Lisp!") -> "We are in Clojure!"
Предупреждение для C программистов: в Clojure ноль не является, пример:
; (if part) (else part) (if 0 "Zero is true" "Zero is false") -> "Zero is true"
Предикат (predicate) — это функция, которая возвращает либо true, либо false. В Clojure считается правилом завершать знаком вопроса имена предикатов, например: true?, false?, nil? и zero?:
(true? expr) (false? expr) (nil? expr) (zero? expr)
true? проверяет является ли значение действительно истинным, но игнорирует то, что это значение может вычислять в истинное в булевом контексте. Единственная вещь, которая будет истинной в true? — это сам true:
(true? true) -> true (true? "foo") -> false
nil? и false? работают также. Только nil будет соответствовать nil? и только false будет соответствовать false?.
zero? работает с любыми числовыми типами, возвращая истинное значение при нуле:
(zero? 0.0) -> true (zero? (/ 22 7)) -> false
(find-doc #"\?$")
Отображения, Ключевые Слова и Записи
Отображения (maps) Clojure — это коллекция пар ключей/значений. Отображения выражаются в виде формы окружённой фигурными скобками. Вы можете использовать литералы отображений для создания таблицы поиска для разработчиков языков программирования:
(def inventors {"Lisp" "McCarthy" "Clojure" "Hickey"}) -> #'user/inventors
Значение «McCarthy» ассоциировано с ключом «Lisp», а значение «Hickey» ассоциировано с ключом «Clojure».
Если вы хотите облегчить чтение, то вы можете использовать запятые для разделения каждой пары ключ/значение. Clojure это не волнует. Он (Clojure) рассматривает запятые как пробелы:
(def inventors {"Lisp" "McCarthy", "Clojure" "Hickey"}) -> #'user/inventors
Отображения являются функциями. Если вы передадите ключ отображению, оно вернёт значение ключа или вернёт nil в случае, когда ключ не найден:
(inventors "Lisp") -> "McCarthy" (inventors "Foo") -> nil
Вы можете использовать более подробную функцию get:
(get the-map key not-found-val?)
get позволяет определить другое значение для отсутствующих ключей:
(get inventors "Lisp" "I dunno!") -> "McCarthy" (get inventors "Foo" "I dunno!") -> "I dunno!"
Поскольку структуры данных Clojure являются неизменными, и реализация hashCode корректна, то любая структура данных Clojure может быть ключом в отображении. Тем не менее, самым распространённым типом ключа в Clojure является ключевое слово.
Ключевое слово (keyword) подобно символу, за исключением того, что ключевые слова начинаются с двоеточия (:). Ключевые слова распознаются в самих себя:
:foo -> :foo
Символы хотят обратится к чему-либо, а ключевые слова — нет:
foo -> CompilerException java.lang.RuntimeException: Unable to resolve symbol: foo in this context
Тот факт, что ключевые слова распознаются в самих себя делает их полезными в роли ключей. Вы можете переопределить отображение inventors через ключевые слова в роли ключей:
(def inventors {:Lisp "McCarthy" :Clojure "Hickey"}) -> #'user/inventors
Ключевые слова также являются функциями. Они принимают аргумент отображения и ищут себя в этом отображении. После применения ключевых слов в inventors вы можете искать создателя с помощью вызова отображения или с помощью вызова ключа:
(inventors :Clojure) -> "Hickey" (:Clojure inventors) -> "Hickey"
Эта гибкость будет очень удобна при вызове функций высокого уровня, таких как ссылки и API агентов в Главе 5, Состояние, на странице 113.
Если несколько отображений обладают общими ключами, вы можете задокументировать (и соблюсти) этот факт через создание записи с помощью defrecord:
(defrecord name [arguments])
Имя аргумента будет преобразовано в ключи, со значениями, переданными им при создании записи. Используйте defrecord для создания записи Book:
(defrecord Book [title author]) -> user.Book
Затем вы сможете создать запись с помощью user.Book.:
(->Book "title" "author")
После того, как вы создали Book, оно будет вести себя подобно другим отображениям:
(def b (->Book "Anathem" "Neal Stephenson")) -> #'user/b b -> #:user.Book{:title "Anathem", :author "Neal Stephenson"} (:title b) -> "Anathem"
Также записи имеют альтернативные вызовы. Ниже приведён оригинальный синтаксис:
(Book. "Anathem" "Neal Stephenson") -> #user.Book{:title "Anathem", :author "Neal Stephenson"}
Также вы можете создать запись с помощью литерального синтаксиса. Это делается путём набора того, что вы хотите получить в REPL. Единственное отличие, которое вы заметите — это то, что литералы записи должны быть полностью определёнными:
#user.Book{:title "Infinite Jest", :author "David Foster Wallace"} -> #user.Book{:title "Infinite Jest", :author "David Foster Wallace"}
К этому времени вы познакомились с числовыми литералами, списками, векторами, символами, строками, знаками, Булевыми значениями, записями и nil. Остальные формы, по мере необходимости, будут рассматриваться в оставшейся части книги. В качестве ссылки, смотрите Таблицу № 1, Формы Clojure, в которой перечислены все формы, использованные в книге, краткий пример каждой из них и указание на более полную информацию.
Считывающие Макросы
Формы Clojure читаются считывателем (reader), который конвертирует текст в структуры данных Clojure. В дополнение к базовым формам, считыватель Clojure также принимает набор считывающего макроса (reader macros). (Считывающий макрос полностью отличается от макросов, обсуждаемых в Главе 7, Макросы, на странице 165.) Считывающий макрос — это специальный считыватель, поведение которого управляется префиксом макрос символов (macro characters).
Самый известный считывающий макрос — это комментарий. Макрос символ, включающий комментарий, — это точка с запятой (;) и обозначает «игнорировать всё до конца этой строки» для специального поведения считывателя.
Считывающий макрос — это аббревиатура более длинных списочных форм, считывающие макросы используются для уменьшения путаницы. Вы уже видели один из них. Символ кавычки (') предотвращает вычисление:
'(1 2) -> (1 2)
'(1 2) — это эквивалентно более длинному (quote (1 2)):
(quote (1 2)) -> (1 2)
Другие считывающие макросы будут рассмотрены позже в этой книге. В следующей таблице вы найдёте краткий обзор синтаксиса и ссылки на разделы, в которых рассматривается каждый считывающий макрос.
Считывающий Макрос | Пример(ы) | Соответствующий раздел |
---|---|---|
Анонимная функция | #(.toUpperCase %) | Раздел 2.3, Функции, на странице 32 |
Комментарий | ; single-line comment | Считывающие Макросы |
Deref | @form => (deref form) | Глава 5, Состояние, на странице 113 |
Метаданные (Metadata) | ^metadata form | Раздел 2.8, Метаданные, на странице 51 |
Закавычивание (Quote) | ' form=> (quote form) | Формы |
Шаблон регулярного выражения (Regex pattern) | #"foo" => a
java.uti l .regex.Pattern |
Последовательные Регулярные Выражения, на странице 72 |
Синтаксическая кавычка | `x | Раздел 7.3, Упрощение Макросов, на странице 172 |
Разкавычивание | ~ | Раздел 7.3, Упрощение Макросов, на странице 172 |
Сращивание разкавычиваний (Unquote-splicing) | ~@ | Раздел 7.3, Упрощение Макросов, на странице 172 |
Закавычивание переменной | #'x => (var x) | Глава 5, Состояние, на странице 113 |
Clojure не позволяет программам определять новые считывающие макросы. Причина этого решения была объяснена (и обсуждена) в списке рассылки Clojure. Если вы пришли в Clojure из языка программирования Lisp, то это вас неприятно удивит. Мы понимаем ваше недовольство. Но этот компромисс в гибкости даёт более стабильное ядро Clojure. Пользовательские считывающие макросы усложнят взаимодействие между Clojure программами и ухудшат читаемость кода.
Функции
В Clojure вызов функции — это просто список, первый элемент которого определяется в функцию. На пример, этот вызов str соединяет его аргументы и создаёт новую строку:
(str "hello" " " "world") -> "hello world"
Обычно, имена функций содержат дефисы, пример: clear-agent-errors. Если функция является предикатом, то по соглашению, её имя должно заканчиваться знаком вопроса. На пример, следующие предикаты проверяют тип их аргумента и все они заканчиваются знаком вопроса:
(string? "hello") -> true (keyword? :hello) -> true (symbol? 'hello) -> true
Для определения своей собственной функции, используйте defn:
(defn name doc-string? attr-map? [params*] body)
attr-map ассоциирует метаданные с переменными функций. Отдельное рассмотрение находится в Разделе 2.8, Метаданные, на странице 51. Для демонстрации остальных компонентов определения функции, создайте функцию greeting, которая получает имя и возвращает приветствие со словом «Hello» в начале:
(defn greeting "Returns a greeting of the form 'Hello, username.'" [username] (str "Hello, " username))
Вы можете вызвать greeting:
(greeting "world") -> "Hello, world"
Также вы можете обратиться к документации по greeting:
user=> (doc greeting) ------------------------- exploring/greeting ([username]) Returns a greeting of the form 'Hello, username.'
Что будет делать greeting, если вызвать его без username?
(greeting) -> ArityException Wrong number of args (0) passed to: user$greeting clojure.lang.AFn.throwArity (AFn.java:437)
Функции Clojure соблюдают свою арность (arity) — то есть число ожидаемых аргументов. Если вы вызовете функцию с неправильным числом аргументов, Clojure выдаст ArityException. Если вы хотите создать greeting, приветствующий всех при пропуске username, вы можете использовать альтернативную форму defn, которая получает список нескольких аргументов и тел методов:
(defn name doc-string? attr-map? ([params*] body)+)
Одна и та же функция при различной арности может вести себя по разному, таким образом, вы можете легко создать greeting с нулевым числом аргументов, которое, на самом деле, будет функцией greeting с одним аргументом, передаваемым в качестве username по умолчанию:
(defn greeting "Returns a greeting of the form 'Hello, username.' Default username is 'world'." ([] (greeting "world")) ([username] (str "Hello, " username)))
Вы можете проверить, что новое приветствие работает так, как ожидалось:
(greeting) -> "Hello, world"
Вы можете создать функцию с переменной арностью, добавив амперсанд в список параметров. Clojure свяжет имя после амперсанда с последовательностью всех остальных параметров.
Следующая функция позволит двум людям пойти на свидание с произвольным числом сопровождающих:
(defn date [person-1 person-2 & chaperones] (println person-1 "and" person-2 "went out with" (count chaperones) "chaperones."))
(date "Romeo" "Juliet" "Friar Lawrence" "Nurse") | Romeo and Juliet went out with 2 chaperones.
Произвольная арность очень полезна при рекурсивных определениях. За примерами обратитесь к Главе 4, Функциональное Программирование, на странице 85.
Написание реализации функции с различной арностью — очень полезная вещь. Но, если вы пришли в Clojure после Объектно-ориентированного Программирования, вы захотите полиморфизма, то есть, различные реализации, выбранные по типу. Clojure может сделать это и ещё многое другое. За деталями обратитесь к Главе 8, Мультиметоды, на странице 187 и Главе 6, Протоколы и Типы Данных, на странице 143.
defn предназначен для определения высокоуровневых функций. Если вы хотите создать функцию внутри функции, то лучше всего использовать форму анонимной функции.
Анонимные функции
В дополнение к именованным функциям, созданным с помощью defn, вы, также, можете создать анонимные функции с помощью fn. По крайней мере, есть три причины создания анонимных функций:
- Функция настолько краткая и очевидная, что создание имени для неё только усложнит код и не сделает его легче.
- Функция будет использоваться только внутри другой функции и требует локального имени, без высокоуровневой привязки.
- Функция создана внутри другой функции для работы с некоторыми данными.
Функции фильтрации часто бывают короткими и само очевидными. На пример, представьте что вам нужно создать индекс для последовательности слов и нужно игнорировать слова короче 3 символов. Вы можете написать функцию indexable-word? с примерно таким кодом:
(defn indexable-word? [word] (> (count word) 2))
Далее вы можете использовать indexable-word? для извлечения индексируемых слов из выражения:
(require '[clojure.string :as str]) (filter indexable-word? (str/split "A fine day it is" #"\W+")) -> ("fine" "day")<source> Вызов ''split'' разбивает выражение на слова, затем ''filter'' вызывает ''indexable-word?'' для каждого слова, и возвращает те слова, для которых ''indexable-word?'' вернул истинное значение. Анонимные функции позволяют сделать тоже самое в одну строку. Ниже приведена простейшая анонимная форма ''fn'': <source lang=lisp>(fn [params*] body)
С помощью этой формы вы можете включить реализацию indexable-word? прямо в вызов filter:
(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+")) -> ("fine" "day")
Существует ещё более короткий синтаксис для анонимных функций, в нём используются не явные имена параметров. Параметры именуются как %1, %2 и так далее. Кроме того, возможно использование % для первого параметра. Этот синтаксис выглядит так:
#(body)
Вы можете переписать вызов filter с короткой анонимной формой:
(filter #(> (count %) 2) (str/split "A fine day it is" #"\W+")) -> ("fine" "day")
Ещё одна причина использования анонимных функций — это использование именованной функции только в пределах видимости другой функции. Продолжая работу с примером indexable-word? вы можете написать так:
(defn indexable-words [text] (let [indexable-word? (fn [w] (> (count w) 2))] (filter indexable-word? (str/split text #"\W+"))))
let связывает имя indexable-word? с той же функцией, которую вы писали ранее, но на этот раз эта функция находится внутри лексической области indexable-words. (Рассмотрению let в деталях посвящён Раздел 2.4, Переменные, Привязки и Пространства Имён, на странице 36.) Вы можете убедиться в том, что indexable-words работает так, как надо:
(indexable-words "a fine day it is") -> ("fine" "day")
Комбинация let и анонимной функции говорит читателю кода следующее: «Функция indexable-word? достаточно интересна для того, чтобы иметь имя, но она актуальна только внутри indexable-words.»
Третья причина использования анонимной функции — это динамическое создание функции во времени выполнения. Ранее вы реализовали простую функцию приветствия. Развивая эту идею, вы можете создать функцию make-greeter, которая будет создавать функции приветствия. make-greeter будет получать greeting-prefix и возвращать новую функцию, которая будет собирать приветствия из greeting-prefix и имени.
(defn make-greeter [greeting-prefix] (fn [username] (str greeting-prefix ", " username)))
Не имеет смысла давать функции fn какое-либо имя, поскольку она при каждом вызове make-greeter создаёт другую функцию. Тем не менее, вы можете дать название результатам конкретных вызовов make-greeter. Вы можете использовать def для того, чтобы дать имя функциям, созданным с помощью make-greeter:
(def hello-greeting (make-greeter "Hello")) -> #'user/hello-greeting (def aloha-greeting (make-greeter "Aloha")) -> #'user/aloha-greeting
Теперь вы можете вызвать эту функцию также, как и все другие функции:
(hello-greeting "world") -> "Hello, world" (aloha-greeting "world") -> "Aloha, world"
Более того, нет никакой причины давать имена каждому приветствию. Вы можете просто создать приветствие и поместить его в первый (функция) слот формы:
((make-greeter "Howdy") "pardner") -> "Howdy, pardner"
Как вы видите, различные функции приветствия запоминают значение greeting-prefix заданного в момент создания. Говоря более формально, функции приветствия являются замыканием над значением greeting-prefix.
Когда использовать анонимную функцию
Анонимные функции имеют краткий синтаксис, а это не всегда приемлемо. Вы можете захотеть быть точным и создавать именованные функции, как, например, indexable-word?. Это прекрасное решение и, безусловно, разумный выбор, в случае, когда indexable-word? должен вызываться из более чем одного места.
Использование анонимных функций не обязательно и не является требованием. Используйте анонимные функции только в том случае, когда вы считаете что они делают ваш код более читаемым. К ним нужно привыкнуть, поэтому не удивляйтесь тому, что вы будете использовать их всё больше и больше.
Переменные, Привязки и Пространства Имён
Когда вы определяете объект с помощью def или defn, объект сохраняется в переменную (var) Clojure. К примеру, следующий def создаёт переменную под названием user/foo:
(def foo 10) -> #'user/foo
Символ user/foo относится к переменной, связанной со значением 10. Если вы скажете Clojure вычислить символ foo, он вернёт ассоциированное значение:
foo -> 10
Инициализирующее значение переменной называется корневой привязкой (root binding). Иногда бывает полезно создавать поточно-локальные привязки к переменной, это описано в Разделе 5.5, Управление Состоянием Потока с Переменными, на странице 127.
Вы можете обращаться напрямую к переменной. var — это специальная форма, возвращающая переменную, а не значение переменной:
(var a-symbol)
Вы можете использовать var для возвращения переменной, привязанной к user/foo:
(var foo) -> #'user/foo
Вы практически никогда не будете напрямую работать с формой var в Clojure коде. Взамен этого, вы будете работать с эквивалентным считывающим макросом #', также возвращающим переменную по символу:
#'foo
-> #'user/foo
Зачем надо обращаться напрямую к переменной? В большинстве случаев этот приём не нужен, и вы можете игнорировать различие между символами и переменными.
Но держите в голове, что переменные содержат ещё и другие свойства, чем просто сохранение значения:
- Некоторые переменные могут быть псевдонимами более чем одного пространства имён (Пространства Имён, на странице 40). Так достигается возможность использования удобных коротких имён.
- Переменные могут содержать метаданные (Раздел 2.8, Метаданные, на странице 51). Метаданные переменных включают в себя документацию (Поиск Документации), типы подсказок для оптимизации и юнит-тесты.
- Переменные можно динамически пересвязывать (dynamically rebound) на основе потоков (Раздел 5.5, Управление Состоянием Потока с Переменными, на странице 127).
Привязки
Переменные привязаны к именам, но кроме этих привязок есть и другие. На пример, при вызове функции, значения аргументов привязываются к именам параметров. В следующем вызове, 10 привязывается к имени number внутри функции triple:
(defn triple [number] (* 3 number)) -> #'user/triple (triple 10) -> 30
Привязки параметров функции имеют лексическую область действия: они видимы только внутри текста тела функции. Функции не являются единственным способом создания лексических привязок. Специальная форма let не выполняет ничего, кроме создания набора лексических привязок:
(let [bindings*] exprs*)
Привязки bindings актуальны для выражений exprs, а значение let — это значение последнего выражения в exprs.
Представьте, что вам нужны координаты четырёх углов квадрата, вам даны нижняя координата bottom, левая координата left и площадь size. Вы можете использовать let для верхней top и правой right координат, основываясь на заданных значениях:
(defn square-corners [bottom left size] (let [top (+ bottom size) right (+ left size)] [[bottom left] [top left] [top right] [bottom right]]))
let создаёт привязки top и right. Таким образом, вы избегаете проблемы повторного вычисления top и right. (При генерации возвращаемого значения и то и другое используется дважды.) Далее let возвращает его последнюю форму, которая в этом примере возвращает значения углов квадрата (square-corners).
Деструктуризация
Во многих языках программирования, вы привязываете переменную ко всей коллекции, в то время, когда вам нужно получить доступ только к одной части этой коллекции.
Представьте, что вы работаете с базой данных авторов книг. Вы отслеживаете и имена и фамилии, но некоторым функциям нужно только фамилия:
(defn greet-author-1 [author] (println "Hello," (:first-name author)))
Функция greet-author-1 работает хорошо:
(greet-author-1 {:last-name "Vinge" :first-name "Vernor"}) | Hello, Vernor
Необходимость привязки author неудовлетворительна. Вам не нужен author; вам нужно только first-name. Clojure решает эту проблему с помощью деструктуризации (destructuring). В любом месте, в котором вы привязываете имена, вы можете вложить вектор или отображение коллекции и привязаться только к нужной части. Это вариант greet-author, привязывающегося только к имени:
(defn greet-author-2 [{fname :first-name}] (println "Hello," fname))
Форма привязки {fname: first-name} указывает Clojure привязать fname к аргументу :first-name функции. Поведение greet-author-2 идентично greet-author-1:
(greet-author-2 {:last-name "Vinge" :first-name "Vernor"}) | Hello, Vernor
Также как вы использовали отображение для деструктуризации любых ассоциативных коллекций, вы можете использовать вектор для деструктуризации любых последовательных коллекций. На пример, вы можете привязаться только к первым двум координатам в трёхмерном координатном пространстве:
(let [[x y] [1 2 3]] [x y]) -> [1 2]
Выражение [x y] деструктуризирует вектор [1 2 3], привязывая x к 1 и y к 2. Таким образом, ни один символ не остаётся с финальным элементом 3, этот элемент не привязывается ни к чему.
Иногда бывает необходимо пропустить элементы с начала коллекции. Вот так можно создать привязку к координате z:
(let [[_ _ z] [1 2 3]] z) -> 3
Подчёркивание (_) — это разрешённый символ и его использование обозначает следующее: «Мне нет дела до этой привязки.» Привязывание происходит слева-направо, таким образом символ _ привязывается дважды:
; *not* idiomatic! (let [[_ _ z] [1 2 3]] _) -> 2
Кроме того, возможна одновременная привязка и коллекции и элементов в коллекции. Внутри деструктуризационного выражения, слово :as указывает о привязывании ко всей структуре. На пример, вы можете захватить x и y по отдельности, плюс всю коллекцию как coords, для сообщения о общем числе размерностей:
(let [[x y :as coords] [1 2 3 4 5 6]] (str "x: " x ", y: " y ", total dimensions " (count coords))) -> "x: 1, y: 2, total dimensions 6"
Попробуйте использовать деструктуризацию для создания функции ellipsize. ellipsize должен получать строку и возвращать первые три слова и многоточие (…).
(require '[clojure.string :as str]) (defn ellipsize [words] (let [[w1 w2 w3] (str/split words #"\s+")] (str/join " " [w1 w2 w3 "..."])))
(ellipsize "The quick brown fox jumps over the lazy dog.") -> "The quick brown ..."
split разделяет строку, ориентируясь по пробелам, далее выполняется форма деструктуризация [w1 w2 w3], захватывающая первые три слова. Деструктуризация игнорирует все остальные элементы, кроме тех, которые нам нужны. И наконец, join пересобирает три слова, добавляя многоточие в конце.
Деструктуризация имеет дополнительные свойства, не показанные здесь и сама по себе является мини языком. Игра Змея в Разделе 5.6, Змея Clojure, на странице 132, очень широко использует деструктуризацию. Для полного списка возможностей деструктуризации, читайте онлайн документацию по let.
Пространство Имён (Namespaces)
Корневые привязки живут в пространстве имён. Доказательство этому вы можете увидеть запустив REPL Clojure и создав привязку:
user=> (def foo 10) -> #'user/foo
Приглашение user=> говорит вам о том, что в данный момент вы работаете в пространстве имён user. (В этой книге, в большинстве листингов REPL сессий, для краткости, пропущено приглашение REPL. В этом разделе, приглашение REPL будет включаться в текст листинга тогда, когда в примере необходимо указание текущего пространства имён.) Вы должны относиться к user как к пространству имён, предназначенному для экспериментальной разработки.
Когда Clojure преобразовывает имя foo, оно попадает в текущее пространство имён user. Вы можете проверить это с помощью вызова resolve:
(resolve sym)
resolve возвращает переменную или класс, под которым был преобразован символ в текущем пространстве имени. Используйте resolve для точного преобразования символа foo:
(resolve 'foo) -> #'user/foo
Вы можете переключаться между пространствами имён, создав, при необходимости, новое пространство с помощью in-ns:
(in-ns name)
Попробуйте создать пространство имён myapp:
user=> (in-ns 'myapp) -> #<Namespace myapp> myapp=>
Теперь вы находитесь в пространстве имён myapp, и всё, что вы создадите с помощью def или defn будет принадлежать myapp.
Когда вы создаёте новое пространство имён с помощью in-ns, пакет java.lang автоматически становится доступным для вас:
myapp=> String -> java.lang.String
Пока вы будете изучать Clojure, вы должны использовать use для пространства имён clojure.core, каждый раз, когда вы будете переходить на новое пространство имён, таким образом, делая доступными функции ядра Clojure:
myapp=> (clojure.core/use 'clojure.core) -> nil
По умолчанию, имена классов за пределами java.lang должны определяться полностью. Вы не можете просто сказать File:
myapp=> File/separator -> java.lang.Exception: No such namespace: File
Взамен этого, вы должны полностью указать java.io.File. Помните, что ваш символ разделения файла может отличаться от того, что показано здесь:
myapp=> java.io.File/separator -> "/"
Если вы не хотите использовать полностью определённое имя класса, то необходимо отобразить одно или более имена классов из Java пакета в текущее пространство имён с помощью import.
(import '(package Class+))
После импорта класса вы можете использовать его короткое имя:
(import '(java.io InputStream File)) -> java.io.File (.exists (File. "/tmp")) -> true
import работает только для Java классов. Если вы хотите использовать переменную Clojure из другого пространства имени, то вы должны использовать его полное имя или отобразить его в текущее пространство имени. К примеру, возьмём функцию Clojure split, которая находится в clojure.string:
(require 'clojure.string) (clojure.string/split "Something,separated,by,commas" #",") -> ["Something" "separated" "by" "commas"] (split "Something,separated,by,commas" #",") -> Unable to resolve symbol: split in this context
Для создания псевдонима split в текущем пространстве имён, вызовите require в пространстве имени split и дайте ему псевдоним str:
(require '[clojure.string :as str]) (str/split "Something,separated,by,commas" #",") -> ["Something" "separated" "by" "commas"]
Показанная выше простая форма require создаёт ссылки всех публичных переменных clojure.string в текущем пространстве имени и предоставляет доступ к ним под псевдонимом str. Такой подход может сбить с толку, поскольку он явно не указывает имена с которыми происходит работа.
Применение import к Java классам и require к пространству имён в начале исходного файла с использованием макроса ns является стандартным подходом:
(ns name & references)
Макрос ns устанавливает текущее пространство имени (доступное как *ns*) имени name, при необходимости создавая пространство имени. Ссылки references могут включать :import, :require и :use, которые работают, подобно одноимённым функциям, для отображения пространства имени в единственную форму и располагаются на верху исходного файла. На пример, этот вызов ns располагается на верхней части демонстрационного кода для этой главы:
(ns examples.exploring (:require [clojure.string :as str]) (:import (java.io File)))
Функции Clojure для работы с пространством имён могут делать немного больше того, чем я показал здесь.
Вы можете добавлять или убирать отображение пространства имён в любое время. Чтобы узнать больше, выполните эту команду в REPL. Так как мы произвели некоторые действия в REPL, нам надо убедиться, что мы находимся в пользовательском пространстве имени и утилиты REPL доступны для нас:
(in-ns 'user) (find-doc "ns-")
Альтернативный способ — это просмотреть документацию на http://clojure.org/namespaces.
Вызов Java
Clojure предоставляет простой, прямой синтаксис для вызова Java кода: создание объектов, вызов методов и доступ к статическим методам и полям. Кроме того, Clojure предоставляет возможность использования синтаксического сахара, что позволяет более кратко вызывать Java из Clojure, по сравнению с вызовом Java из Java!
Не все типы в Java созданы одинаково: примитивы и массивы работают по разному. Там где в Java приходится прибегать к специальным уловкам, в Clojure работает так же, как и остальной код. И наконец, Clojure предоставляет набор удобных функций для выполнения общих задач. Наличие этих функций в Java привело бы к её перегрузке.
Доступ к Конструкторам, Методам и Полям
Первый шаг во многих сценариях взаимодействия с Java — это создание Java объекта. Для этих целей Clojure предоставляет специальную форму new:
(new classname)
Попробуем создать новый объект Random:
(new java.util.Random) -> <Random java.util.Random@667cbde6>
REPL просто печатает новый экземпляр Random через вызов метода toString(). Для использования экземпляра Random вы должны сохранить его куда нибудь. А теперь, просто воспользуйтесь def для сохранения Random в переменную Clojure:
(def rnd (new java.util.Random)) -> #'user/rnd
Теперь вы можете вызывать методы rnd с помощью использования специальной формы Clojure точка (.):
(. class-or-instance member-symbol & args) (. class-or-instance (member-symbol & args))
«.» может вызывать методы. На пример, следующий код вызывает без аргументную версию nextInt():
(. rnd nextInt) -> -791474443
Random содержит nextInt(), который принимает аргумент. Вы можете вызвать эту версию просто добавив аргумент к списку:
(. rnd nextInt 10) -> 8
В предыдущем вызове форма . используется для получения доступа к методу экземпляра. Но . работает со всеми видами членов класса: поля как методы и static как экземпляры. Ниже вы можете увидеть . используемую для получения значения pi:
(. Math PI) -> 3.141592653589793
Учтите, что Math не определён полностью. Полного определения не нужно, поскольку Clojure автоматически импортирует java.lang. Чтобы не печатать везде java.util.Random вы можете явно импортировать его:
(import [& import-lists]) ; import-list => (package-symbol & class-name-symbols)
import получает некоторое количество списков, первый элемент которых означает имя пакета а остальная часть имён импортируется из этого пакета. Следующее применение import позволяет получить доступ к Random, Locale и MessageFormat:
(import '(java.util Random Locale) '(java.text MessageFormat)) -> java.text.MessageFormat Random -> java.util.Random Locale -> java.util.Locale MessageFormat -> java.text.MessageFormat
На данный момент вы знаете всё, что нужно для вызова Java из Clojure. Вы можете делать следующие вещи:
- Импортировать имена классов
- Создавать экземпляры
- Получать доступ к полям
- Вызывать методы
Тем не менее, здесь нет никакого удивительного синтаксиса. Это просто «Java с другими скобками.»
Javadoc
Хотя вызов Java из Clojure лёгок, помните, что непосредственная работа с Java может быть сложной. Clojure предоставляет функцию javadoc, которая может значительно облегчить вашу жизнь. Она даст вам удобные подсказки при работе в REPL.
(javadoc java.net.URL) ->
Управление ходом программы
Clojure содержит очень малое количество форм управления ходом программы. В этом разделе вы встретитесь с if, do и loop/recur. Как выяснится дальше, это всё, что вам нужно.
Разветвление с помощью if
Clojure’овский if вычисляет его первый аргумент. Если аргумент является логически истинным, то возвращается результат вычисления его второго аргумента:
(defn is-small? [number] (if (< number 100) "yes")) (is-small? 50) -> "yes"
Если первый аргумент if окажется логической ложью, то будет возвращён nil:
(is-small? 50000) -> nil
Если вы хотите определить результат для «else» части формы if, добавьте третий аргумент:
(defn is-small? [number] (if (< number 100) "yes" "no")) (is-small? 50000) -> "no"
Макросы управления ходом программы when и when-not построены на базе if и описаны в when и when-not, на странице 171.
Введение в Побочные Эффекты с do
Clojure’овский if позволяет работать только с одной формой для каждого ветвления. А как быть в том случае, когда вам нужно выполнять более одной вещи в каждом ветвлении? Например, если вам нужно протоколировать выбор конкретного ветвления. do получает произвольное число форм, вычисляет их всех и возвращает последнюю форму.
Вы можете использовать do для печати протоколируемого выражения, полученного с if:
(defn is-small? [number] (if (< number 100) "yes" (do (println "Saw a big number" number) "no")))
результатом будет следующее:
(is-small? 200) | Saw a big number 200 -> "no"
Это пример побочного эффекта (side effect). println не способствует возвращению значения is-small?. Вместо этого, он выходит за пределы функции во внешний мир и что-то там делает.
Многие языки программирования особым образом смешивают чистые функции и побочные эффекты. Но не Clojure. В Clojure — побочные эффекты являются отдельной и не обычной вещью. do — это одна из возможностей сказать «дальше последует побочный эффект». Поскольку do игнорирует возвращаемые значения всех форм, кроме последнего, то все эти игнорируемые формы должны иметь побочные эффекты, иначе они (формы) будут бесполезны.
Повторения с loop/recur
loop — это Швейцарский Армейский Нож управления ходом программы в Clojure:
(loop [bindings *] exprs*)
Специальная форма loop работает также, как и let, устанавливает привязки bindings и вычисляет выражение exprs. Разница в том, что loop устанавливает точку рекурсии (recursion point), которая может быть изменена с помощью специальной формы recur:
(recur exprs*)
recur привязывает новые значения для привязок loop и возвращает управление в начало цикла. Например, следующий loop/recur возвращает обратный отсчёт:
(loop [result [] x 5] (if (zero? x) result (recur (conj result x) (dec x))))
При первой проходке, loop привязывает result к пустому вектору и x к 5. Так как x не равняется нулю, recur привяжет новые значения для x и result:
- result привяжется к предыдущему result с присоединённым к нему (conj) предыдущим x.
- x привяжется к декременту предыдущего x.
Управление вернётся в начало цикла. x опять не равно нулю, цикл продолжается, накапливается result и декрементируется x. В конце концов x достигает значения нуль и if прекращает выполнение повторения, возвращая result.
Взамен использования loop, вы можете вернуться с помощью recur в начало функции. Это позволяет легко писать функции, в которых всё тело выступает в качестве неявного loop:
(defn countdown [result x] (if (zero? x) result (recur (conj result x) (dec x)))) (countdown [] 5) -> [5 4 3 2 1]
recur — это мощный строительный блок. Но вы не будете его (recur) часто использовать, потому что есть много общих рекурсий предоставляемых библиотекой последовательностей Clojure.
Например, обратный отсчёт может быть выражен следующими способами:
(into [] (take 5 (iterate dec 5))) -> [5 4 3 2 1] (into [] (drop-last (reverse (range 6)))) -> [5 4 3 2 1] (vec (reverse (rest (range 6)))) -> [5 4 3 2 1]
Не думайте, что достаточно знать эти формы — просто знайте что есть много альтернатив прямому использованию recur. Использованные здесь функции из библиотеки последовательности описаны в Разделе 3.2, Использование Библиотеки Последовательности, на странице 60. Clojure не выполняет автоматическую оптимизацию хвостовой рекурсии (tail-call optimization — TCO). Тем не менее, выполняется оптимизация recur вызовов. Глава 4, Функциональное Программирование, на странице 85 описывается оптимизация хвостовой рекурсии, сама рекурсию и производится детальный разбор оптимизации хвостовой рекурсии.
К этому моменту вы уже увидели немало особенностей языка Clojure, но до сих пор не работали с переменными. Некоторые вещи действительно меняются, и в Главе 5, Состояние, на странице 113 показано как Clojure работает с изменяемыми ссылками. Но большинство переменных в традиционных языках не обязательны и просто опасны. Давайте посмотрим как Clojure избавляется от них.
Где Мой Цикл for?
В Clojure нет циклов for и нет прямо изменяемых переменных. (Clojure позволяет работать с непрямыми изменяемыми ссылками, но эти ссылки должны явно вызываться из вашего кода. За деталями обращайтесь к Главе 5, на странице 113.) Так, как же писать код, в котором вы использовали циклы for?
Вместо того, чтобы создавать гипотетический пример, мы решили взять кусок случайного открытого Java кода, найти метод с использованием for циклов и переменных и перенести его в Clojure. Мы открыли широко используемый проект Apache Commons. Мы выбрали класс StringUtils в Commons Lang, предполагая что такой класс будет требовать некоторого понимания в предметной области. Далее мы стали просматривать код и искать метод, который содержит несколько for циклов и локальных переменных и нашли indexOfAny:
data/snippets/StringUtils.java
// From Apache Commons Lang, http://commons.apache.org/lang/ public static int indexOfAny(String str, char[] searchChars) { if (isEmpty(str) || ArrayUtils.isEmpty(searchChars)) { return -1; } for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); for (int j = 0; j < searchChars.length; j++) { if (searchChars[j] == ch) { return i; } } } return -1; }
indexOfAny проходит через str и возвращает индекс первого символа, соответствующий любому символу в searchChars, в случае неудачи возвращается −1.
Ниже представлены несколько примеров результатов из документации indexOfAny:
StringUtils.indexOfAny(null, *) = -1 StringUtils.indexOfAny("", *) = -1 StringUtils.indexOfAny(*, null) = -1 StringUtils.indexOfAny(*, []) = -1 StringUtils.indexOfAny("zzabyycdxx",['z','a']) = 0 StringUtils.indexOfAny("zzabyycdxx",['b','y']) = 3 StringUtils.indexOfAny("aba", ['z']) = -1
Здесь есть три if, два for, три возможных точек возвращения, три изменяемых локальных переменных в indexOfAny и метод состоит из четырнадцати строк, это подсчитано с помощью SLOCCount за авторством Дэвида А. Вилера (David A. Wheeler).
Ну а теперь, давайте создадим, шаг за шагом, index-of-any в Clojure. Если бы нам надо было найти только совпадения, мы могли бы использовать Clojure’овский filter. Но нам нужно найти индекс (index) совпадений. Теперь мы создадим indexed, функцию, которая получает коллекцию и возвращает индексированную коллекцию:
(defn indexed [coll] (map-indexed vector coll))
indexed возвращает последовательность пар вида [idx elt]. Попробуйте проиндексировать строку:
(indexed "abcde") -> ([0 \a] [1 \b] [2 \c] [3 \d] [4 \e])
Далее, нам надо найти индексы для всех символов в строке, соответствующих поисковому набору.
Создадим функцию index-filter, подобную Clojure’овскому filter, но возвращающую индексы взамен соответствий:
(defn index-filter [pred coll] (when pred (for [[idx elt] (indexed coll) :when (pred elt)] idx)))
Clojure’овский for — это не цикл, а охват последовательностей (смотрите Преобразование Последовательностей, на странице 66). Пары индекс/элемент из (indexed coll) привязана к именам idx и elt, но только тогда, когда (pred elt) вычисляется в истинное значение. И наконец, охват даёт значение idx для каждой соответствующей пары.
Clojure устанавливает функции, которые проверяют наличие в наборе. Так вы можете передать набор символов и строку в index-filter и получить индексы для всех символов в строке, которые принадлежат набору. Попробуйте поработать с несколькими различными строками и набором символов:
(index-filter #{\a \b} "abcdbbb") -> (0 1 4 5 6) (index-filter #{\a \b} "xyz") -> ()
На данный момент, мы сделали больше чем планировали. index-filter возвращает индексы для всех совпадений, а нам нужен только первый индекс. Теперь, index-of-any просто получит первый (first) результат из index-filter:
(defn index-of-any [pred coll] (first (index-filter pred coll)))
Проверим правильность работы index-of-any с помощью нескольких различных входных данных:
(index-of-any #{\z \a} "zzabyycdxx") -> 0 (index-of-any #{\b \y} "zzabyycdxx") -> 3
Clojure’овская версия по всем параметрам проще чем императивная версия (смотрите Таблицу 2, Относительная сложность императивной и функциональной реализаций indexOfAny.) Чем объясняется разница?
- Императивная версия indexOfAny должна работать с несколькими особыми случаями: отсутствие строки или пустая строка, отсутствие или пустой набор искомых символов, и отсутствие совпадения. Эти особые случаи добавляют ветвления и точки выхода из метода. При функциональном подходе, большинство этих случаев обрабатывается без написания явного обрабатывающего кода.
- Императивный indexOfAny вводит локальные переменные для прохода по коллекции (и для строки и для набора символов). С помощью таких высокоуровневых функций как map и охват последовательностей for, функциональный index-of-any избегает необходимости использования переменных.
Ненужная сложность растёт как снежный ком. Например, ответвление для специального случая в императивном indexOfAny использует магическое число −1 для обозначения не соответствия. А что если магическое число будет символьной константой? Но о чём бы вы не думали, такой вопрос не возникает в функциональной версии. Будучи коротким и простым, функциональный indexOfAny является гораздо более универсальным:
- indexOfAny ищет строки, в то время, когда index-of-any может искать любую последовательность.
- indexOfAny может сравнивать с набором символов, в то время, когда index-f-any может сравнивать с любым предикатом.
- indexOfAny возвращает первое совпадение, в то время, когда index-filter возвращает все совпадения и может комбинироваться с другими фильтрами.
В качестве примера более общей универсальности функционального index-of-any, вы можете использовать код, который мы только написали, для определения на каком разе выпала третья решка в серии бросков монеты:
(nth (index-filter #{:h} [:t :t :h :t :h :t :t :t :h :h]) 2) -> 8
Таким образом, можно сделать вывод, что index-of-any написанный в функциональном стиле, без циклов или переменных, более прост, содержит меньшее количество ошибок и более универсален чем императивный indexOfAny. (Однако стоит заметить, что вы можете написать функциональный indexOfAny в простом Java, хотя это будет противоречить идеологии языка. Такой подход может подпадать в идеологию языка тогда, когда в язык будут добавлены замыкания. За дополнительной информацией посетите http://functionaljava.org/ .) На более крупных участках кода, эти преимущества становятся более выраженными.
Таблица № 2 — Относительная сложность императивной и функциональной реализаций indexOfAny
Стиль | Строки кода | Ветвления | Выходы/Методы | Переменные |
---|---|---|---|---|
Императивная версия | 14 | 4 | 3 | 0 |
Функциональная версия | 6 | 1 | 1 | 0 |
Метаданные
Статья в Википедии о метаданных начинается со следующего описания метаданных: «данные о данных». Это правильное, но не совсем точное описание. В Clojure метаданные — это данные ортогональные логическому значению объекта. Например, фамилия и имя персоны — это просто старые данные. Факт того, что объект персоны может быть сериализован в XML не выполняет ничего с персоной и является метаданными. Аналогичным образом, факт того, что объект персоны загрязнился и нуждается в очистке в базе данных также является метаданными.
Считыватель Метаданных
Язык Clojure сам использует метаданные в нескольких местах. На пример, переменные имеют метаданные, отображающие содержащуюся документацию, информацию типа и исходную информацию. Здесь показаны метаданные для переменной str:
(meta #'str) -> {:ns #<Namespace clojure.core>, :name str, :file "core.clj", :line 313, :arglists ([] [x] [x & ys]), :tag java.lang.String, :doc "With no args, ... etc."}
Некоторый общие ключи метаданных и их использование показано в Таблице 3, Общие ключи метаданных.
Большинство метаданных для переменной добавляются автоматически компилятором Clojure. Для добавления собственных пар ключ/значение, используйте считывающий макрос метаданных:
^metadata form
Например, вы можете создать простую функцию shout, которая будет переводить строку в верхний регистр, используя ключ :tag:
; более короткая форма расположена ниже (defn ^{:tag String} shout [^{:tag String} s] (.toUpperCase s)) -> #'user/shout
Вы можете проверить метаданные функции shout и увидите, что Clojure добавил :tag:
(meta #'shout) -> {:arglists ([s]), :ns #<Namespace user>, :name shout, :line 32, :file "NO_SOURCE_FILE", :tag java.lang.String}
Вы заполнили :tag, а Clojure заполнил остальные ключи. :file со значением NO_SOURCE_FILE указывает на то, что код был введён в REPL’е.
Поскольку метаданные :tag являются общими, вы можете использовать короткую форму ^Classname, которая будет расширяться до ^{:tag Classname}. Используя короткую форму вы можете переписать shout таким образом:
(defn ^String shout [^String s] (.toUpperCase s)) -> #'user/shout
Если метаданные мешают вам читать определение функции, то вы можете располагать метаданные в конце определения. Используйте вариант defn, который обворачивает одну или более форму в скобки, после которых следует отображение метаданных:
(defn shout ([s] (.toUpperCase s)) {:tag String})
Таблица № 3 — Ключи общих метаданных
Ключ метаданных | Для чего используется |
---|---|
:arglists | Информационный параметр, используемый docом |
:doc | Документация, используемая docом |
:file | Исходный файл |
:line | Номер строки исходника |
:macro | Истинное значение для макросов |
:name | Локальное имя |
:ns | Пространство имени |
:tag | Ожидаемый аргумент или возвращаемый тип |
Заключение
Это была длинная глава. Но подумайте, сколько всего вы узнали: вы можете создавать экземпляры базовых литеральных типов, определять и вызывать функции, управлять пространством имён и читать и записывать метаданные. Вы можете писать чисто функциональный код, но при необходимости, вы можете добавить побочные эффекты. Также вы познакомились с такими концепциями Lisp’а как: считыватель макросов, специальные формы и деструктуризация.
Данный здесь материал занял бы сотни страниц в большинстве других языков. Действительно ли путь Clojure настолько прост? Да, отчасти. Половина заслуги в этом принадлежит Clojure. Элегантный дизайн Clojure и выбор абстракции делают язык простым в изучении.
Тем не менее, в данный момент язык может показаться вам не таким простым в изучении. Дело в том, что пользуясь мощью Clojure мы гораздо быстрее продвигаемся в изучении языка, чем большинство других книг, посвящённых другим языкам программирования.
Другая половина заслуги в изучении этой главы принадлежит вам, читатель. Clojure вернёт вам то, что вы в него вложили и ещё немного сверху. Должно пройти некоторое время, для того, чтобы вы почувствовали себя комфортно при работе с примерами из главы и REPL’ом. Остальная часть книги даст вам возможность для этого.
Глава 3. Объединение Данных с Последовательностями
Программы управляют данными. На самом низком уровне программы работают с такими структурами как строки, списки, векторы, отображения, наборы и деревья. На более высоком уровне такие структуры данных возникают снови и снова. Например:
- XML данные являются деревьями.
- Наборы результатов из Базы Данных можно рассматривать как списки или векторы.
- Иерархии директорий являются деревьями.
- Файлы часто рассматриваются как одна большая строка или вектор строк.
В Clojure, доступ ко всем структурам данных можно получить через единственную абстракцию: последовательность (sequence или seq).
seq (произносится как "seek", в русской транскрипции - "сик") - это логический список. Он называется логическим по причине того, что Clojure не привязывает последовательности к реализации деталей списка так, как это делает Lisp'овская ячейка cons (для того, чтобы узнать историю cons, читайте Происхождение Cons, на 58). seq - это абстракция, которую можно использовать везде.
Те коллекции, которые можно рассматривать как seq называются seq-able (произносится как "SEEK-a-bull" или в русской транскрипции "СИК-э-бел"). В этой главе вы познакомитесь с различными seq-able коллекциями:
- Все Clojure коллекции
- Все Java коллекции
- Java массивы и строки
- Регулярные выражения
- Структуры директорий
- Потоки I/O
- XML деревья
Вы познакомитесь с библиотеками последовательностей, набором функций, которые могут работать с любыми seq-able коллекциями. Поскольку многие вещи являются последовательностями, библиотека последовательностей является более мощной и универсальной нежели коллекция API в большинстве других языках. Библиотека последовательностей включает в себя функции для создания, фильтрации и трансформации данных. Эти функции выступают в роли Коллекции API для Clojure и они заменяют множество циклов, которые вам пришлось бы написать в императивных языках.
В этой главе вы станете опытным пользователем Clojure последовательностей. Вы увидите как использовать универсальный набор очень выразительных функций с невероятно широким диапазоном типов данных. Далее, в следующей главе (Глава 4, Функциональное Программирование, на странице 85), вы изучите функциональный стиль, в котором написана библиотека последовательностей.
Всё Является Последовательностями
Каждую структуру данных в Clojure можно рассматривать как последовательность. Последовательность имеет три основные возможности:
- Вы можете получить первый first элемент в последовательности:
(first aseq)
first вернёт nil в случае когда его аргумент пустой или nil.
- Вы можете получить всё что осталось после первого элемента, другими словами, остаток rest последовательности:
(rest aseq)
rest вернёт пустую последовательность (не nil) если не осталось других элементов.
- Вы можете сконструировать новую последовательность через добавление элемента в начало существующей последовательности. Эта операция выполняется с помощью cons:
(cons elem aseq)
Все эти три возможности находятся в Java интерфейсе clojure.lang.ISeq. (Запомните этот факт, поскольку ISeq часто используется в качестве обозначения последовательности.)
Функция seq вернёт последовательность из любой seq-able коллекции:
(seq coll)
seq вернёт nil в случае когда его коллекция coll будет пустой или будет nil. Функция next вернёт seq элементов, следующих после первого элемента:
(next aseq)
(next aseq) - это эквивалент (seq (rest aseq)). Поведение rest/next представлено в Таблице № 4 - Уточнение поведения rest/next, на странице 57.
Если вы ранее работали с Lisp, то обнаружите что функции для работы с последовательностями работают и со списками:
(first '(1 2 3)) -> 1 (rest '(1 2 3)) -> (2 3) (cons 0 '(1 2 3)) -> (0 1 2 3)
В Clojure эти же функции будут также работать с другими структурами данных. Вы можете рассматривать векторы как последовательности:
(first [1 2 3]) -> 1 (rest [1 2 3]) -> (2 3) (cons 0 [1 2 3]) -> (0 1 2 3)
Когда вы применяете rest или cons к вектору, результат будет последовательностью, а не вектором. В REPL последовательности печатаются как списки, как вы уже увидели в ранее приведённом примере. Вы можете проверить возвращённый тип через получение его класса class:
(class (rest [1 2 3])) -> clojure.lang.PersistentVector$ChunkedSeq
$ChunkedSeq в конце имени класса - это метка Java о вложенных именах класса. Последовательности, которые вы получаете от определённого типа коллекции, часто реализованы как класс ChunkedSeq, вложенный внутрь исходного класса коллекции (в данном примере - это PersistentVector).
В целом последовательности обладают очень мощными свойствами, но иногда вам понадобится создать какую-нибудь особую реализацию типа. Этому посвящён Раздел 3.5, Вызов Структурно-Ориентированных Функций, на странице 76.
Вы можете рассматривать отображения как последовательности, в этом случае пара ключ/значение будет элементом последовательности:
(first {:fname "Aaron" :lname "Bedra"}) -> [:lname "Bedra"] (rest {:fname "Aaron" :lname "Bedra"}) -> ([:fname "Aaron"]) (cons [:mname "James"] {:fname "Aaron" :lname "Bedra"}) -> ([:mname "James"] [:lname "Bedra"] [:fname "Aaron"])
Также вы можете рассматривать наборы как последовательности:
(first #{:the :quick :brown :fox}) -> :brown (rest #{:the :quick :brown :fox}) -> (:quick :fox :the) (cons :jumped #{:the :quick :brown :fox}) -> (:jumped :brown :quick :fox :the)
Отображения и наборы имеют стабильный порядок работы с ними, но этот порядок зависит от деталей реализации и вы не должны полагаться на него. Элементы набора не обязательно будут возвращаться в той последовательности, в какой они были внесены:
#{:the :quick :brown :fox} -> #{:brown :quick :fox :the}
Если вам нужен определённый порядок, вы можете воспользоваться этим:
(sorted-set & elements)
sorted-set отсортирует значения по их естественному порядку:
(sorted-set :the :quick :brown :fox) -> #{:brown :fox :quick :the}
Таким же образом, не обязательно пары ключ/значение в отображениях будут возвращаться в том порядке, в котором вы их вставили:
{:a 1 :b 2 :c 3} -> {:a 1, :c 3, :b 2}
Вы можете создать отсортированное отображение с помощью sorted-map:
(sorted-map & elements)
sorted-maps не будет возвращаться в том порядке, в каком вы его вставили, но он будет возвращён будучи отсортированным по ключи:
(sorted-map :c 3 :b 2 :a 1) -> {:a 1, :b 2, :c 3}
В дополнение к основным свойствам последовательностей, есть ещё два свойства, о которых стоит упомянуть: conj и into.
(conj coll element & elements) (into to-coll from-coll)
conj добавляет один или более элементов в коллекцию, а into добавляет все элементы из одной коллекции в другую. И conj и into добавляет элементы в наиболее подходящее место в структуре данных. Для списков conj и into производит добавление спереди:
(conj '(1 2 3) :a) -> (:a 1 2 3) (into '(1 2 3) '(:a :b :c)) -> (:c :b :a 1 2 3)
Для векторов, conj и into добавляет элементы сзади:
(conj [1 2 3] :a) -> [1 2 3 :a] (into [1 2 3] [:a :b :c]) -> [1 2 3 :a :b :c]
Поскольку conj (и связанные с ним функции) выполняют эффективные действия с переданными им структурами данных, то вы часто можете писать эффективный код, полностью независимый от определённой реализации.
Библиотека последовательности Clojure особенно хорошо работает с большими (и часто бесконечными) последовательностями. Большинство Clojure последовательностей являются ленивыми: они создают элементы только тогда, когда они нужны. Таким образом, функции могут работать с последовательностями не помещающимися в памяти.
Последовательности Clojure являются иммутабельными: они никогда не меняются. Это облегчает программы и означает что последовательности Clojure безопасны для одновременного доступа. Однако, это создаёт маленькие проблемы для человеческого языка. Описания на английском языке изменяемых вещей получаются более гладкими. Рассмотрим следующие два описания для гипотетической функции triple, которая работает с последовательностями:
- triple утраивает каждый элемент последовательности.
- triple получает последовательность и возвращает новую последовательность, с утроением каждого элемента исходной последовательности.
Последняя версия конкретная и точная. Предыдущая версия более лёгкая для чтения, но может привести к ошибочному представлению о том, что функция triple изменяет последовательность. Не обманитесь: последовательность никогда не меняется. Если вы увидели фразу "foo изменяет x", то следует читать: "foo возвращает изменённую копию x".
Таблица № 4 - Уточнение поведения rest/next
Форма | Результат |
---|---|
(rest ()) | () |
(next ()) | nil |
(seq (rest ())) | nil |
Происхождение Cons
Последовательности Clojure - это абстракции, основанные на базе списков из языка Lisp. В оригинальной реализации Lisp есть три фундаментальные операции над списком, они называются car, cdr и cons. car и cdr - это акронимы, произошедшие из деталей реализации Lisp на платформе IBM 704. Многие реализации Lisp, включая Clojure, заменяют эти эзотерические имена более осмысленными названиями first и rest.
Третья функция, cons - это сокращение от construct (конструкция). Lisp программисты используют cons как существительное, глагол и прилагательное. Вы же будете использовать cons для создания структуры данных под названием ячейка cons (cons cell) или, для краткости, просто cons.
Большинство реализаций Lisp, включая Clojure, сохраняют оригинальное название cons, поскольку "construct" ("конструкция") - это удачное мнемоническое обозначение того, что выполняет cons. Кроме того, это название напоминает вам что списки являются не изменяемыми. Для удобства вы можете говорить что cons добавляет элемент к последовательности, но более верно будет сказать что cons конструирует новую последовательность, которая похожа на исходную последовательность, но отличие заключается в одном добавленном элементе.
Вопрос Джо: Почему Функции Для Работы с Векторами Возвращают Списки?
Почему при работе с примерами в REPL, результаты rest и cons становятся списками, даже если были введены векторы, отображения или наборы? Означает-ли это что внутренние механизмы Clojure конвертируют всё в списки? Нет! Функции для работы с последовательностями всегда возвращают последовательности вне зависимости от их ввода. Вы можете убедиться в этом проверив Java тип возвращённых объектов:
(class '(1 2 3)) -> clojure.lang.PersistentList (class (rest [1 2 3])) -> clojure.lang.PersistentVector$ChunkedSeq
Здесь вы можете видеть, что результат (rest [1 2 3]) является разновидностью Последовательности, а не Списком. Итак, почему результат становится списком?
Ответ находится в REPL. Когда вы запрашиваете REPL отобразить последовательность, все знают что мы имеем дело с последовательностью. Он не знает какой вид коллекции последовательности был построен. Таким образом, REPL печатает все последовательности одинаковым путём: он проходит по всей последовательности и печатает его в виде списка.
Использование Библиотеки Последовательностей
Библиотека последовательностей Clojure предоставляет богатый набор функциональности, работающий с любыми последовательностями. Если вы пришли из объектно-ориентированного мира, в котором правят существительные, то библиотека последовательностей будет настоящей "Местью Глаголов". (Стив Эгги (Steve Yegge) в своей статье "Выполнение в Королевстве Существительных" (“Execution in the Kingdom of Nouns”) (http://tinyurl.com/the-kingdom-of-nouns) утверждает о том, что объектно-ориентированное программирование продвинуло существительные в не реалистично доминирующее положение и что настало время изменить ситуацию.) Функции обеспечивают богатую основу функциональности, которая позволяет получить всю выгоду от структур данных, подчиняющихся базовым first/rest/cons.
Следующие функции сгруппированы в четыре обширные категории:
- Функции, создающие последовательности
- Функции, фильтрующие последовательности
- Предикаты последовательностей
- Функции, преобразующие последовательности
Это разделение является несколько произвольным. Поскольку последовательности неизменны, большинство функций для работы с последовательностями создают новые последовательности. Некоторые функции для работы с последовательностями одновременно являются фильтрующими и преобразующими. Тем не менее, это разделение позволяет ориентироваться в этой большой библиотеке.
Создание Последовательностей
В дополнение к последовательностям литералов Clojure предоставляет ряд функций, создающих последовательности. range создаёт последовательность в диапазоне от start до end, увеличивая каждый следующий элемент на step.
(range start? end step?)
В диапазон входит начало start, но не входит конец end. Если вы не определили их, start, по умолчанию, устанавливается в ноль, а step - в 1. Попробуйте создать несколько диапазонов в REPL:
(range 10) -> (0 1 2 3 4 5 6 7 8 9) (range 10 20) -> (10 11 12 13 14 15 16 17 18 19) (range 1 25 2) -> (1 3 5 7 9 11 13 15 17 19 21 23)
Функция repeat повторяет элемент x n раз:
(repeat n x)
Попробуйте повторить несколько элементов в REPL:
(repeat 5 1) -> (1 1 1 1 1) (repeat 10 "x") -> ("x" "x" "x" "x" "x" "x" "x" "x" "x" "x")
Обе функции range и repeat представляют собой идеи, которые можно бесконечно расширять. iterate вы можете рассматривать как бесконечное расширение range:
(iterate f x)
iterate начинается со значения x и продолжается бесконечно, применяя функцию f для вычисления каждого следующего значения.
Если вы начали с 1 и применяете iterate с функцией ins, то вы сможете генерировать целые числа:
(take 10 (iterate inc 1)) -> (1 2 3 4 5 6 7 8 9 10)
Поскольку последовательность является бесконечной, то вам понадобится другая новая функция для просмотра последовательности с REPL.
(take n sequence)
take возвращает ленивую последовательность из первых n элементов из коллекции и предоставляет один способ создания конечного вида из бесконечной коллекции.
Целые числа очень удобная последовательность, которая может пригодится где угодно, поэтому давайте создадим функцию с помощью defn для будущего использования:
(defn whole-numbers [] (iterate inc 1)) -> #'user/whole-numbers
Будучи вызванным с единственным аргументом, repeat возвращает ленивую, бесконечную последовательность:
(repeat x)
Попробуйте повторить с помощью repeat несколько элементов в REPL. Не забудьте обернуть результат с помощью take:
(take 20 (repeat 1)) -> (1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)
Функция cycle получает коллекцию и бесконечно её повторяет:
(cycle coll)
Попробуйте зациклить некоторую коллекцию в REPL:
(take 10 (cycle (range 3))) -> (0 1 2 0 1 2 0 1 2 0)
Функция interleave получает несколько коллекций и создаёт коллекцию, в которой чередуются значения из других коллекций, до тех пор, пока не исчерпается какая-либо из коллекций.
(interleave & colls)
Когда одна из коллекций исчерпается, функция interleave остановит свою работу. Таким образом, вы можете смешивать конечные и бесконечные коллекции:
(interleave (whole-numbers) ["A" "B" "C" "D" "E"]) -> (1 "A" 2 "B" 3 "C" 4 "D" 5 "E")
С функцией interleave очень тесно связана функция interpose. Эта функция получает последовательность и разделитель, а на выходе создаёт последовательность, в которой элементы разделены разделителем:
(interpose separator coll)
Вы можете использовать interpose для создания разделённых строк:
(interpose "," ["apples" "bananas" "grapes"]) -> ("apples" "," "bananas" "," "grapes")
Для создания строк удобно сочетать interpose с (apply str ...):
(apply str (interpose \, ["apples" "bananas" "grapes"])) -> "apples,bananas,grapes"
Идиома (apply str ...) настолько широко распространена, что для неё в Clojure создана обвёртка clojure.string/join:
(join separator sequence)
Воспользуйтесь clojure.string/join для разделения запятыми слов в списке:
(use '[clojure.string :only (join)]) (join \, ["apples" "bananas" "grapes"]) -> "apples,bananas,grapes"
Для каждого типа коллекции в Clojure есть функция, получающая произвольное число аргументов и создающая коллекцию с этим типом:
(list & elements) (vector & elements) (hash-set & elements) (hash-map key-1 val-1 ...)
hash-set - это двоюродный брат set, но hash-set работает немного по-другому: set ожидает коллекцию в виде его первого аргумента:
(set [1 2 3]) -> #{1 2 3}
hash-set получает список с переменным числом аргументов:
(hash-set 1 2 3) -> #{1 2 3}
vector также имеет двоюродного брата, vec, который получает единственную коллекцию, в виде аргумента вместо списка с произвольным числом аргументов:
(vec (range 3)) -> [0 1 2]
Теперь, когда вы узнали базовые способы создания последовательностей, вы можете переходить к функциям Clojure, предназначенным для фильтрации и трансформации последовательностей.
Фильтрация Последовательностей
Clojure содержит несколько функций, предназначенных для фильтрации последовательностей, эти функции фильтруют последовательность и возвращают последовательность, оставшуюся после фильтрации. Самая базовая из них - это filter:
(filter pred coll)
filter получает предикат и коллекцию, а возвращает последовательность объектов, для которых фильтр вернул истинное значение (при интерпретации в логическом контексте). Вы можете фильтровать whole-numbers из предыдущего раздела для получения чётных и нечётных чисел:
(take 10 (filter even? (whole-numbers))) -> (2 4 6 8 10 12 14 16 18 20) (take 10 (filter odd? (whole-numbers))) -> (1 3 5 7 9 11 13 15 17 19)
Функция take-while фильтрует последовательность до тех пор, пока предикат будет истинным:
(take-while pred coll)
Например, для получения всех символов в строке до первой гласной используйте следующий код:
(take-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") -> (\t \h)
В этом коде происходит несколько интересных вещей:
- Наборы работают как функции. Так, вы можете читать #{\a\e\i\o\u} как "набор гласных" или "функция, которая проверяет является ли её аргумент гласной буквой."
- complement переворачивает поведение другой функции. Предыдущая дополненная функция (complement #{\a\e\i\o\u}) проверяет, не является-ли её аргумент гласной.
Противоположностью take-while является функция drop-while:
(drop-while pred coll)
drop-while отбрасывает элементы с начала последовательности, пока предикат принимает истинное значение и возвращает оставшиеся элементы. Вы можете использовать drop-while для отбрасывания первых не-гласных символов строки:
(drop-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") -> (\e \- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)
split-at и split-with разделяет коллекцию на две коллекции:
(split-at index coll) (split-with pred coll)
split-at получает индекс, а split-with получает предикат:
(split-at 5 (range 10)) ->[(0 1 2 3 4) (5 6 7 8 9)] (split-with #(<= % 10) (range 0 20 2)) ->[(0 2 4 6 8 10) (12 14 16 18)]
И конечно, все эти take-, split- и drop- функции возвращают ленивые последовательности.
Преобразования Последовательностей
Функции преобразования трансформируют значения в последовательности. Самая простое преобразование - это map:
(map f coll)
map получает исходную коллекцию coll и функцию f и возвращает новую последовательность через вызов f для каждого элемента в коллекции coll. Вы можете использовать map для обёртки каждого элемента в коллекции HTML тегом.
(map #(format "<p>%s</p>" %) ["the" "quick" "brown" "fox"]) -> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")
В качестве аргументов map может получать более чем одну коллекцию. f должен быть функцией, поддерживающей несколько аргументов. map будет вызывать f с одним аргументом из каждой коллекции, и будет останавливаться тогда, когда исчерпается самая маленькая из коллекций:
(map #(format "<%s>%s</%s>" %1 %2 %1) ["h1" "h2" "h3" "h1"] ["the" "quick" "brown" "fox"]) -> ("<h1>the</h1>" "<h2>quick</h2>" "<h3>brown</h3>" "<h1>fox</h1>")
Другое преобразование общего вида - это reduce:
(reduce f coll)
f - это функция, поддерживающая два аргумента. reduce применяет f к первым двум элементам в коллекции coll, затем применяет f к результату и третьему элементу из коллекции и так далее. reduce - это удобная функция для, в некотором роде, "подведения итогов" по последовательности. Вы можете использовать reduce для сложения элементов:
(reduce + (range 1 11)) -> 55
или для их перемножения:
(reduce * (range 1 11)) -> 3628800
Упорядочить коллекцию вы можете с помощью sort или sort-by:
(sort comp? coll) (sort-by a-fn comp? coll)
sort сортирует элементы в естественном порядке, в то время как sort-by сортирует последовательность по результатам вызова a-fn для каждого элемента:
(sort [42 1 7 11]) -> (1 7 11 42) (sort-by #(.toString %) [42 1 7 11]) -> (1 11 42 7)
Если вам не нужна сортировка в естественном порядке, вы можете определить произвольную функцию сравнения comp для sort или sort-by:
(sort > [42 1 7 11]) -> (42 11 7 1) (sort-by :grade > [{:grade 83} {:grade 90} {:grade 77}]) -> ({:grade 90} {:grade 83} {:grade 77})
Дедушкой всех фильтров и преобразований является охват списка (list comprehension). Охват списка - это создание списка на основе существующего списка и некого набора правил. Другими словами, состояния охвата - это свойства, которым должен удовлетворять результирующий список. В целом, охват списка состоит из следующих пунктов:
- Ввод списка(ов)
- Заполнитель переменных (Понятие "переменные" следует понимать в математическом смысле, а не с точки зрения императивного программирования. Эти "переменные" вы не можете изменять. Я смиренно прощения за такую перегрузку в Английском языке.) для элементов во вводимых списках
- Предикаты элементов
- Форма вывода, которая создаёт, удовлетворяющий предикатам, вывод из элементов введённого списка
Конечно, Clojure обобщает понятие охвата списков в охват последовательности. Охват Clojure использует макрос for. (for, используемый в охвате списков, не имеет ничего общего с for, который используется в циклах императивных языков программирования.)
(for [binding-form coll-expr filter-expr? ...] expr)
for получает вектор из связанной формы/коллекции binding-form/coll-exprs, плюс необязательное фильтрующее выражение filter-expr и возвращает последовательность выражений exprs.
Охват списка это более общее понятие чем такие функции как map и filter, и фактически может эмулировать большинство вышеописанных фильтрующих и преобразующих функций.
Вы можете переписать предыдущий map пример в виде охвата списка:
(for [word ["the" "quick" "brown" "fox"]] (format "<p>%s</p>" word)) -> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")
Читается почти как Английский язык: "For [each] word in [a sequence of words] format [according to format instructions]." ("Для [каждого] слова в [последовательности слов] отформатировать [в соответствии с инструкциями форматирования].")
Охват может эмулировать filter с помощью использования пункта :when. Вы можете передать even? пункту :when для фильтрации чётных чисел:
(take 10 (for [n (whole-numbers) :when (even? n)] n)) -> (2 4 6 8 10 12 14 16 18 20)
Пункт :while продолжает вычисление только тогда, когда его выражение сохраняет значение истинное значение:
(for [n (whole-numbers) :while (even? n)] n) -> ()
Настоящая мощь for проявляется тогда, когда вам нужно работать с более чем одним связанным выражением. Например, вы можете выразить все возможные позиции на шахматной доске в алгебраической нотации, через связывание rank и file:
(for [file "ABCDEFGH" rank (range 1 9)] (format "%c%d" file rank)) -> ("A1" "A2" ... elided ... "H7 ""H8")
Clojure начинает итерацию с правого выражения и двигается налево. Поскольку в форме привязки rank находится правее чем file, то итерация начинается с rank. Если вы хотите чтобы первой происходила итерация files, то вы должны изменить порядок привязки и разместить rank первым в списке:
(for [rank (range 1 9) file "ABCDEFGH"] (format "%c%d" file rank)) -> ("A1" "B1" ... elided ... "G8" "H8")
В многих языках преобразования, фильтры и охваты отрабатывают сразу. Не думайте что такой подход существует и в Clojure. Большинство функций ничего не делают со своими элементами до тех пор, пока вы не будете их использовать.
Ленивые и Бесконечные Последовательности
Большинство Clojure последовательностей являются ленивыми; другими словами, элементы не вычисляются до тех пор, пока они не станут нужны. Использование ленивых последовательностей имеет много плюсов:
- Вы можете отложить сложные вычисления, которые могут не пригодится.
- Вы можете работать с огромными наборами данных, которые не помещаются в память.
- Вы можете приостановить Ввод/Вывод до тех пор, пока он не понадобятся.
Рассмотрим код и следующее выражение:
(ns examples.primes) ;; Взято из clojure.contrib.lazy-seqs ; primes нельзя реализовать также эффективно, как функцию, поскольку ; требуется учёт всей последовательности. отличие между ; fibs и powers-of-2 в том, что требуется фиксированный буфер из 1 или 2 ; предыдущих значение. (def primes (concat [2 3 5 7] (lazy-seq (let [primes-from (fn primes-from [n [f & r]] (if (some #(zero? (rem n %)) (take-while #(<= (* % %) n) primes)) (recur (+ n f) r) (lazy-seq (cons n (primes-from (+ n f) r))))) wheel (cycle [2 4 2 4 6 2 6 4 2 4 6 6 2 6 4 2 6 4 6 8 4 2 4 2 4 8 6 4 6 2 4 6 2 6 6 4 2 4 6 2 6 4 2 4 2 10 2 10])] (primes-from 11 wheel)))))
</source>
(use 'examples.primes) (def ordinals-and-primes (map vector (iterate inc 1) primes)) -> #'user/ordinals-and-primes
ordinals-and-primes включает пары, подобные [5, 11] (одиннадцать - это пятое простое число). И простые и составные числа являются бесконечными, но ordinals-and-primes отлично помещается в памяти, по той причине, что она является ленивой. Просто возьмите то, что вам нужно с помощью take:
(take 5 (drop 1000 ordinals-and-primes)) -> ([1001 7927] [1002 7933] [1003 7937] [1004 7949] [1005 7951])
Когда следует использовать ленивые последовательности? Почти всегда. Большинство функций для работы с последовательностями возвращают ленивые последовательности, таким образом, вы будете платить только за то, что вы будете использовать. Что более важно, ленивые последовательности не требуют от вас особых усилий. В предыдущем примере, iterate, primes и map возвращают ленивые последовательности, таким образом, ordinals-and-primes получает ленивую последовательность "бесплатно".
Ленивые последовательности очень важны для функционального программирования в Clojure. Более подробно вопрос создания и работы с ленивыми последовательностями рассматривается в Разделе 4.2, Как Быть Ленивым, на странице 90.
Принудительное Вычисление Последовательностей
При просмотре больших последовательностей в REPL, вы можете использовать take для предотвращения вычисления всей последовательности. В другом случае, вы можете столкнуться с противоположной проблемой. Вы создали ленивую последовательность и хотите чтобы она полностью вычислялась. Обычно такая проблема возникает при генерации кода последовательности с побочными эффектами. Рассмотрим следующую последовательность, которая включает в себя побочный эффект, реализованный с помощью println:
(def x (for [i (range 1 3)] (do (println i) i))) -> #'user/x
Новички в Clojure будут удивлены тем, что предыдущий код ничего не напечатает. Поскольку определение x не использует никакие элементы, Clojure не будет вычислять другие формы для их получения. Вы можете применить принудительное вычисление с помощью doall:
(doall coll)
doall заставляет Clojure пройтись по всем элементам последовательности и вернуть элементы в виде результата:
(doall x) | 1 | 2 -> (1 2)
Также вы можете использовать dorun:
(dorun coll)
dorun проходит по всем элементам последовательности без сохранения последних элементов в памяти. В результате, dorun может проходить по последовательностям, не помещающимся в памяти.
(def x (for [i (range 1 3)] (do (println i) i))) -> #'user/x (dorun x) | 1 | 2 -> nil
Значение nil напоминает нам о том, что dorun не сохраняет ссылку ко всей последовательности. Функции dorun и doall помогут вам работать с побочными эффектами, в то время как остальные функции препятствуют возникновению побочных эффектов. Эти функции (dorun и doall) надо использовать как можно меньше.
Clojure Позволяет Применить Последовательности к Java
Абстракции последовательностей first/rest применимы ко всему тому, количество которого больше единицы. К этому в мире Java можно включить следующее:
- Коллекция API
- Регулярные выражения
- Структура файловой системы
- Обработка XML
- Результаты реляционных баз данных
Clojure заворачивает эти API Java, позволяя применить библиотеку последовательностей практически ко всему.
Последовательности и Java Коллекции
Если вы попытаетесь применить функции для работы с последовательностями к Java коллекциям, вы увидите, что они ведут себя как последовательности. Коллекции, которые могут выступать как последовательности, называются seq-able (последовательно-образные). Например, массивы являются последовательно-образными:
; String.getBytes возвращает массив байтов (first (.getBytes "hello")) -> 104 (rest (.getBytes "hello")) -> (101 108 108 111) (cons (int \h) (.getBytes "ello")) -> (104 101 108 108 111)
Хэш-таблицы и Отображения также последовательно-образные:
; System.getProperties возвращает Хэш-таблицу (first (System/getProperties)) -> #<Entry java.runtime.name=Java(TM) SE Runtime Environment> (rest (System/getProperties)) -> (#<Entry sun.boot.library.path=/System/Library/... etc. ...
Учтите, что обёртки последовательности - не изменчивы, в то время, когда нижележащая Java коллекция изменяема. Таким образом, вы не сможете обновить системные свойства нового элемента в (System/getProperties) с помощью cons. cons вернёт новую последовательность; существующие свойства останутся без изменений.
Поскольку строки - это последовательности символов, они также являются последовательно-образными:
(first "Hello") -> \H (rest "Hello") -> (\e \l \l \o) (cons \H "ello") -> (\H \e \l \l \o)
Clojure автоматически заворачивает коллекции в последовательности, но не поддерживает автоматического распаковывания в исходный тип. С большинством типов коллекции это поведение является интуитивным, но со строками вам придётся вручную конвертировать результат в строку. Рассмотрим переворачивание строки. В Clojure содержится функция reverse:
; возможно, это не то, что вам нужно (reverse "hello") -> (\o \l \l \e \h)
Для конвертирования последовательности в строку, используйте (apply str seq):
(apply str (reverse "hello")) -> "olleh"
Несмотря на то, что коллекции Java являются последовательно-образными, они не дают преимуществ перед встроенными коллекциями Clojure. Java коллекции следует предпочесть только в том случае, когда вам придётся работать с устаревшими Java API.
Последовательности и Регулярные Выражения
Под капотом регулярных выражений Clojure находится библиотека java.util.regex. На более низком уровне это представлено изменчивой природой Java Matcher. Вы можете использовать re-matcher для создания Matcher регулярных выражений и строк, а затем посредством loop и re-find пройтись по всем соответствиям.
(re-matcher regexp string)
; не делайте этого! (let [m (re-matcher #"\w+" "the quick brown fox")] (loop [match (re-find m)] (when match (println match) (recur (re-find m)))))
| the | quick | brown | fox -> nil
Более лучшим решением будет использование высоко-уровневого re-seq.
(re-seq regexp string)
re-seq предоставляет неизменяемую последовательность над совпадениями. Так вы можете использовать мощь всех функций последовательностей Clojure. Наберите эти выражения в REPL:
(re-seq #"\w+" "the quick brown fox") -> ("the" "quick" "brown" "fox") (sort (re-seq #"\w+" "the quick brown fox")) -> ("brown" "fox" "quick" "the") (drop 2 (re-seq #"\w+" "the quick brown fox")) -> ("brown" "fox") (map #(.toUpperCase %) (re-seq #"\w+" "the quick brown fox")) -> ("THE" "QUICK" "BROWN" "FOX")
re-seq - это отличный пример того, как хорошая абстракция уменьшает количество кода. Совпадения регулярных выражений - это не особый вид вещей, требующих специальных методов работы с ними. Они такие-же последовательности, что и все остальные. Благодаря большому числу функций для работы с последовательностями вы, без лишних затрат, получаете большую функциональность, чем в функциях, специально написанных для регулярных выражений.
Последовательности и Файловая Система
Вы можете использовать последовательности и с файловой системой. Для начинающих: вы можете прямо вызывать java.io.File:
(import '(java.io File)) (.listFiles (File. ".")) -> [Ljava.io.File;@1f70f15e
[Ljava.io.File... - это toString() представление Java'ой массива файлов Files. Функции для работы с последовательностями могут автоматически вызывать seq, но этого не может выполнять REPL.
Теперь примените seq:
(seq (.listFiles (File. ".")) ) -> (#<./concurrency> #<./sequences> ...)
Если формат вывода по-умолчанию не подходит для вас, вы можете использовать map для вывода в строковой форме с getName:
; с излишком (map #(.getName %) (seq (.listFiles (File. ".")))) -> ("concurrency" "sequences" ...)
Если вы решили использовать такие функции как map, то вызов seq является лишним. Библиотека последовательностей сама вызывает seq для вас, поэтому нет надобности в ручном вызове. Предыдущий код упрощается до следующего:
(map #(.getName %) (.listFiles (File. "."))) -> ("concurrency" "sequences" ...)
Часто бывает нужным рекурсивно пройти по всему дереву. Clojure предоставляет рекурсивный проход с помощью file-seq. Если вы примените file-seq для директории, содержащей исходный код этой книги, вы увидите что-то подобное следующему:
(count (file-seq (File. "."))) -> 104 ; ваше число будет большим чем это!
А если вам понадобится узнать какие файлы были недавно изменены? Написать предикат recently-modified?, проверяющий какие файлы были изменены за последние пол-часа:
(defn minutes-to-millis [mins] (* mins 1000 60)) (defn recently-modified? [file] (> (.lastModified file) (- (System/currentTimeMillis) (minutes-to-millis 30))))
Попробуйте выполнить следующее (Ваш результат будет отличаться от данного здесь.):
(filter recently-modified? (file-seq (File. "."))) -> (./sequences ./sequences/sequences.clj)
Последовательности и Потоки
Вы можете использовать seq к строкам любого Java Reader-а, использующего line-seq. Для получения Reader, вы можете использовать библиотеку Clojure clojure.java.io. Библиотека clojure.java.io предоставляет функцию reader, возвращающую считыватель для потоков, файлов, URL и URI.
(use '[clojure.java.io :only (reader)]) ; оставляем считыватель открытым... (take 2 (line-seq (reader "src/examples/utils.clj"))) -> ("(ns examples.utils" " (:import [java.io BufferedReader InputStreamReader]))")
Поскольку считыватели могут представлять ресурсы, не находящиеся в памяти, то их необходимо закрывать, вы можете обернуть создание считывателя функцией with-open. Создайте выражение, использующее функцию последовательности count, для подсчёта числа строк в файле и использует with-open для корректного закрытия считывателя:
(with-open [rdr (reader "src/examples/utils.clj")] (count (line-seq rdr))) -> 64
Для того, чтобы пример был более полезным, добавьте фильтр для подсчёта не-пустых строк:
(with-open [rdr (reader "src/examples/utils.clj")] (count (filter #(re-find #"\S" %) (line-seq rdr)))) -> 55
Используя последовательности для файловой системы и для содержимого отдельных файлов, вы можете легко создавать интересные утилиты. Создайте программу, которая определяет следующие три предиката:
- non-blank? определяет не-пустые строки.
- non-svn? определяет файлы, которые не являются метаданными Subversion.
- clojure-source? определяет файлы исходного коды Clojure.
Затем, создадите функцию clojure-loc, которая будет подсчитывать количество строк в коде Clojure, находящемуся в дереве директорий используя комбинацию таких функций для работы с последовательностями, как: reduce, for, count и filter.
(use '[clojure.java.io :only (reader)]) (defn non-blank? [line] (if (re-find #"\S" line) true false)) (defn non-svn? [file] (not (.contains (.toString file) ".svn"))) (defn clojure-source? [file] (.endsWith (.toString file) ".clj")) (defn clojure-loc [base-file] (reduce + (for [file (file-seq base-file) :when (and (clojure-source? file) (non-svn? file))] (with-open [rdr (reader file)] (count (filter non-blank? (line-seq rdr)))))))
Теперь, давайте попробуем найти с помощью clojure-loc сколько Clojure кода содержит сам Clojure:
(clojure-loc (java.io.File. "/home/abedra/src/opensource/clojure/clojure")) -> 38716
Функция clojure-loc является очень задачно-ориентированной, но, поскольку она построена с помощью функций для работы с последовательностями и простыми предикатами, вы можете легко адаптировать её под любые другие задачи.
Последовательности и XML
В Clojure можно применять seq к XML данным. Примеры использования XML:
data/sequences/compositions.xml
<compositions> <composition composer="J. S. Bach"> <name>The Art of the Fugue</name> </composition> <composition composer="F. Chopin"> <name>Fantaisie-Impromptu Op. 66</name> </composition> <composition composer="W. A. Mozart"> <name>Requiem</name> </composition> </compositions>
Функция clojure.xml/parse разбирает XML файл/поток/URI и возвращает дерево данных в виде Clojure отображения, с вложенными векторами для потомков:
(use '[clojure.xml :only (parse)]) (parse (java.io.File. "data/sequences/compositions.xml")) -> {:tag :compositions, :attrs nil, :content [{:tag :composition, ... etc. ...
Вы можете манипулировать этим отображением как прямо, так и с помощью функции xml-seq для просмотра дерева в виде последовательности:
(xml-seq root)
Следующий пример использует охват списка для xml-seq с извлечением одних только композиторов:
(for [x (xml-seq (parse (java.io.File. "data/sequences/compositions.xml"))) :when (= :composition (:tag x))] (:composer (:attrs x))) -> ("J. S. Bach" "F. Chopin" "W. A. Mozart")
Вызов Структурно-Ориентированных Функций
Функции Clojure для работы с последовательностями дают вам возможность писать очень универсальный код. Иногда вам будет нужно писать более конкретный код (предназначенный для более специфичных целей) для получения возможностей от конкретных структур данных. Clojure включает в себя функции, специально предназначенные для работы со списками, векторами, отображениями, структурами и наборами. Далее у нас последует краткий обзор некоторых структурно-ориентированных функций. Полный список структурно-ориентированных функций Clojure представлен в разделе Структуры Данных (Data Structures) на web-сайте Clojure.
Функции и Списки
Clojure поддерживает традиционные имена peek - для получения первого элемента и pop - для оставшихся элементов:
(peek coll) (pop coll)
Применим peek и pop к простому списку:
(peek '(1 2 3)) -> 1 (pop '(1 2 3)) -> (2 3)
peek подобен first, но pop не похож на rest. При пустой последовательности pop вызовет исключение:
(rest ()) -> () (pop ()) -> java.lang.IllegalStateException: Can't pop empty list
Функции и Векторы
Векторы поддерживают peek и pop, но в этом случае работа с элементами происходит с конца:
(peek [1 2 3]) -> 3 (pop [1 2 3]) -> [1 2]
get возвращает значение по заданному индексу или возвращает nil в случае, когда индекс находится за пределами вектора:
(get [:a :b :c] 1) -> :b (get [:a :b :c] 5) -> nil
Векторы сами по-себе являются функциями. Они берут индекс в роли аргумента и возвращают значение или выбрасывает исключение в случае выхода индекса за пределы:
([:a :b :c] 1) -> :b ([:a :b :c] 5) -> java.lang.ArrayIndexOutOfBoundsException: 5
assoc ассоциирует новое значение с соответствующим индексом:
(assoc [0 1 2 3 4] 2 :two) -> [0 1 :two 3 4]
subvec возвращает субвектор вектора:
(subvec avec start end?)
Если end не определён, то по умолчанию принимается конец вектора:
(subvec [1 2 3 4 5] 3) -> [4 5] (subvec [1 2 3 4 5] 1 3) -> [2 3]
Конечно, вы можете имитировать subvec с помощью комбинации drop и take:
(take 2 (drop 1 [1 2 3 4 5])) -> (2 3)
Разница в том, что take и drop являются универсальными функциями и работают с любыми последовательностями. С другой стороны subvec гораздо быстрее работает с векторами. Несмотря на то, что структурно-ориентированная функция, подобная subvec дублирует функциональность уже доступную в библиотеке последовательности, она предназначена для повышения производительности. Документация для таких функций, как subvec включает в себя характеристики производительности.
Функции и Отображения
Clojure предоставляет несколько функций для чтения ключей и значений из отображения. keys возвращает ключи и vals возвращает последовательность значений:
(keys map) (vals map)
Попробуйте применить keys и vals к простому отображению:
(keys {:sundance "spaniel", :darwin "beagle"}) -> (:sundance :darwin) (vals {:sundance "spaniel", :darwin "beagle"}) -> ("spaniel" "beagle")
get возвращает значение для ключа или возвращает nil.
(get map key value-if-not-found?)
Используйте REPL для проверки поведения get для существующих и отсутствующих ключей:
(get {:sundance "spaniel", :darwin "beagle"} :darwin) -> "beagle" (get {:sundance "spaniel", :darwin "beagle"} :snoopy) -> nil
Есть некоторые трюки, которые облегчают использование get. Отображения - это функции для их ключей. Таким образом вы можете обойтись без get, вставив отображение в начало формы в роли функции:
({:sundance "spaniel", :darwin "beagle"} :darwin) -> "beagle" ({:sundance "spaniel", :darwin "beagle"} :snoopy) -> nil
Ключевые слова ведут себя подобным образом. Они получают коллекцию в виде аргумента и ищут себя в этой коллекции. Поскольку :darwin и :sundance - это ключевые слова, предыдущие формы могут быть записаны с помощью их же элементов в обратном порядке.
(:darwin {:sundance "spaniel", :darwin "beagle"} ) -> "beagle" (:snoopy {:sundance "spaniel", :darwin "beagle"} ) -> nil
Если вы ищете ключ в отображении и в ответ получили nil, вы не можете сказать действительно-ли отсутствует данный ключ в данном отображении или он существует в данном отображении, но со значением nil. Функция contains? решает эту проблему и проверяет наличие указанного ключа.
(contains? map key)
Создайте отображение, в котором nil является допустимым значением:
(def score {:stu nil :joey 100})
:stu присутствует, но если вы увидите значение nil, то можете подумать что его там нет:
(:stu score) -> nil
Если вы используете contains?, то вы увидите что :stu участвует в игре, хотя по внешним признакам его нет:
(contains? score :stu) -> true
Другой подход - это вызвать get, передать ему необязательный третий аргумент, который будет возвращён в случае отсутствия ключа:
(get score :stu :score-not-found) -> nil (get score :aaron :score-not-found) -> :score-not-found
Значение возвращаемое по-умолчанию :score-not-found позволяет определить что :aaron не содержится в определении, в то время когда присутствует :stu со значением nil.
Если nil - допустимое значение в отображении, используйте contains? или форму третьего-аргумента для проверки наличия ключа.
Clojure предоставляет несколько функций для создания новых отображений:
- assoc возвращает отображение с добавленной парой ключ/значение.
- dissoc возвращает отображение без ключа.
- select-keys возвращает отображение, содержащее только переданные ключи.
- merge комбинирует отображения. Если несколько отображений содержат одинаковый ключ, то победит правое отображение.
Для проверки этих функций, создайте некоторые данные по песням:
(def song {:name "Agnus Dei" :artist "Krzysztof Penderecki" :album "Polish Requiem" :genre "Classical"})
Далее, создадим несколько модифицированных версий коллекции информации по песням:
(assoc song :kind "MPEG Audio File") -> {:name "Agnus Dei", :album "Polish Requiem", :kind "MPEG Audio File", :genre "Classical", :artist "Krzysztof Penderecki"} (dissoc song :genre) -> {:name "Agnus Dei", :album "Polish Requiem", :artist "Krzysztof Penderecki"} (select-keys song [:name :artist]) -> {:name "Agnus Dei", :artist "Krzysztof Penderecki"} (merge song {:size 8118166, :time 507245}) -> {:name "Agnus Dei", :album "Polish Requiem", :genre "Classical", :size 8118166, :artist "Krzysztof Penderecki", :time 507245}
Помните, что song никогда не изменится. Каждая из показанных функций возвращает новую коллекцию.
Наиболее интересная функция, применяемая для конструкции отображений - это merge-with.
(merge-with merge-fn & maps)
merge-with похож на merge, за тем исключением, когда два или более отображения имеют одинаковый ключ, вы можете определить свою собственную функцию комбинирования значений для ключа. Используйте merge-with и concat для создания последовательности по каждому ключу:
(merge-with concat {:rubble ["Barney"], :flintstone ["Fred"]} {:rubble ["Betty"], :flintstone ["Wilma"]} {:rubble ["Bam-Bam"], :flintstone ["Pebbles"]}) -> {:rubble ("Barney" "Betty" "Bam-Bam"), :flintstone ("Fred" "Wilma" "Pebbles")}
Начиная с трёх различных коллекций из членов семьи с ключом по фамилии, предыдущий код комбинирует их в одну коллекцию с ключом по фамилии.
Функции и Наборы
В дополнение к функциям для работы с наборами в пространстве clojure, Clojure предоставляет группу функций в пространстве clojure.set. Для использования этих функций с неограниченными именами, вызовите (use 'clojure.set) из REPL. Для следующих примеров, вам понадобятся следующие переменные:
(def languages #{"java" "c" "d" "clojure"}) (def beverages #{"java" "chai" "pop"})
Первая группа clojure.set функций выполняет операции из теории множеств:
- union возвращает набор из всех элементов, присутствующих в любом входном наборе.
- intersection возвращает набор из всех элементов, присутствующих в обоих входных наборах.
- difference возвращает набор из всех элементов, присутствующих в первом входном наборе, без элементов, присутствующих во втором.
- select возвращает набор для всех элементов, соответствующих предикату.
Напишем выражение, объединяющее все языки и напитки:
(union languages beverages) -> #{"java" "c" "d" "clojure" "chai" "pop"}
Далее, попробуем найти языки, не входящие в напитки:
(difference languages beverages) -> #{"c" "d" "clojure"}
Если вам нравится ужасная игра слов, то в таком случае, вам понравится тот факт, что некоторые вещи входят и в языки и в напитки:
(intersection languages beverages) -> #{"java"}
Некоторое количество языков имеет имена не превышающие одного символа:
(select #(= 1 (.length %)) languages) -> #{"c" "d"}
Наборы union и difference - часть теории множеств, но кроме того, они часть реляционной алгебры, являющейся базой для таких языков запросов, как SQL. Реляционная алгебра состоит из шести простейших операторов: набор union и набор difference (описанные ранее), плюс rename, selection, projection и cross product.
Вы можете понять реляционные примитивы с помощью следующей аналогии с реляционными базами данных (смотрите в следующей таблице).
Реляционная алгебра | База Данных | Тип Системы Clojure |
---|---|---|
Соотношение | Таблица | Всё, что похоже на set |
Кортеж | Ряд | Всё, что похоже на map |
Следующие примеры работают с Базой Данных, находящейся в памяти и содержащей сведения о музыкальных композиторах. Перед работой загрузите базу данных:
(def compositions #{{:name "The Art of the Fugue" :composer "J. S. Bach"} {:name "Musical Offering" :composer "J. S. Bach"} {:name "Requiem" :composer "Giuseppe Verdi"} {:name "Requiem" :composer "W. A. Mozart"}}) (def composers #{{:composer "J. S. Bach" :country "Germany"} {:composer "W. A. Mozart" :country "Austria"} {:composer "Giuseppe Verdi" :country "Italy"}}) (def nations #{{:nation "Germany" :language "German"} {:nation "Austria" :language "German"} {:nation "Italy" :language "Italian"}})
Функция rename переименовывает ключи (колонки базы данных), преобразовывая в отображении оригинальные имена в новые имена.
(rename relation rename-map)
Переименуем в compositions ключ name в ключ title:
(rename compositions {:name :title}) -> #{{:title "Requiem", :composer "Giuseppe Verdi"} {:title "Musical Offering", :composer "J.S. Bach"} {:title "Requiem", :composer "W. A. Mozart"} {:title "The Art of the Fugue", :composer "J.S. Bach"}}
Функция select возвращает отображения, для которых предикат вернул истинное значение и является аналогом WHERE в SQL SELECT:
(select pred relation)
Напишем выражение с помощью select, находящее все композиции, заголовок которых равен "Requiem":
(select #(= (:name %) "Requiem") compositions) -> #{{:name "Requiem", :composer "W. A. Mozart"} {:name "Requiem", :composer "Giuseppe Verdi"}}
Функция project вернёт только часть отображений, соответствующей набору ключей.
(project compositions [:name]) -> #{{:name "Musical Offering"} {:name "Requiem"} {:name "The Art of the Fugue"}}
Последний реляционный примитив - cross product (векторное произведение) - основа для различных видов присоединения в реляционных базах данных. Cross product возвращает все возможные комбинации строк в различных таблицах. Сделать такую операцию в Clojure достаточно легко с помощью охвата списков:
(for [m compositions c composers] (concat m c)) -> ... 4 x 3 = 12 rows ...
Несмотря на то, что cross product теоретически интересная вещь, обычно бывает нужно получить лишь некоторое подмножество от векторного произведения. Например, вам нужно объединить (join) множество на основе общих ключей:
(join relation-1 relation-2 keymap?)
Вы можете присоединить имена композиций и композиторов через join с помощью общего ключа :composer:
(join compositions composers) -> #{{:name "Requiem", :country "Austria", :composer "W. A. Mozart"} {:name "Musical Offering", :country "Germany", :composer "J. S. Bach"} {:name "Requiem", :country "Italy", :composer "Giuseppe Verdi"} {:name "The Art of the Fugue", :country "Germany", :composer "J. S. Bach"}}
Если ключевые имена в двух отношениях не совпадают, то вы можете передать keymap, отображающий ключевые имена в relation-1, соответствующий ключам в relation-2. Например, вы можете объединить composers, использующие :country, с nations, использующие :nation. Например:
(join composers nations {:country :nation}) -> #{{:language "German", :nation "Austria", :composer "W. A. Mozart", :country "Austria"} {:language "German", :nation "Germany", :composer "J. S. Bach", :country "Germany"} {:language "Italian", :nation "Italy", :composer "Giuseppe Verdi", :country "Italy"}}
Вы можете комбинировать реляционные примитивы. Возможно вам понадобится узнать множество всех стран, являющиеся домом композиторов реквиема. Вы можете воспользоваться select-ом для нахождения всех реквиемов, применить к ним join и отсеять результаты project-ом только по названиям стран:
(project (join (select #(= (:name %) "Requiem") compositions) composers) [:country]) -> #{{:country "Italy"} {:country "Austria"}}
Аналогия между реляционной алгеброй Clojure и реляционными базами данных весьма поучительна. Однако учтите, что реляционная алгебра Clojure является универсальным инструментом. Вы можете использовать его для любого множества с реляционными данными. При его использовании, в ваших руках будет вся мощь Clojure и Java.
Заключение
Clojure объединяет все виды коллекций под единственной абстракцией - последовательности. После десятилетия доминирования объектно-ориентированного программирования, библиотека Clojure для работы с последовательностью представляет из себя "Месть Глаголов."
Последовательности Clojure реализованы с использованием техники функционального программирования: неизменяемые данные, рекурсивные определения и реализация ленивости. В следующей главе вы увидите как непосредственно использовать эту технику, что позволяет ещё больше увеличить мощь Clojure.
Глава 4. Функциональное Программирование
Функциональное программирование (FP) - это большая тема, и её невозможно изучить за двадцать один день и тем более охватить в единственной главе книги. Тем не менее, вы, довольно быстро, можете достичь первого уровня эффективности через использование ленивой и рекурсивной техник Clojure и на этом мы закончим эту главу.
Вот как мы это сделаем:
- В Разделе 4.1, Концепции Функционального Программирования, на странице 85, вы кратко ознакомитесь с терминами и концепциями ФП. Кроме того в этой вы познакомитесь с "Шестью Правилами Функционального Программирования в Clojure" на эти правила мы будем ссылаться на протяжении всей книги.
- В Разделе 4.2, Как Быть Ленивым, на странице 90, вы познаете мощь ленивых последовательностей. Вы создадите несколько реализаций чисел Фибоначчи, начиная с самого плохого подхода и постепенно улучшая его и в конечном счёте придёте к элегантному, ленивому решению.
- Вы будете мало работать с такими крутыми вещами как ленивые последовательности. В Разделе 4.3, Ленивее Ленивого, на странице 98, мы переосмыслим проблемы и решим их с помощью библиотеки последовательности, описанной в Главе 3, Объединение Данных с Последовательностями.
- И в Разделе 4.4, Возвращаясь к Рекурсии, на странице 103, мы рассмотрим несколько более сложных проблем. Некоторым программистам никогда не понадобится этот материал. И если вы новичок в ФП, то не будет ничего страшного, если вы пропустите этот раздел.
Концепции Функционального Программирования
Функциональное программирование помогает создавать код, который легко писать, читать, тестировать и применять. Ниже описано как это работает.