Перевод книги "Programming Crystal" (2019), IVO BALBAERT, SIMON ST. LAURENT.
___________
Аннотация:
Что НЕ переведено:
[Предисловие], [Часть 4. ПРИЛОЖЕНИЯ (Appendices).], Исходный код (листинги), Системные сообщения (почти все они от компилятора), Веб-адреса (разумеется), Интервью (Companys Story) в конце каждой главы.
Что переведено:
Все теоретические главы (9), Соглашения из оригинальной книги (и её название), некоторые сообщения о неполадках.
& Warning: Авторские права на материал, извлеченный из руководства "Programming Crystal (2019)", включая исходный код, изображения и т.п., сохраняются за правообладателем. Равным образом русскоязычный перевод (или полная переводная версия цифровой книги-оригинала) остаётся интеллектуальной собственностью компании Pragmatic Bookshelf.
---
Книгу-первоисточник (если таковая обнаружится в папке вместе с этим файлом) рекомендую использовать для изучения примеров кода, ещё в Приложениях должны быть ответы к практическим заданиям.
---
Изучающим программирование рекомендую приобрести книгу Алгоритмы: вводный курс, Томас Х. Кормен.
=============
Программирование на ЯП Кристалл.
Создание высокоэффективных и безопасных приложений на языке Crystal.
---------------------------
ОГЛАВЛЕНИЕ:
ЧАСТЬ 1. Начало.
Глава 1. Погружение в Crystal.
Глава 2. Фундаментальные основы.
ЧАСТЬ 2. Стандартные блоки Crystal.
Глава 3. Типизация переменных и управление ходом исполнения.
Глава 4. Организация кода в методах и процедурах.
Глава 5. Использование Классов и Структур.
Глава 6. Работая с Модулями.
Глава 7. Управление проектами.
ЧАСТЬ 3. Продвинутый Crystal-кодинг.
Глава 8. Передовые возможности Кристалла.
Глава 9. Использование веб-фреймворков и Шардов.
_________
ЧАСТЬ 4: Appendices.
----------------------
Соглашения, принятые в этой книге:
"# =>" Вывод или Результат исполнения отрывка программного кода. Пример:
p 2 + 2 # => 4
Для краткой записи кода мы иногда пропускаем операторы "p", "puts" и "print" в этой книге:
2 + 2 # => 4
Однако, вам не рекомендуется пренебрегать "p", "puts" или "print", чтобы в разнообразных IDE и текстовых редакторах инициировать вывод данных для пользователя.
"Error: message" ошибка, обнаруженная во время компилирования;
"Runtime error: message" ошибка, полученная во время выполнения программы.
Если возникает нужда в использовании интерфейса командной строки, может появиться символ $, предваряющий текстовое сообщение. Например, $ crystal build hello_world.cr ;
Условные обозначения из ЯП Ruby также используются в Crystal.
Символ # означает комментарий.
В составных выражениях (вроде Shape#perim) символ # означает: метод "perim" в экземпляре Shape.
Сокращения, применяемые переводчиком:
eBook исходная англоязычная книга Programming Crystal (english book).
Кристалл ЯП Crystal, так как меня достало частое повторение несклоняемого Crystal в русскоязычных предложениях.
ИКС интерфейс командной строки; ставлю там, где нужно набрать команду, вроде [$ ruby error1.cr].
КС ключевое слово; но (во избежание путаницы) я буду использовать близкий по смыслу термин "команда".
т.е. то есть (иначе говоря, другими словами);
т.н. так называемый;
см. смотри;
ПО программное обеспечение, софт.
ОП оперативная память.
ЯП язык программирования.
ЧАСТЬ 1. Начало.
Глава 1. Погружение в Crystal.
Crystal желает быть "шустрым как язык C и гламурным как Ruby". Crystal это скорее эволюционный этап, чем новый вектор развития, нацеленный на производительность и чистые структуры. ЯП Crystal, как и Ruby, предлагает прочный объектно-ориентированный фундамент с функциональными приправами, и призван отвечать запросам основной массы разработчиков ПО.
_ЯП для людей и компьютеров_
Человек по имени Ary Borenszweig создал Crystal в 2011 году, и к нему вскоре присоединились Хуан Ваджнерман, Брайан Кардифф и вся команда из аргентинской компании Manas Technology Solutions. В настоящее время на GitHub также ведется полномасштабная работа с открытым исходным кодом, где задействовано примерно 250 участников. На момент написания этой КНИГИ "Кристалл" находился на стадии версии 0.27.0. Настойчивые усилия по достижению производственной версии 1.0 принесли свои плоды, и довольно мощное сообщество уже поддерживает рост популярности Crystal. Компилируемый язык, похожий на Ruby, звучит привлекательно, не так ли?
Программировать на более низком уровне в Crystal гораздо проще, чем в Ruby, и к тому же код Crystal выполняется намного быстрее. Поэтому его название содержит букву "С" это отсылка к прародителю. Помимо сильных корней Ruby, Crystal также ищет вдохновение у других современных языков, таких как Rust, Go, C#, Python, Julia, Elixir, Erlang, и Swift. Он сочетает лучшие черты своих предшественников таким образом, как ни один другой ЯП до него.
Crystal объединяет синтаксис и многие идиомы из Ruby с такими вещами как:
Статическая система типов, в которой типы определяются в основном автоматически.
Автоматизированный сбор мусора, который делает использование памяти безопасным.
Компиляция в машинный код посредством конвейера LLVM для скорости, с упором на низкий объем потребляемой памяти.
Макросистема оценки времени компиляции, обеспечивающая значительную гибкость динамического подхода Ruby, но без ограничения производительности.
Исключительная объектная ориентация: всё в Crystal является объектом по сути.
Поддержка групповых параметров, а также перегружаемых методов и операторов.
Масштабируемость и параллелизм, реализованные в совместной, простой и легкой для понимания базисной модели, называемой Fibers (Нити, волокна). Они представляют собой коммуникационные последовательные процессы (иначе говоря пути сообщения); подобная архитектура под названием CSP используется в ЯП Go.
Crystal стремится быть кросс-платформенным, поддерживая Linux, macOS и FreeBSD как для x86/64 (64-бит), так и для архитектуры x86 (32-бит). Он также имеет поддержку ARMv6/ARMv7. Лучшие члены команды активно работают над портом для Windows. Crystal также является самодостаточным (в смысле реализации): его компилятор написан на Crystal, что позволяет увидеть этот ЯП в действии. Название Кристалл говорит само за себя: больше прозрачности. Все, что вам нужно знать, чтобы внести свой вклад в развитие языка, это сам язык.
_Похож на Ruby, но гораздо быстрее_
Безошибочно узнаваемый аромат Ruby веет от элегантного, читаемого и менее многословного кода.
Пример Hello world"(см. hello_world.cr) на Кристалле будет кратким:
puts "Hello, Crystal World!" # => Hello, Crystal World!
Этот код выводит строку на стандартный вывод. Если вы хотите, чтобы он был еще короче, используйте 'p' для печати любого объекта:
p "Hello, Crystal World!" # => "Hello, Crystal World!"
(Если вы хотите следовать этому примеру, смотрите в Приложении eBook раздел Установка Crystal на ваш компьютер). Вам не нужно помещать свой код в класс или начинать с функции main(). Это делается просто, в одну строку!
Crystal является идеальным дополнением к Ruby: Кристалл приносит гораздо большую производительность в тех местах, где Ruby в этом особенно нуждается, в то время как Ruby может продолжать играть свою очень динамичную роль в других частях приложения. Чтобы сравнить производительность языков, давайте сравним программу, которая является совершенно одинаковой как для Crystal, так и для Ruby, и запустим ее в обеих языковых реализациях. Число Фибоначчи это сумма двух его предшественников, за исключением варианта с нулем и единицей, где оно возвращает само себя.
Эта программа вычисляет сумму ряда чисел Фибоначчи с помощью рекурсивного алгоритма, и является корректной для Ruby и Crystal одновременно. Программа вычисляет числа Фибоначчи от 1 до 42 и складывает их в переменную сумму, которая будет показана в конце. Фактический расчет происходит внутри метода Фибоначчи.
__why_crystal/fibonacci.cr__
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end sum = 0
(1..42).each do |i|
sum += fib(i)
end
puts sum # => 701408732
Программа называется fibonacci.cr, где '.cr' в качестве расширения файла с исходным кодом Crystal. (Ruby не возражает.) Глядя на программный код fibonacci.cr, вы увидите "знакомые черты" Ruby: переменные без указания типа, знакомое определение метода, и также итератор ".each" сверх диапазона значений.
Мы приведем время его выполнения на одной машине: Ubuntu 16.04, 64-битная ОС, процессор AMD A8-6419 с 12 ГБ оперативной памяти.
Код не оптимизирован как следует, но поскольку тот же самый код запускается в двух разных языках, это вполне справедливое сравнение. Код не очень хорошо настроен, но потому, что тот же самый код работает в разных языках, это справедливое базовое сравнение. Давайте посмотрим, как это сработает в Ruby:
$ time ruby fibonacci.cr
real 3 m 44.437s
user 3 m 43.848s
sys 0 m 0.048s
К счастью я как раз вышел за кофе. Теперь результат Кристалла на достигнутом этапе развития:
$ time crystal fibonacci.cr
real 0m12.149s
user 0m12.044s
sys 0m0.356s
Crystal выполняет ту же задачу за 12 секунд, включая время сборки (компиляции), производительность улучшена в 18,5 раза.
Если бы вы собирались использовать эту программу в дальнейшей работе, вы могли бы собрать одну версию:
$ crystal build --release fibonacci.cr
Будет сгенерирован исполняемый файл fibonacci, и посмотрите на результат его запуска:
$ time ./fibonacci
real 0m10.642s
user 0m10.636s
sys 0m0.000s
Вы заметите дополнительное улучшение, дающее повышение скорости в 21 раз над ЯП Ruby!
Crystal компилируется в нативный код, но это не препятствует быстрой разработке ПО: можно запустить проект (даже если он содержит зависимости от других библиотек) так же просто, как здесь в ИКС:
$ crystal project.cr
Это похоже на режим интерпретатора. Тем не менее, исходный код был полностью скомпилирован во временный файл, содержащий нативный код, а затем выполнен. Запомните: Crystal не имеет интерпретатора или какой-то виртуальной машины. Итак, каковы же тогда языковые различия между Ruby и Кристаллом, которые объясняют этот огромный разрыв в производительности?
Программы Кристалла компилируются в исполняемый код, и для того, чтобы сделать это, компилятору необходимо знать типы всех выражений, содержащихся в исходном коде.
Но благодаря тому, что это умный компилятор, который вычисляет большинство типов самостоятельно через алгоритм вывода, компилятор редко требует от вас при написании кода уточнений посредством указания типов. Тем не менее, весьма желательно четко пояснять ваш код использованием типов, чтобы ваши намерения были ясны и людям, читающим его, и компилятору.
_Ускорение работы в Web-пространстве_
Веб-фреймворки были оплотом для динамических языков, таких как Ruby, Python и PHP.
А что предлагает Crystal?
Стандартный веб-сервер библиотеки Crystal сам по себе очень производителен. В тесте, сравнивающем веб-серверы, реализованные в Node.js, Nim, Rust и Scala, HTTP-сервер Кристалла обработал больше запросов в секунду, чем любой другой сервер. Тесты приходят и уходят, но Crystal стабильно выдает хороший результат, несмотря на то, что пока его производительность ограничена использованием лишь одного процессорного ядра.
А как же более полноценные веб-фреймворки? Одна подобная платформа для Crystal стоит особняком, это Kemal.
Эта небольшая и гибкая веб-инфраструктура поддерживает интерфейс RESTful, очень похожий
на Синатру в ЯП Ruby. Более полное рассмотрение в Главе 9 (параграф "Создание веб-приложений с помощью
фреймворков Kemal и Amber").
Kemal и Sinatra сравнивались с помощью wrk (современного инструмента эталонного тестирования для HTTP-приложений в режиме с использованием одного ядра) и веб-варианта типичной программы "Hello-world".
Вот исходный код на Crystal:
__why_crystal/kemal.cr__
require "kemal"
get "/" do
"Hello from Kemal!"
end
Kemal.run
Серверы, построенные на двух этих фреймворках, были протестированы со 100 подключенными хостами в течение 30 секунд:
$ wrk -c 100 -d 30 http://localhost:3000
В результате Kemal оказался быстрее примерно на 88-93%.
Другой аналогичный тест показал, что Kemal обрабатывает до 64986 запросов в секунду со средним временем отклика 170 микросекунд на запрос, в то время как Sinatra отвечал на 2274 запросов в секунду со средним временем 43.82 миллисекунд. Оба теста показывают, что Kemal обрабатывает примерно в 28 раз больше запросов в секунду, чем Sinatra. (Это из выступления Сердара Дограйола на Конференции Poly Conf 2016: "Кемаль: Создание молниеносных и быстрых веб-приложений с легкостью".)
_Связь с базами данных_
Crystal предлагается в комплекте с библиотекой баз данных для нормальной работы с SQLite, MySQL и PostgreSQL. Стефан Вилле сравнил производительность клиентской библиотеки, используемой в Redis, для разных ЯП, совместимых с Redis в потоковом (конвейерном) режиме.
Это означает, что клиент ставит в очередь всю серию (1 000 000 шт.) запросов, посылает их в одной большой партии, а затем получает все ответы в другой, что минимизирует влияние операционной системы и фокусирует измерения на конкретных трудозатратах, обусловленных клиентской библиотекой и языком программирования.
Результаты, представленные как количество команд, обрабатываемых в секунду, достаточно убедительны.
В сравнении c Ruby, Node.js, Java, io.js, Go и C, Crystal легко обходит конкурентов, более чем вдвое опережает С, и хоть как-то соперничать с ним может только Go.
Вышеприведенные факты свидетельствуют: нет ничего удивительного в том, что Crystal может быть эффективным в разных областях применения: для веб-серверов, трудоемких вычислительных задач бэкэнд-серверов, в утилитах командной строки, микросервисах, работе с базами данных и даже в играх. Кроме скорости, какие еще преимущества приносит инструментарий Кристалла?
_Больше безопасности с помощью Типов_
Crystal обеспечивает больше, чем одну лишь производительность. Компилятор Crystal также предоставляет преимущества статической проверки типов, предотвращающей множество ошибок "времени выполнения" (runtime errors). Следующий фрагмент, который синтаксически корректен и для Crystal, и для Ruby, показывает, как это работает. Он вызывает метод "add", который, в свою очередь, сам вызывает несколько значений:
__why_crystal/error1.cr__
def add(x, y)
x + y
end
puts add(2, 3) # => 5
#() могут быть отброшены при вызове метода
puts add(1.0, 3.14) # => 4.14
puts add("Hello ", "Crystal") # => "Hello Crystal"
#+ связывает две последовательности (т.е. строки)
Вы знаете, что операция конкатенации (+) допустима для целых чисел, плавающих чисел и строк, но что будет, если попробовать:
puts add(42, " times")
Для запуска этого в Ruby используйте ИКС: $ ruby error1.cr.
Программа выводит значения 5, 4, 14 и "Hello Crystal", а затем рапортует об ошибке во время выполнения программы:
'+': String can't be coerced into Fixnum (TypeError).
Ruby интерпретирует код при его запуске, и методы просматриваются во время выполнения. Ruby не найдет проблему до тех пор, пока она не всплывет в программе.
Чтобы запустить тот же код в Crystal, используйте
$ crystal error1.cr.
Кристалл компилирует программу целиком; это отдельный этап перед запуском на выполнение. Поскольку Crystal обнаруживает ошибку во время этого процесса, он останавливается во время компиляции и программа не запускается:
Error in error1.cr:8: instantiating 'add(Int32, String)'
add(42, " times")
^~~
in error1.cr:2: no overload matches 'Int32#+' with type String
В Crystal отсутствует метод добавления строки к целому числу. Компилятор знает это и останавливается. Потому код не доходит до стадии исполнения. Теперь рассмотрим подобную ситуацию, снова на примере кода с синтаксисом, который подходит и Crystal, и Ruby:
__why_crystal/error2.cr__
str = "Crystal"
ix = str.index('z')
puts str[ix]
Здесь выполняется поиск местоположения символа "z" в строке str, содержащей "Crystal"; переменная ix должна сохранить порядковый номер искомого символа. Но что если символ не присутствует в строке, как в данном случае? Как в Ruby, так и в Crystal, ix будет равен нулю, сигнализируя, что ничего не найдено.
Ruby выдает ошибку, если совпадений не обнаружено:
$ ruby error2.cr
error2.cr:3:in `[]': no implicit conversion from nil to integer (TypeError)
from error2.cr:3:in `main'
Это происходит во время исполнения программы; и желательно, чтобы во время тестирования, а не когда вы пользователь, запустивший программу.
Crystal, тем не менее, выдаёт ошибку во время компиляции:
$ crystal error2.cr
Error in error2.cr:3: no overload matches 'String#[]' with type (Int32 | Nil)
Ошибка содержит гораздо больше информации, но это ключевая строка (нет совпадений для 'String#[]' с таким типом значения). Компилятор улавливает вероятную ошибку даже раньше фазы тестирования, не говоря уже о стадии компилирования. Сообщение об ошибке также указывает на возникшую проблему гораздо вернее, чем это делается в Ruby: метод puts, применяемый к String, не будет работать с нулевым аргументом.
Другими словами: поиск элемента в позиции ix внутри 'str' завершается неудачно, когда 'ix' равно Nil.
Вы можете исправить эту ситуацию (а также и в Ruby), тестируя ix таким образом:
__why_crystal/error2.cr__
if ix
puts str[ix]
end
Выражение if в Кристалле может принимать значение true и любые другие значения, исключая false и "ноль" (а также нулевые указатели). Итак, в условном ветвлении if Crystal знает, что ix не может быть нулевым, и ошибки компиляции больше нет.
Кроме того, Crystal позволяет избежать ошибок компиляции пока длится поддержание методов, принимающих многообразие различных типов.
Превосходя ограничения синтаксиса Ruby, Crystal позволяет создавать различные методы, которые имеют одинаковое имя, но используют различные типы аргументов; такие методы зовутся перегружаемыми.
_Комплект поставки Кристалла_
Crystal поставляется с неким инструментальным оснащением, сформированным с целью помочь вам быстро начать работу над проектами.
Поскольку компилятор достаточно умный, чтобы отловить многие ошибки, Crystal имеет множество фактически уже написанных входных тестов, которые приходится писать самостоятельно, когда программируешь на динамических языках. Тем не менее, стандартная библиотека Crystal содержит модуль Spec, поддерживающий модульное тестирование и Поведенчески-Ориентированную Разработку (аббревиатура "BDD"); методику, где программная инженерия отталкивается от всевозможных линий поведения.
Упомянутый модуль даже передает вид и ощущение от RSpec, известного BDD-фреймворка в Ruby.
Вот обзор того, что включено:
Инструменты создания проектов с готовыми шаблонами кода и с проверкой спецификаций.
Управление зависимостями, которое обеспечивает легкое автоматизированное включение внешних пакетов (Называются "shards": сленговое словечко для Crystal, и библиотека коллективного пользования).
Форматировщик кода (формат), чтобы весь код был стандартизирован и легко опознаваем, избегая
азартных обсуждений формата кода.
Генератор документации (docs), использующий формат Markdown.
Элегантная локальная веб-среда для работы с кодом, называемая Crystal Playground, содержащая инструменты для просмотра кода (включая функцию визуального выделения контекста и типов).
Стандартная библиотека поддерживает современные потребности, например, синтаксический анализ JSON, Crypto, HTTP-клиенты и серверы, Web-сокеты, OAuth2 и драйверы баз данных. Это дополняется целой экосистемой из более чем 2200 shards (это библиотеки или пакеты Crystal), изготовленных отзывчивым сообществом. Развернуть приложение Кристалла тоже проще простого: только скопируйте исполняемый файл в целевую систему. Нет необходимости в установке заранее определенного времени прогона программы и нет проблем с зависимостями кода Crystal.
_Как приспособить Crystal для оптимального использования_
Crystal привносит C-подобную производительность в программирование общего назначения, в зоны актуальности Ruby, Python, Java или C#.
Вот некоторые из ключевых областей, где можно применить Crystal:
Быстрые веб-приложения, которые подобны своим аналогам, созданным средствами Ruby.
Быстрые расширения Ruby.
Обслуживание бэкэнд-систем.
Веб-сервисы и микросервисы.
Приложения с интерфейсом командной строки.
Программы для прикладных информационных наук.
Компьютерные игры и ПО для рендеринга.
Небольшие служебные программы.
Приложения для Интернета Вещей (IoT).
Crystal способен сыграть ключевую роль, чтобы стартап на базе Ruby был выведен на следующий уровень
производительности. Если нужно оптимизировать части проекта Ruby, то более целесообразно портировать код на Crystal, чем в ЯП Go или С++.
_Заключение_
Мы надеемся, что этот краткий обзор убедил вас в потенциале Кристалла, и вы готовы углубиться в тему.
Позже в этой Книге вы увидите еще больше функций Crystal, которые добавляют ему популярности,
и среди них:
Возможность использования родовых типов в методах и структурах данных.
Макросистема для генерации кода.
Облегченная система распараллеливания на каналы и волокна (fibers).
Простые привязки к ЯП C.
Мы детально рассмотрим их в последующих главах, но сначала мы себе построим комфортабельную среду для программирования на Crystal.
Глава 2. Фундаментальные основы.
В этой главе вы будете работать с руководством Кристалла. Вы научитесь создавать простые виды данных и структуры данных, а также управляться с ними. Вы напишете код, который выбирает линию поведения в зависимости от принимаемых им данных, и вы научитесь структурировать этот код в методы, классы, модули и потоки. Эта глава дает базовые знания, необходимые для того, чтобы вы делали простые вещи в Crystal, а последующие главы будут содержать подробные сведения о более широких возможностях.
Если вы Рубист, Crystal позволит вам чувствовать себя как рыба в воде. Вы, наверное, можете просматривать материал, особое внимание уделяя отличиям от Ruby. Фактически, в разделе Организация кода в классах и
Модулях, вы будете портировать некий код Ruby шаг за шагом в код Кристалла. Вы скоро осознаете, что для Кристалла все важное связано с сообщениями, которые вы получаете во время компиляции, тогда как с Ruby все значительное происходит во время выполнения кода.
Если вы не разработчик ПО на Ruby, то сначала синтаксис может показаться странным, но со временем вы увидите, какой он лаконичный и интуитивный. Затем во 2-й части книги мы глубже рассмотрим некоторые важные детали и приблизимся к истинному пониманию того, почему Crystal работает именно так, как он это делает.
Запустите среду Crystal Playground, чтобы следовать дальше.
_Использование простых переменных и типов_
Программирование главным образом направлено на перемещение данных с места на место, и вам потребуются контейнеры для данных. Переменные Кристалла могут хранить различные виды данных. Как и в большинстве языков, вы присваиваете их конкретной переменной символом = ;
__foundations/variables.cr__
name = "Diamond"
element = 'C'
hardness = 10
Однако предназначение Crystal это нечто большее, нежели только помещение значения в переменную. Компилятор определяет тип переменной из приписанного ей значения. Вы можете просмотреть типы переменных, определенных компилятором самовольно через отображение типа, проверкой с помощью выражения "typeof".
__foundations/variables.cr__
puts typeof(name) # => String
puts typeof(element) # => Char
puts typeof(hardness) # => Int32
Crystal интерпретировал переменную "name" как String, так как вы использовали двойные кавычки.
Он интерпретировал element как символ (одиночное символьное значение), поскольку вы использовали одиночные кавычки.
Он интерпретировал hardness как целое число, а точнее как 32-разрядное целое число, поскольку 10 является коротким числом без десятичной точки (или запятой, которую мы используем в десятичной дроби). (Typeof - отличный диагностический инструмент, но если вы включаете его в программный код, вы работаете против уклона Crystal на управление представлением типа во время компиляции.)
В большинстве случаев можно доверить Кристаллу определить подходящий тип переменной. Если вы хотите
большей ясности в этом вопросе, вы можете подсказать компилятору, чего именно вы хотите, и компилятор выполнит это для вас.
__foundations/variables.cr__
hardness : Int32
hardness = 10
puts typeof(hardness) # => Int32
hardness = "20" # => Error... type must be Int32, not (Int32 | String)
Как показано в первой строке, вы можете объявить переменную и ее тип, прежде чем присвоить ей значение.
Компилятор Crystal затем использует этот тип для вынесения решения о соответствии последующих операций присваивания. Но эту переменную нельзя использовать ни для чего до момента, когда ей будет приписано значение.
(Если вы явно определите тип переменной, Crystal позволит присваивать ей значения различных типов, а
компилятор будет соблюдать эти установки, если возможно.)
Вы можете также объявить и присвоить в одной строке, например hardness : Int32 = 10. (Int32 - 32-разрядное целое число со знаком (+-); и не забудьте пробелы вокруг двоеточия.) Все же помните, что вы можете позволить Crystal создавать типы переменных в большинстве случаев, и нет необходимости загромождать ваш код объявлениями типов.
При желании, вы можете также раздать значения множеству переменных за один раз.
__foundations/variables.cr__
name, element, hardness = "Diamond", 'C', 10
Если вы хотите, вы также можете поместить несколько операторов на одну строку, если вы разделяете их с помощью точки с запятой.
__foundations/variables.cr __
name = "Diamond"; element = 'C'; hardness = 10
Точка с запятой позволяет размещать на строке множество операторов любого рода, а не только переменные назначения.
В спешке вы можете также переключать или заменять значения двух переменных в одной строке:
__crystal_new/variables.cr__
# swap
n = 41
m = 42
n, m = m, n
n # => 42
m # => 41
(Компилятор Кристалла "за ширмой" создает временную переменную, чтобы сделать эту работу.)
Нормальные переменные, которые могут изменять значение, начинаются со строчных букв и используют подчеркивания для пробелов, называемых змеей или боксом подчеркивания.
Константы, которые не могут менять значение, начинаются с букв верхнего регистра, и они традиционно пишутся заглавными буквами (с символами подчеркивания вместо пробелов). Если вы пытаетесь установить для них значения после того, как они уже были установлены, компилятор Crystal будет протестовать.
__foundations/variables.cr__
DIAMOND_HARDNESS = 10
DIAMOND_HARDNESS = 20 # => already initialized constant DIAMOND_HARDNESS
Crystal также имеет несколько резко ограниченных типов, которые полезны для логических операций.
Булевы (относящиеся к логическому типу) переменные могут принимать значения true и false,
тогда как "nil" (относится к типу Nil) явно указывает, что здесь нет никакого значения.
_______________________________________________
Не глобальный...
Ruby поддерживает глобальные переменные, имена которых начинаются с $. Переменные Кристалла
всегда локально определены, и глобальных переменных в этом ЯП нет.
______________________________________________
_Операции над переменными_
Теперь, поскольку у вас есть контейнеры для хранения значений, вы захотите сделать что-нибудь с этими значениями.
Crystal предлагает традиционные математические операции, с незначительными отличиями, зависящими от
того, что именно обрабатывается: значения типа "float" или целые числа. (Также можно смешивать числа с плавающей точкой и целочисленные).
__foundations/operations.cr__
d = 10 + 2 # => 12
e = 36 - 12 # => 24
f = 7 * 8 # => 56
g = 37 / 8 # => 4 ;(integer division)
h = 37 % 8 # => 5 ;(integers remainder / mod)
i = 36.0 / 8 # => 4.5 ;(float, or use fdiv function)
В строках применяют оператор + для конкатенации, как и во многих ЯП.
__foundations/operations.cr__
"this" + "is" + "a" + "test" # => thisisatest
"this " + "is " + "a " + "test" # => this is a test
Конкатенация работает, но это неуклюжий инструмент. Первый элемент должен быть строкой, чтобы он вообще работал; и он откажет, если вы попытаетесь (в стиле Руби) добавить число.
К счастью, Crystal позволяет вам собирать строки более естественно, с интерполяцией.
__foundations/operations.cr__
name = "Diamond"
hardness = 10
"The hardness of #{name} is #{hardness}." # => The hardness of Diamond is 10.
Crystal вычисляет значение каждой синтаксической структуры вида #{expression}, при необходимости преобразует его в строку, и сочетает с предыдущей строкой.
За этими операторами сокрыта нехитрая истина: все эти значения объекты.
Crystal преобразует универсальный синтаксис оператора для методов, облегчающих написание читаемого кода. Тип соответствует классу, что означает, что все эти значения имеют методы, которые можно использовать.
Например, size это метод для объектов String, возвращающий количество символов в виде Int32.
__foundations/operations.cr__
name = "Diamond"
hardness = 10
name.size # => 7
hardness.size # => compilation error - undefined method 'size' for Int32
Но size не является методом для значений Int32, поэтому компилятор выдаст ошибку (undefined method size on Int32).
ВАШ ЧЕРЁД 1 (Упражнение).
Проверьте и объясните выходные данные следующих операторов в Crystal Playground:
12 + 12
"12 + 12"
"12" + "12"
"I" * 5
'12' + '12'
5 * "I"
"12" + 12
"2" * "5"
_Структурирование данных с помощью контейнерных типов_
Простые переменные обеспечивают устойчивый фундамент, но по мере роста ваших программ вам не захочется продолжать жонглирование сотнями имен переменных в голове. Crystal предоставляет ряд типов группировки, которые позволяют хранить немало информации в структурированном виде.
Массивы и хэши помогут вам начать работу, а кортежи и наборы появятся в следующих главах.
ИСПОЛЬЗУЕМ МАССИВЫ.
Иногда необходимо создать список значений, сохраняемых в порядке и доступных по очередному номеру. Поскольку это Crystal, давайте коллекционировать минералы. (Все эти названия минералов настоящие, и здесь их немало)
Простейший способ это использовать массив:
__foundations/compound_types_arrays.cr__
minerals = ["alunite", "chromium", "vlasovite"]
typeof(minerals) # => Array(String)
# or, to use a different notation
minerals2 = %w(alunite chromium vlasovite)
typeof(minerals2) # => Array(String)
Crystal говорит вам, что minerals это не просто массив; это массив String.
Его тип, Array(String), также содержит тип, хранящий внутри последовательность элементов.
Вы можете легко добавлять минералы с помощью оператора <<. Метод "size" сообщает вам количество элементов, содержащихся в массиве:
__foundations/compound_types_arrays.cr__
minerals << "wagnerite"
minerals << "muscovite"
minerals
# => ["alunite", "chromium", "vlasovite", "wagnerite", "muscovite"]
minerals.size # => 5
Crystal проверяет содержимое: добавлять элементы другого типа не разрешается. Попробуйте добавить число:
__foundations/compound_types_arrays.cr__
minerals << "wagnerite"
minerals << "muscovite"
minerals
# => ["alunite", "chromium", "vlasovite", "wagnerite", "muscovite"]
minerals.size # => 5
Вы получите ошибку во время компиляции. Вы также получите ошибку, если попытаетесь начать с пустого массива:
__foundations/compound_types_arrays.cr__
precious_minerals = []
# => Error: for empty arrays use '[] of ElementType'
Это происходит оттого, что компилятор не имеет достаточной информации для определения типа и поэтому не может выделить память для массива. Здесь наблюдается острый контраст с практикой Ruby, где тип выясняется во время выполнения.
Можно создать пустой массив, но необходимо указать тип с помощью символов [] или путем создания объекта класса "Array" методом "new":
__foundations/compound_types_arrays.cr__
precious_minerals = [] of String
precious_minerals2 = Array(String).new
Как и следовало ожидать, вы можете считывать элементы по индексу, отражающему позицию элемента в массиве:
__foundations/compound_types_arrays.cr__
minerals[0] # => "alunite"
minerals[3] # => "wagnerite"
minerals[-2] # => "wagnerite"
# negative indices count from the end, which is -1
Вы можете читать вложенные массивы, смежные секции массивов двумя различными способами. Можно дать начальный индекс и размер, или использовать диапазон индексов:
__foundations/compound_types_arrays.cr__
minerals[2, 3] # => ["vlasovite", "wagnerite", "muscovite"]
minerals[2..4] # => ["vlasovite", "wagnerite", "muscovite"]
Что будет, если вы используете неправильный индекс? Первый элемент в массиве Crystal всегда имеет индекс "ноль", а последний всегда равен числу элементов массива, которое уменьшили на 1. Если вы попытаетесь затронуть что-то за пределами этого диапазона, вы получите ошибку во время выполнения (что в Crystal случается нечасто):
__foundations/compound_types_arrays.cr__
minerals[7] # => Runtime error: Index out of bounds (IndexError)
Если логика вашей программы требует, чтобы вы периодически отслеживали несущественные (несрабатывающие) ключи, вы можете избежать этой ошибки. Используйте метод []?, который возвращает "nil" вместо сбоя программы:
__foundations/compound_types_arrays.cr__
minerals[7]? # => nil
В главе 1 вы видели, что компилятор предотвращает использование "nil", когда катастрофа притаилась за углом. Скоро вы увидите, как справиться с этим довольно элегантным образом.
Что, если вы попробуете поместить элементы разных типов в свой массив?
__foundations/compound_types_arrays.cr__
pseudo_minerals = ["alunite", 'C', 42]
Это сработает, но результирующий тип этого массива своеобразен:
__foundations/compound_types_arrays.cr__
typeof(pseudo_minerals) # => Array(Char | Int32 | String)
Компилятор делает вывод, что тип элемента либо Char, либо Int32, либо String. Другими словами, любой выделенный элемент принадлежит объединенному типу "Char | Int32 | String". (Если вам требуется структурировать множество элементов таким образом, чтобы оно содержало конкретные типы на определенных позициях, то следует изучить кортежи.)
Совместные (или комбинированные) типы данных являются мощной особенностью Crystal: выражение может иметь набор из множества типов во время компиляции, и компилятор тщательно проверяет, разрешены ли все вызовы метода для всех этих типов. Вам встретится немало примеров, демонстрирующих совмещенные типы данных и способы их использования, позже в этой книге.
Метод "includes?" позволяет вам удостовериться, что определенный элемент присутствует в массиве.
arr = [56, 123, 5, 42, 108]
arr.includes? 42 # => true
Требуется удалить начальный или конечный элемент? Методы "shift" и "pop" могут помочь.
p arr.shift # => 56
p arr # => [123, 5, 42, 108]
p arr.pop # => 108
p arr # => [123, 5, 42]
Если вы захотите пройтись по всем значениям массива, лучше сделать это с методом "each" или с одним из его вариантов.
arr.each do |i|
puts i
end
API для класса "Array" описывает гораздо больше полезных методов. Кроме того, массивы могут добавлять и удалять элементы, поскольку они хранятся в "общей куче" (динамически выделяемой области) памяти и не имеют фиксированного размера. Если вам требуется производительность, используйте "StaticArray", который является массивом фиксированного размера, помещаемым внутрь стека во время компиляции.
_________________________________________
Демонстрация массивов.
Хотите отобразить массив? Вот несколько быстрых вариантов:
arr = [1, 'a', "Crystal", 3.14]
print arr # [1, 'a', "Crystal", 3.14] (no newline)
puts arr # [1, 'a', "Crystal", 3.14]
p arr # [1, 'a', "Crystal", 3.14]
pp arr # [1, 'a', "Crystal", 3.14]
p arr.inspect # "[1, 'a', \"Crystal\", 3.14]"
printf("%s", arr[1]) # a (no newline)
p sprintf("%s", arr[1]) # "a"
"pp" и "inspect" полезны для отладки; printf и sprintf принимает строку в формате, свойственном ЯП C; последний вариант (sprintf) возвращает тип String.
_______________________________________
ВАШ ЧЕРЁД 2 (Упражнение).
Удаление по значению:
Чаще всего при работе с массивами программист оперирует их содержимым, отталкиваясь от позиции нужного элемента относительно остальных. Однако Crystal также позволяет манипулировать содержимым массива, опираясь на значения элементов.
Ознакомьтесь с документацией по API Crystal и сообразите, как перейти от ["алунит", "хром", "власовит"] к ["алунит", "власовит"], не ссылаясь на расположение значений в массиве (т.е. не используя индексы).
minerals = ["alunite", "chromium", "vlasovite"]
minerals.delete("chromium")
p minerals #=> ["alunite", "vlasovite"]
ИСПОЛЬЗОВАНИЕ ХЭШЕЙ.
Массивы превосходны, если вы желаете получить информацию, исходя из ее положения в "обойме", но иногда вместо этого требуется запросить данные, опираясь на ключевое значение. Хэши делают это удобнее и проще.
Давайте построим коллекцию, где будет представлено название минерала и свойство его твердости, используя Шкалу Твердости Мооса.
Получив название минерала, вам нужно быстро найти его твердость. Для этого хэш (иногда называемый картой или словарем) подходит идеально:
__foundations/compound_types_hashes.cr__
mohs = { "talc" => 1,
"calcite" => 3,
"apatite" => 5,
"corundum" => 9,
}
typeof(mohs) # => Hash(String, Int32)
Его тип, Hash (String, Int32), основан на типе ключа (String) и типе значения (Int32). Вы можете быстро
получить значение для данного ключа, используя индексирование ключа:
__foundations/compound_types_hashes.cr__
mohs["apatite"] # => 5
Что, если искомый ключ не существует, например gold ??
Тогда, как это уже было с массивами, вы получите ошибку времени выполнения:
__foundations/compound_types_hashes.cr__
mohs["gold"]
# => Runtime error: Missing hash key: "gold" (KeyError)
Так же, как и с массивами, если логика программы должна справляться с ситуацией, когда ключ является несущественным, вам безопаснее будет использовать вариант с []?, возвращающий ноль (nil):
__foundations/compound_types_hashes.cr__
mohs["gold"]? # => nil
Или, еще лучше, проверить действительность ключа с помощью "has_key?":
__foundations/compound_types_hashes.cr__
mohs.has_key? "gold" # => false
Добавить новую пару ключ-значение или изменить существующую пару довольно просто. Вы сможете использовать обычное для массивов "индексное" представление, отличие лишь в том, что теперь используется ключ вместо индекса:
__foundations/compound_types_hashes.cr__
mohs["diamond"] = 9 # adding key
mohs["diamond"] = 10 # changing value
mohs
# => {"talc" => 1, "calcite" => 3, "apatite" => 5,
# "corundum" => 9, "diamond" => 10}
mohs.size # => 5
Заметьте, что размер хэша увеличился с 4 до 5. Что происходит при добавлении пары(ключ+значение), где тип ключа или тип значения отличны от основного типа большинства элементов?
__foundations/compound_types_hashes.cr__
mohs['C'] = 4.5 # Error: no overload matches
# 'Hash(String, Int32)#[]=' with types Char, Float64
Снова вы получаете ошибку во время компиляции: Crystal статически управляет вашими типами!
Что, если вы хотите начать с пустого хэша?
__foundations/compound_types_hashes.cr__
mohs = {} # Error: Syntax error: for empty hashes use
# '{} of KeyType => ValueType'
Это не работает. Как и в случае с массивами, необходимо снова уточнить типы:
__foundations/compound_types_hashes.cr__
mohs = {} of String => Int32 # {}
mohs = Hash(String, Int32).new
Как вы можете догадаться теперь, хэши наследуют все свои методы из класса Hash, который вы можете найти в документации к API.
ВАШ ЧЕРЁД 3 (Упражнение).
Этот хэш пуст? Crystal позволит вам создавать пустые хеши, пока вы указываете типы и для значений, и для ключей. Но пустые хэши могут создавать ошибки и нулевые значения, когда вы не ожидаете этого. Изучите документы API и найдите способы проверки пустых хешей и безопасного управления ими даже в тех случаях, когда они могут оказаться пустыми.
mohs = {
"talc" => 1,
"calcite" => 3,
"apatite" => 5,
"corundum" => 9
} of String => Int32
p mohs.empty? => false
_Проектирование кода: Вариативность алгоритмического маршрута_
Теперь, когда вы овладели основным набором переменных, пришло время проводить решения с их помощью. Как любой
язык программирования, Crystal имеет ряд конструкций, управляющих порядком выполнения строк кода, что позволяет вам описать поведение вашей программы после запуска.
Мы начнем с некоторых из них прямо здесь и рассмотрим ещё больше информации в Главе 3 Типизация переменных и управление ходом исполнения.
СОВЕРШЕНИЕ ВЫБОРА.
Crystal предлагает классическое выражение if с вариациями, что позволяет вам создавать сложные структуры. A простое выражение if проверяет соответствие условию и делает что-нибудь, если это условие выполнено:
__foundations/control_flow.cr__
hardness = 7 # quartz
if hardness < 8
puts "softer than topaz"
end
# => softer than topaz
В этом случае 7 меньше 8, поэтому Crystal выведет сообщение "мягче топаза". Если бы значение твердости оказалось больше или равным 8, условие не было бы выполнено, и конструкция if не сделала бы ничего.
Crystal также поддерживает усложненные выражения if, включая операторы elsif и else, чтобы проверить больше условных "веток" за один подход. (В целях поддержания "читабельности" кода делайте записи условных вариантов схожими с одиночной инструкцией if.)
__foundations/control_flow.cr__
hardness = 5.25
if 0 < hardness < 5
puts "softer than apatite"
elsif hardness < 8
puts "harder than apatite, softer than topaz"
else
puts "topaz or harder!"
end
# => harder than apatite, softer than topaz
В этом случае первая условная проверка на начальном if не дает совпадений. Вторая, на "elsif",
оказалась успешной. Последняя проверка, "else", которая используется, если ни одно из других условий не сработало, в данном случае не будет вызвана.
Когда Crystal сопоставляет условия, он вычисляет, истинны они или фальшивы, но разделение на "верно
или ложно" не будет абсолютно точным. Crystal имеет довольно ограниченное представление "ошибочного", рассматривая "false" и "nil" (и пустые указатели) как фальшивые, а всё остальное "true"; цифры, ноль, строки, массивы вам представляются как истинное.
Каждая ветвь выражения if возвращает значение, поэтому можно комбинировать операцию присваивания переменной с конструкцией if для создания избирательных назначений.
__foundations/control_flow.cr__
output = if 0 < hardness < 5
"softer than apatite"
elsif hardness < 8
"harder than apatite, softer than topaz"
else
"topaz or harder!"
end
output # => harder than apatite, softer than topaz
Вы можете также записать оператор if в виде суффикса, добавляемого в конце назначения переменной.
__foundations/control_flow.cr__
output = "softer than topaz" if hardness < 8 # => softer than topaz
В зависимости от состояния, которое вы проверяете, оно может быть более читаемым, благодаря использованию "unless", вот так:
__foundations/control_flow.cr__
output = "softer than topaz" unless hardness >= 8
output # => softer than topaz
Хотя выражения if легко читаются и удобны для простых условий, при злоупотреблении они загромождают код и делают его многословным.
Если вы хотите протестировать какое-то значение на соответствие разным условиям, выражение "case-when" может создать код, который будет удобнее для восприятия.
__foundations/control_flow.cr__
output = case hardness
when 4
"hard as fluorite"
when 7
"hard as quartz"
when 10
"hard as diamond"
else
"can't say how hard"
end # => "can't say how hard"
Можно также создать выражение "case-when", которое использует условные конструкции, почти такие же, как для оператора if.
__foundations/control_flow.cr__
output = case
when 0 < hardness < 5
"softer than apatite"
when hardness < 8
"harder than apatite, softer than topaz"
else
"topaz or harder!"
end # => harder than apatite, softer than topaz"
ЗАЦИКЛИВАНИЕ.
Иногда возникает надобность выполнить блок кода фиксированное количество раз. Вы можете использовать специальный метод times для типа Int. Либо можно охватить блок кода с помощью двойного оператора "do ... end" так, чтобы он содержал внутри несколько строк кода; вместо него можно использовать {;;}, где строки кода разделяет точка с запятой.
__foundations/control_flow.cr__
# Int#times
5.times do
p "Hi"
p "Low"
end
# same as:
5.times { p "Hi"; p "Low" }
Это просто, зато быстро: метод times работает так же быстро, как цикл Cи, потому что он интегрируется в исполняемый код. Иногда вам требуется не просто выполнить код определенное количество раз, а ещё и охватить диапазон значений.
Тогда вы можете использовать ещё один тип Кристалла, Range. Вы объявляете диапазоны, используя синтаксис вида 2..7 (см. пример ниже), а в общем случае отмечаете начало и конец (start..end).
Результат содержит все элементы от начала до конца включительно. (Вы можете использовать ... для создания
эксклюзивного диапазона, отбрасывающего конечное значение.) Начальные и конечные значения могут быть целыми числами, символами, или даже строками. Если вы желаете просмотреть значения диапазона, то потребуется преобразовать их в массивы с помощью "to_a".
__foundations/control_flow.cr__
inc = 2..7
p inc.to_a #=> [2, 3, 4, 5, 6, 7]
exc = 2...7
p exc.to_a #=> [2, 3, 4, 5, 6]
Диапазоны имеют метод "each", который позволяет сделать что-либо с каждым из элементов последовательно. Это может быть полезным, например, для извлечения набора значений из массива.
__foundations/control_flow.cr__
# Range#each
mohs_list = ["nothing", "talc", "gypsum", "calcite", "fluorite", "apatite",
"orthoclase feldspar", "quartz", "topaz", "corundum", "diamond"]
(2..5).each do |i|
puts mohs_list[i]
end
# produces:
# gypsum
# calcite
# fluorite
# apatite
Если вам нужно обработать каждое значение в массиве, можно, не связываясь с диапазонами, просто применить метод "each" к вашему массиву.
__foundations/control_flow.cr__
# Range#each
mohs_list = ["nothing", "talc", "gypsum", "calcite", "fluorite", "apatite",
"orthoclase feldspar", "quartz", "topaz", "corundum", "diamond"]
mohs_list.each do |mineral|
puts mineral
end
# produces:
# nothing
# talc
# gypsum
# calcite
# fluorite
# apatite
# orthoclase feldspar
# quartz
# topaz
# corundum
# diamond
Если вы хотите создать собственную логику для циклов, Crystal предлагает очень простую конструкцию "loop do ... end". Она начинается как бесконечный цикл, поэтому необходимо выйти из нее с явным разрывом (break):
__foundations/control_flow.cr__
n = 1
loop do
puts "a mighty crystal"
n += 1
break if n == 3
end
# => a mighty crystal
# => a mighty crystal
Crystal также реализует типичную конструкцию while, которая "гоняет" цикл до тех пор, пока заданное условие не становится ложным (false):
__foundations/control_flow.cr__
a = 1
while (a += 1) < 10
if a == 3
next
elsif a > 6
break
end
puts a
end # => 2, 4, 5 and 6 on successive lines
Опираясь на истинность предшествующей условной инструкции, "next" переходит к следующей итерации цикла (т.е. начинает его заново), тогда как break выходит из цикла (когда переменная вырастет до 7).
Кроме того, если вы проверяете варианты, когда условие не соответствует истине, вы можете сделать код легче для чтения, написав "until condition" вместо "while !condition".
__________________________________________
Пустые строки.
Принятая в Crystal система обозначений для символьных и стоковых литералов имеет больше общего с ЯП C, чем с Ruby, который не имеет явного типа Chars для отдельных символов.
Имейте в виду, что пустой символьный литерал не существует в Crystal: если вам нужна пустая
строка, используйте "".
_________________________________________
ВАШ ЧЕРЕД 4.
Напишите программу Crystal, используя while и if, которая правильно напечатает текст этой песни [https://en.wikipedia.org/wiki/99_Bottles_of_Beer].
В качестве бонуса: попробуйте также применить метод "Int32#downto".
_Использование Методов_
Переменные и базовая логика образуют основу ЯП, но большинство "кодерских" решений требуют большего разнообразия конструкций. Crystal, как и Ruby, предлагает методы в качестве первоначальной единицы, образующей программную архитектуру. Методы имеют имена, принимают аргументы (в ЯП Crystal типизированные аргументы!) и возвращают значения. Независимые методы также называются функциями, но поскольку Crystal обычно использует их в контексте объектов и классов, вы чаще будете иметь дело с методами.
Вы уже использовали несколько методов - typeof, puts, p - являются методами верхнего уровня, доступными откуда угодно. Некоторые методы предназначены для специфического контекста.
Метод "size" допустимо применять к строкам, тогда как "each" предназначен для массивов. Методы, которые вы использовали до сих пор, встроены в ядро Кристалла и его библиотеку, но вы можете создавать собственные методы, используя синтаксис "def" и "end", весьма схожий с применяемым в Ruby.
__foundations/methods.cr__
def double(num)
num * 2
end
puts double(6) # => 12
Аргументы перечисляются в скобках сразу после имени метода. Этот метод принимает только один аргумент; при наличии нескольких аргументов они будут разделены запятыми. Аргументы воспринимаются как переменные, существующие в области действия метода. Обратите внимание, что метод возвращает значение своего последнего выражения. Код может ясно сформулировать "returns num * 2", но это не обязательно так.
Метод double выглядит так, как будто он предназначен для работы с числами, с оператором умножения,
но на самом деле его применение является более разносторонним. Строки Кристалла также позволяют использовать оператор умножения; double("6") возвращает результат 66. Но double(true) выводит сообщение:
in line 2: undefined method * for Bool.
Если вы желаете больше контроля над тем, как ваши функции реагируют на аргументы различных типов, вы можете указать тип явно:
__foundations/methods.cr__
def double(num : Int32)
num * 2
end
puts double(6) # => 12
Теперь, если попытаться выполнить double("6"), Crystal скажет "нет", возвращая ошибку без перегружаемых совпадений double с типом String. Кстати, "перегрузка" указывает на другую интересную возможность.
Crystal предоставляет вам перегруженные методы, иначе говоря возможность использовать одно и то же имя метода с различным набором (количеством и типами) аргументов. Если вы хотите подготовить разные дубликаты для чисел и строк, вы можете:
__foundations/methods.cr__
def double(num : Int32)
num * 2
end
def double(str : String)
str + " " + str
end
puts double("6") # => 6 6
puts double(6) # => 12
Хотя обычно вы можете позволить Кристаллу определять типы, могут быть моменты, когда вы хотите указать их более точно.
ВАШ ЧЕРЕД 5.
Запишите метод sample, который возвращает массив, содержащий случайные числа с плавающей десятичной запятой. Размер массива передается в качестве аргумента в выборку. (Подсказка: Используйте rand для создания случайного числа, соответствующего виду float.)
_Организация кода в классах и модулях_
Переменные и методы являются мощным средством, но они должны быть организованы в более крупные структуры, чтобы быть действительно полезными. Поскольку Crystal является полноценным объектно-ориентированным языком, классы представляются основным инструментом для реализации сложных конструкций. Классы позволяют описать комбинации методов и связанных с ними данных, которые затем можно превратить в объекты с помощью команды "new". Когда вы создаете классы, которые будут работать друг с другом, вы можете скомпоновать их в более крупные модули. Базовые классы Кристалла очень похожи на классы Ruby, хотя Crystal и внёс некоторые изменения.
ОСНОВЫ ПОНЯТИЯ КЛАСС.
Классы группируют общедоступные методы и свойства, а также могут включать в себя дополнительные методы и переменные, чтобы всё вместе работало как задумано. Имена классов начинаются с заглавной буквы, но остальная часть имени обычно пишется строчными или же в ГорбатомРегистре (верхний регистр служит для разделения слитной последовательности слов), для контраста с названиями методов и переменных.
Когда вы строите новый класс, вы также создаете новый тип. Этот чрезвычайно простой пример демонстрирует оформление пустого класса, создание из него объекта и проверку типа объекта.
__foundations/classes.cr__
class Mineral
end
mine = Mineral.new()
puts typeof(mine) # => Mineral
Это еще не полновесный объект, но дополнить его совсем не сложно. При создании минерала следует указать его наименование и твердость, что-то вроде mine = Mineral.new("talc", 1.0). Твердость не обязательно является целым числом, и потому этот объект ориентирован на дробные числа (типа Float64).
В Кристалле, как и в Ruby, это делается с помощью метода initialize (он исполняет роль конструктора), принимающего аргументы указанного типа.
__foundations/classes.cr__
class Mineral
def initialize(common_name : String, hardness : Float64)
@common_name = common_name
@hardness = hardness
end
end
mine = Mineral.new("talc", 1.0)
puts typeof(mine) # => Mineral
Поскольку аргументы "common_name" и "hardness" пока никак не используются (помимо простого назначения),
компилятору не хватает информации, необходимой для определения их типа, и он пожалуется, если вы не укажете их.
"@common_name" и "@hardness" являются переменными экземпляра, специфичными для объекта созданного здесь. Crystal также может сохранить несколько ваших типизаций в методе initialize. Если вы используете имена переменных экземпляра (добавляя префикс @) в качестве имен аргументов, то Crystal просто помещает эти аргументы в переменные экземпляра.
__foundations/classes.cr__
class Mineral
def initialize(@common_name : String, @hardness : Float64)
end
end
mine = Mineral.new("talc", 1.0)
puts typeof(mine) # => Mineral
Но теперь уже нет возможности получить доступ к этим переменным экземпляра извне объекта. Если вы запросите, например, Mineral.common_name, то получите сообщение неопределенный метод common_name для Mineral.class.
Можно создать метод с именем "common_name=", возвращающий значение @common_name, но Crystal предлагает кое-что более простое: "извлекатели" и "установщики" (getters и setters).
Вы можете разрешить чтение и манипулирование "common_name" извне объекта, а также считывание твердости (hardness), написав:
__foundations/classes.cr__
class Mineral
getter common_name : String
setter common_name
getter hardness : Float64
def initialize(common_name, hardness)
@common_name = common_name
@hardness = hardness
end
end
mine = Mineral.new("talc", 1.0)
puts mine.common_name # => talc
mine.common_name="gold"
puts mine.common_name # => gold
puts mine.hardness # => 1.0
Компилятор может опознать объявления типов, прописанные выше или ниже в тексте (где вам удобнее), но размещение в верхней части класса считается более понятным для людей, поэтому в предыдущем примере с getter сделано именно так.
Доработанная реализация класса позволяет вам читать и изменять название минерала, и также считывать твердость. Эта версия позволяет читать и изменять название минерала, и читать твердость. Если вы попытаетесь установить твердость (hardness), то всё равно получите ошибку "undefined method name" из-за отсутствия установщика "setter". (Если вы Рубист, то могли заметить, что getter эквивалентен "attr_reader" в ЯП Ruby, установщик setter эквивалентен "attr_writer", а "property" соответствует "attr_accessor". Только они более кратки и выразительны.)
В отличие от переменных, методы в классах Crystal по умолчанию видны вне объекта. Они похожи на методы, которые вы задавали ранее, но только на них необходимо ссылаться через объект или из их собственного содержимого. Простой объектный метод будет выглядеть следующим образом:
__foundations/classes.cr__
class Mineral
getter common_name : String
setter common_name
getter hardness : Float64
getter crystal_struct : String
def initialize(@common_name, @hardness, @crystal_struct)
end
def describe
"This is #{common_name} with a Mohs hardness of #{hardness}
and a structure of #{crystal_struct}."
end
end
mine = Mineral.new("talc", 1.0, "monoclinic")
puts mine.describe # => This is talc with a Mohs hardness of 1.0
# => and a structure of monoclinic.
Чтобы расширить возможности describe, в примере добавлена переменная crystal_struct. Метод описания (describe) собирает три переменные объекта и вставляет их между словосочетаниями, образуя предложение.
Пример также демонстрирует, что строки могут содержать "разрывы", которые могут быть полезными или раздражающими (в зависимости от контекста и ваших предпочтений.) В части II, главе 5 (Использование классов и структур), мы глубже исследуем классы и обсудим видимость, наследование, а также иерархию классов.
ВАШ ЧЕРЕД 6.
1) Предположим, вы хотите иметь возможность сделать объекты класса Mineral для минералов, кристаллическая структура (crystal_struct) которых вам пока не известна. Как можно это сделать? Подсказка: Используйте тип объединения для описания свойств минералов (property).
2) Добавьте метод to_s, который создает строковое (String) представление объекта Mineral, и используйте его для вывода предложения. (Подсказка: Подходящий объект self.)
СОЗДАНИЕ МОДУЛЕЙ.
Модули группируют методы и классы, реализующие соответствующие функциональные возможности. Например, модуль Random из стандартной библиотеки содержит методы генерации всех видов случайных значений. Класс может быть "привязан" к одному или нескольким модулям; тогда они образуют так называемый mixin. Таким образом, объекты класса могут использовать методы модуля.
Давайте сделаем модуль под названием Hardness, который содержит метод hardness, возвращающий эту характеристику для данного минерала:
__foundations/modules.cr__
module Hardness
def data
{"talc" => 1, "calcite" => 3, "apatite" => 5, "corundum" => 9}
end
def hardness
data[self.name]
end
end
В этом примере наш класс Mineral теперь имеет лишь одно свойство name, но зато включает в себя "полезные ресурсы" модуля Hardness:
__foundations/modules.cr__
class Mineral
include Hardness
getter name : String
def initialize(@name)
end
end
Включив этот модуль, вы сможете вызывать его методы для любого объекта Mineral:
__foundations/modules.cr__
min = Mineral.new("corundum")
min.hardness # => 9
Класс также может расширять (extend) модуль, и тогда его методы становятся для класса "своими".
_________________________________________
Примечание:
Наглядно объяснить различия extend и include можно так:
*Расширение класс вытягивается и накрывает пространство модуля; происходит обобщение ресурсов модуля и класса.
*Включение методы модуля будут добавлены к личному арсеналу всех создаваемых экземпляров класса, для которого прописано "include ..." ; Короче, это дополнение собственного содержимого.
________________________________________
_Параллельное исполнение кода с помощью Волокон_
Хотя и трудно продемонстрировать их с реальной пользой в крошечном примере, вы должны узнать о
Волокнах Кристалла, прежде чем идти дальше. Они могут изменить ваш подход к проектированию программ.
В наш век многоядерных микропроцессоров и распределенных вычислений разработчикам ПО нужно, чтобы их ЯП обеспечивали великолепную поддержку многопоточности и параллельной обработки данных.
К сожалению, это то, в чем Руби никогда не был очень хорош. В Ruby вы можете создать множество потоков выполнения с помощью Thread.new, но они работают на уровне операционной системы. Их можно получить максимум несколько сотен. Кроме того, в его Си-реализации, Ruby ограничен GIL (глобальная блокировка интерпретатора), которая подразумевает, что только один поток Ruby может работать единовременно, так что будет задействовано лишь одно ядро.
В противоположность ему, Crystal изначально разработан с учетом поддержки параллельных синхронных вычислений; хотя во время написания этой книги (eBook) реализация многопоточности все еще была в активной разработке.
Модель конвейерного "разложения" нагрузки в Crystal базируется на двух концепциях:
1) Волокна, которые представляют собой легкие нити, созданные методом "spawn" и контролируемые непосредственно Кристаллом.
2) Каналы, по которым волокна взаимодействуют друг с другом.
Основной программный поток это волокно, но другие волокна, которые оно порождает, будут работать на заднем плане, не мешая первичному. Каналы должны знать, данные какого типа проходят через них, и потому каналы приходится типизировать.
Надо сказать, что Ruby с версии 1.9 тоже поддерживает волокна, но Crystal сам занимается распределением волокон, не требуя внимания программиста.
Вот простой пример, создающий канал для транспортировки строк. В цикле, повторяющемся 10 000 раз, мы порождаем волокно и говорим ему отправить строку "fiber #{i}: I like crystals!" через канал, и так мы знаем, какое именно волокно отправило его. Затем первичное волокно получает эту строку и записывает ее сообщение в стандартный вывод:
__foundations/fibers.cr__
chan = Channel(String).new
num = 10000
num.times do |i|
spawn do
chan.send "fiber #{i}: I like crystals!"
end
puts chan.receive
end
# =>
# fiber 0: I like crystals!
# fiber 1: I like crystals!
# fiber 2: I like crystals!
# ...
# fiber 9999: I like crystals!
На практике, конечно, здесь будет выведено гораздо больше, чем в приведенном примере. Обратите внимание на важное различие между "синхронным" и "параллельным" в Crystal:
concurrent означает, что в одном потоке одновременно запущено несколько волокон (и выполнение
идет на одном процессорном ядре), их называют "сопрограммы" (т.е. группа подпрограмм) во многих других ЯП;
параллельный (parallel), с другой стороны, означает, что одновременно выполняются две или более программные ветви, каждая на отдельном ядре (или обособленном процессоре);
До версии 0.26.1 код Кристалла выполнялся в одном потоке. Это значит, что все волокна выполняются
синхронно, и Crystal делает это хорошо и быстро, но он еще не мог работать параллельно на нескольких ядрах.
Команда разработчиков активно работала над тем, чтобы в ближайшем будущем язык стал "параллельно разделяемым".
Относительно этой темы мы обсудим ещё многое в разделе восьмой главы "Создание синхронно исполняемого кода".
ВАШ ЧЕРЕД 7.
Используя вышеприведенный пример, узнайте время, необходимое для создания 500 000 волокон.
_Заключение_
Используя несколько простых примеров на основе минералов, мы исследовали базовые конструкции и синтаксис Кристалла. Вы познакомились с основными типами переменных и с циклами, вы изучили некоторые "множественные" типы, такие как массивы и хэши. Вы также научились работать с методами, классами и модулями, и пример на ЯП Ruby вы преобразовали в Crystal. Мы остановились на простом примере с использованием волокон. По пути вам следует увидеть, как компилятор направляет программиста к более надежному и безопасному коду.
В части II вы получите гораздо больше подробностей по всем этим темам, и посмотрите, как Crystal комбинирует собственные оригинальные решения, чтобы подчеркнуть свой акцент на производительность.
ЧАСТЬ 2.
Стандартные блоки Crystal.
В этой части мы проведем обзор строительных блоков Crystal: вы узнаете как происходит классификация разных типов в этом ЯП, об основных и составных типах данных, о структурах управления, методах, классах, и модулях (генерирующих и форматирующих документы, тестирующих "черновые" шаблоны и фреймворки).
Глава 3.
Типизация переменных и управление ходом исполнения.
Теперь, когда вы знаете, как выглядит Crystal, и вы видели, как ведет себя компилятор, пришло время
углубиться в понимание того, как этот язык использует типы и как они влияют на архитектуру ваших программ.
Программы манипулируют данными. Эти данные относятся к таким типам, как целое число, строка или массив. В динамически типизированных языках, таких как Ruby или Python, типы, как правило, обычно не указывают в исходном коде. И когда программа окажется на стадии выполнения, управляющая среда установит подходящие типы для используемых данных.
Crystal отвергает эту модель, в основном потому, что она позволяет ошибкам обнаруживаться, когда программа уже запущена. Вместо этого Crystal компилирует программу перед ее запуском, и компилятору необходимо знать все
типы. Это позволяет компилятору сигнализировать о многих возможных ошибках и генерировать гораздо более оптимизированный код.
После того, как компилятор завершил свою работу, программа выполняется гораздо более безопасным и
производительным образом. Тем не менее, вы не увидите, что многие типы написаны именно на коде Кристалла, потому что находчивый компилятор может сам установить происхождение многих типов. В некоторых случаях вы упоминаете тип, помогая компилятору; например, когда самостоятельно определяете тип содержимого для массива.
Crystal также поддерживает более сложные сценарии. В некоторых случаях компилятор делает "умозаключение", что
переменная должна содержать данные более чем одного типа: например, иногда логическое (булево) значение, иногда целое число. Тогда подходящим типом будет тип объединения, в данном случае "Bool | Int32".
В этой главе мы углубимся в Типы и обработку исключений. Чтобы проиллюстрировать это, мы рассмотрим, как получить входные данные с терминала (т.е. консоли ввода, command-line). Дело не в том, что это "прикольно", но через это вы сможете ощутить, как работает Crystal.
Вы также получите больше опыта, используя особые методы управления строками, массивами и хэшами. Последние два являются групповыми типами: они могут содержать элементы разных типов. Затем вы будете использовать
символы, перечисления и регулярные выражения и узнаете несколько замечательных трюков из области
проектирования архитектуры ПО. На протяжении этой и следующих глав мы будем строить проект Конвертер валют, позволяющий применить многие вещи, которые вы изучили.
_Межтиповое преобразование данных_
В большинстве случаев вам нет нужды объявлять типы в Crystal, что может быть облегчением. Иногда все-таки необходимо убедиться, что вы работаете со значениями определенного типа. Вам не стоит особо рассчитывать на автоматические преобразования в стиле Ruby: Crystal не только сильно типизирован он хочет, чтобы вы были очень аккуратны с преобразованиями. Преобразование типа это то, что вам нужно сделать недвусмысленно и ясно
для компилятора Кристалла. (Хотя Crystal вполне справляется с простейшими преобразованиями автоматически, например, int8 в int32.)
Деньги представляют удобный пример: курсы валют это числа с множеством цифр после десятичной запятой, к примеру, 1 USD когда-то стоил 64.34603985 индийских рупий (INR). Хотя банки не используют числа с плавающей запятой из соображений аккуратности, они хорошо подойдут в качестве примера. Все, что мы печатаем на экране, поступает в нашу программу в виде строки.
Добавление чисел нечто совсем другое по сравнению с аналогичным действием, выполняемым со строками, а умножать строки было бы еще более странно.
Рассмотрим ситуацию, где вы пытаетесь преобразовать строку, представляющую валютный курс, в целое число с помощью метода "to_i":
"64.34603985".to_i # Runtime error: Invalid Int32: 64.34603985 (ArgumentError)
Руби с радостью вернул бы значение '64', но Crystal здесь "запинается" во время выполнения. Кристалл более разборчив в своем подходе к преобразованиям, которые он выполняет, потому что для разных типов предназначены разные методы "to_".
Вы можете, к примеру, конвертировать строку в значение с плавающей запятой:
"64.34603985".to_f # => 64.34603985
Вы также можете преобразовать тип "float" в целое число, используя метод to_i, но оно будет "урезано" без
предупреждения:
rate = 64.34603985
rate.to_i # => 64
Как вы разгадаете эту ошибку компилятора?
rate1 = 64.34603985
rate2 = "7"
rate1 + rate2 # Error: no overload matches 'Float64#+' with type String
Если вы хотите добавить сюда, вы должны использовать to_i на rate2; Затем to_s преобразует всё в тип String,
в результате чего rate1 и rate2 сцепляются друг с другом. Это сделает компилятор довольным, но вполне возможно совсем не то, что вы хотели:
rate1 + rate2.to_i # => 71.34603985
rate1.to_s + rate2 # => "64.346039857"
Если, напротив, необходимо удостовериться, что вы работаете с числами с плавающей запятой, вы будете использовать 'to_f ':
rate1.to_f # => 64.34603985
rate2.to_f # => 7.0
Конечно, бывают случаи, когда преобразования не срабатывают, потому что данные не подходят:
curr = "Crystal"
curr.to_i # => Runtime error: Invalid Int32: Crystal (ArgumentError)
Crystal также предлагает больше способов для информирования компилятора о том, как ему надлежит обработать типы. Когда компилятор считает, что часть данных имеет тип объединения (A | B), но вы уверены, что это
именно тип B, вы можете использовать "as(B)", чтобы заставить компилятор учесть его как тип 'B'.
Хотя это и выглядит очень знакомо, это вовсе не преобразование вроде метода 'to_', рассмотренного выше.
Далее в этой главе вы увидите, как проверять тип переменной, запуская различные участки кода в зависимости от результата проверки.
ВАШ ЧЕРЕД 1.
Преобразование типа среди целочисленных типов: Crystal чрезвычайно пунктуален относительно типов, но до сих пор вы использовали to_i только для преобразования каких-то чисел в целочисленные значения. Crystal предлагает, между прочим, множество различных типов целых чисел от 8-разрядных до 64-разрядных, как со знаком, так и без знака.
Как выполнять преобразования между этих типов? И выходит, что вы не делаете преобразования очевидным образом, но есть такие особо мудрёные биты, завершающие этот процесс. Компилятор Crystal позволяет смешивать целочисленные типы, но в результате по умолчанию будет использоваться тип первого числа, а не тип наибольшего используемого значения. Как по-вашему, какие результаты дадут два примера суммирования разнородных целочисленных:
p int8 = 1_i8 # 8-bit signed integer
p int16 = 16_i16 # 16-bit signed integer
p int32 = 132_i32 # 32-bit signed integer
p int64 = 164_i64 # 64-bit signed integer
p uns64 = 264_u64 # 64-bit unsigned integer
1) p int64 + int32 + uns64= ;
2) p int8 + int64= ;
_Обработка пользовательского ввода_
Вы можете сотворить неправильные действия с данными во время программирования, но еще более вероятно, что
пользователи будут вставлять неверные данные в ваши программы во время запроса на ввод. Создание живучего, устойчивого к ошибкам кода требует особой осторожности с пользовательским вводом данных. В следующем разделе ваша программа считывает входные данные; и вы поймете, как сделать ее устойчивой к ошибкам, возникающим во время выполнения.
Чтобы лучше понять, как Crystal работает с типами, давайте встроим код для чтения ввода в ту самую программу, посвященную валюте и ее относительным курсам обмена. Начнем с чтения целых чисел в массив. Вы можете найти полный исходный код в "getting_input.cr".
Консоль позволяет "подбирать" входные данные, но в Playground такого нет. Чтобы это сработало, вам нужно открыть терминал (ИКС) и напечатать:
crystal getting_input.cr.
Итак, пользователь отправляет числа, а вы ожидаете, что они будут меньше 256 ( в этом случае они
соответствуют типу 'Int8').
puts "Enter the numbers one by one, and end with an empty line:"
## "Вводите числа одно за другим, и закончите ввод пустой строкой:"
arr = [] of Int8
Вы начнете с пустого массива, так как вам надо обозначить тип "[] of Int8". Есть и другие способы, чтобы указать тип содержимого массива, например, заполнение массива данными при инициализации:
arr1 = [75, 42, 126]
typeof(arr1) # => Array(Int32)
Почему тип определился как 'Int32', когда все эти числа меньше 127? Int32 является
целочисленным типом, устанавливаемым по умолчанию. Если вы хотите установить тип Int8, допустим, в целях повышения производительности, вы должны прописать это явным образом, примерно так:
arr1 = [75_i8, 42_i8, 126_i8]
typeof(arr1) # => Array(Int8)
(Crystal предлагает суффиксы i8, i16, i32, i64 для положительных и отрицательных целых чисел, и u8, u16, u32 и u64 для беззнаковых целых чисел.) Вы читаете с консоли с помощью gets, которая возвращает все, что она читает, в виде String. Вы можете отобразить вводимые в консоли данные, воспользовавшись внутристроковой "вставкой".
puts "Enter a number:"
num = gets
p "You entered #{num}" # => "You entered 42"
@@@ErrOR=arr
Давайте добавим число в наш массив:
arr1 << num
Crystal не позволит вам сделать это:
Error: no overload matches 'Array(Int8)#<<' with type (String | Nil)
Теперь уже это должно восприниматься как нечто знакомое. Лучший способ выяснить, что пошло не так, это протестировать тип "num" (первый комментарий из предыдущей некорректной строки). Вы можете сделать это двумя способами:
p typeof(num) # => (String | Nil)
p num.class # => String
Вы видите, что Crystal делает различие между двумя понятиями:
Тип на стадии компиляции: какой тип видит компилятор, он задается с помощью "typeof";
Тип времени выполнения: какой тип объект имеет, когда программа уже запущена; и он определяется
методом "class".
Кроме того, nil не может быть добавлен к массиву целых чисел. Но почему компилятор считает, что ввод может быть нулем (nil)?
Ну ладно, вместо ввода числа введите "CTRL+D" и посмотрите, что происходит:
Тип данных для num это Nil! Но метод '<<' не может быть применен к нулю (nil). Вам следует остерегаться такого "нулевого" ввода.
Есть несколько способов сделать это. Поскольку nil противопоставлен "истине", самое простое тестирование условным оператором if:
if num
arr << num
end
Мы пока не закончили, потому что теперь мы получаем еще одну ошибку:
no overload matches 'Array(Int8)#<<' with type String
Компилятор теперь знает, что num соответствует строковому (String) типу, но массив может содержать только Int8. Настало время преобразовать входные данные с помощью to_i8:
if num
arr << num.to_i8
p arr # => for example: [42]
end
Теперь Crystal преобразует строку в 8-битное целое число со знаком (+-).
Даже если строка содержит числа (т.е. арабские цифры), то далее возможно несколько вариантов развития событий. Присутствие десятичной запятой приведет к ошибке, как и целые числа за пределами диапазона от -127 до 128.
Нечисловые знаки нарушат это преобразование, как именно будет рассмотрено ниже в этой главе. Кроме того,
if работает для одиночного пользовательского ввода. Мы не знаем, сколько чисел будет отправлено, поэтому нам потребуется цикл. И while подходит идеально: gets самолично возвращает значение, которое может быть проверено как состояние while. Если это значение не "ноль" или "ложь" (false), то while срабатывает нормально и добавляет содержимое "num" в массив.
while num = gets
arr << num.to_i8
p arr # => for example: [2, 3, 3, 5]
end
Запомните, только nil, false или нулевые указатели считаются в Crystal ложными в любом логическом показателе или в выражениях if, unless, while и until. Любое другое значение включая строку "false" и число 'ноль' распознается как истина при тестировании выражений.
___________________________________________
Сокращайте выражения.
Основанное на правилах истинности\ложности выражение "a ||= b", которое является сокращенной формой для
a || (a = b), используется для присвоения 'b' переменной 'a' лишь в том случае, когда 'a' равно нулю:
mem = nil
mem ||= 1
mem # 1
mem ||= "Crystal"
mem # 1
Это обычно используется в "memoization", что означает способность запечатлеть (задержать в памяти) результаты вычислений; в нашем случае возвращается значение 'a', когда 'a' не равно нулю, но в противном случае возвращается b.
_________________________________________
Возвращаясь к нашему вводу, давайте удалим все символы пробела с помощью strip, на всякий случай. Чтобы завершить цикл, проверим, вводит ли пользователь "stop" или просто ENTER, а затем прервем цикл (break):
while num = gets
num = num.strip # removes whitespace
if num == "" || num == "stop"
break
end
arr << num.to_i8
end
p arr # => for example: [78_i8, 56_i8, 12_i8]
_______________________________
read_line.
Если тестирование после gets показало, что ввод, не являясь нулевым (отличается от nil), не выглядит элегантным, вы можете также использовать метод "read_line". Пользуйтесь им упражнение read_line.cr показывает, как это сделать.
______________________________
_Сложим все вместе конвертация валют (версия 1)_
Давайте применим все это к текущему проекту. Наша цель выбрать базовую валюту, а затем вычислить примерное количество другой валюты соответственно первичной базе, например:
42 доллара США (USD) = 277.86846192 китайских юаней (CNY).
Какие здесь типы?
Коды валют, такие как "CNY" или "USD", очевидно, строки (String). А также и их полные имена, как "китайский юань" или "индийская рупия". Для совместного хранения кода и имени мы могли бы использовать хэш Hash(String, String), вот так:
CURRENCIES = {
"EUR" => "Euro",
"CAD" => "Canadian Dollar",
"CNY" => "Chinese Yuan"
}
Держите вашу базовую валюту в переменной base. Коэффициенты преобразования (отражающие колебания курсов валют) представляют собой десятичные числа. В этом примере, но не для случаев реальной разработки ПО, мы рекомендуется выбрать тип Float64. Таким образом, структуры данных (rates),которые содержат другие валюты и их
обменные курсы, сравниваемые с базовой валютой, будут хэшем Hash(String, Float64). Мы читаем в нашей базовой валюте (пускай это будет "USD" по умолчанию), а затем каждая строка подставлятся в другую валюту и её обменный курс.
Вот первый подход:
__types_and_control_flow/curr_conv1.cr__
puts "Enter the base currency, default is: USD"
base = gets
1. exit unless base
if base.strip == ""
base = "USD" # take "USD" as default value
end
puts "Enter the other currencies and their exchange rate,"
puts "like this: EUR - 0.84320536"
puts "Stop input by entering 'stop'"
rates = Hash(String, Float64).new
while input = gets
2. break unless input
input = input.strip
if input == "" || input == "stop"
puts "No more input: ok, we stop"
break
end
3. if !input.includes?(" - ")
puts("Wrong input format, use: CUR - 1.23456")
break
end
4. arr = input.split(" - ")
curr = arr[0].upcase
if curr.size != 3
puts "Currency code must be 3 characters"
break
end
rate = arr[1]
5. rates[curr] = rate.to_f
end
puts "base: #{base}"
puts "rates: #{rates}"
И он будет генерировать типичный вывод вроде этого:
<= Enter the base currency, default is: USD
=> CAD
<= Enter the other currencies and their exchange rate, like this:
EUR - 0.84320536
Stop input by entering 'stop'
=> EUR - 0.84
=> INR - 64.34
=> stop
<= No more input: ok, we stop
base: "CAD"
rates: {"EUR" => 0.84, "INR" => 64.34}
1. Здесь мы проверяем, что 'base' не равна 'nil' (нулю), потому что компилятор требует от нас такой проверки.
Если мы этого не сделаем, то получим ошибку компилятора: "undefined method strip for Nil" в следующей строке.
2. Мы "вырываемся" из цикла, если не имеется никаких входящих данных (т.е. ввод отсутствует).
3. Проверяем формат ввода. Содержит ли он символ " - "(дефис)?
4. Метод "Split" возвращает массив, в котором присутствует код валюты в качестве первого элемента и курс обмена в качестве второго.
Теперь пришло время проверить ввод, на предмет того каков его размер (3) или содержит ли код только буквы.
Попробуйте это самостоятельно: используйте метод each_char для типа "String" и метод "ascii_letter?" для типа Char. Заметьте, что мы "выскакиваем" из цикла при ошибке.
5. Здесь мы назначаем курс для определенной валюты, в хэше. Можно также использовать ярлык ||=;
тогда только первый введенный кэффициент курсовой стоимости будет сохранен.
Изначально курсовой коэффициент принадлежит строковому типу "String", но переменная "rates" должна сохранить это значение в виде Float64, так что компилятор требует преобразования. Отбросьте "to_f" и вы получите:
"no overload matches Hash(String, Float64)#[]= with types String, String"
Проверьте, что произойдет, если ввести ставку валютного курса, которая не является числом.
_Обработка исключений при некорректном вводе_
Конечно, здорово собрать внешний "ввод" для вашей программы, но иногда эти входные данные могут создать новые проблемы. Вы, наверное, знаете слоган "Мусор внутрь, мусор наружу": неправильный ввод может испортить всю дальнейшую обработку. Пользуясь нашим предыдущим примером, предположим, что пользователь вводит строку, такую
как "abc", когда программа ожидает целое число; иначе говоря, строку, которая не может быть преобразована в целое число. Что случится тогда?
Что-то ужасное: ошибка во время выполнения или исключительная ситуация (ЧП). Как и профессиональным программистам, вам надлежит избегать этого любой ценой.
<= Invalid Int8: abc (ArgumentError)
0x451477: *CallStack::unwind:Array(Pointer(Void)) at ??
0x465a92: to_i8 at /opt/crystal/src/string.cr 358:5
...
Вы всегда должны остерегаться опасного ввода. Лучший способ управиться с этим сделать простой тест "to_i8?", чтобы увидеть, начинается ли строка с допустимого числа. Если нет, тестовая проверка возвращает ноль (nil):
if num.to_i8?
arr << num.to_i8
else
puts "Input is not an integer."
end
Теперь вы получите замечательное сообщение об ошибке, и никаких отказов во время выполнения! (Как и в Ruby, в Crystal имеется соглашение, что методы с именами, оканчивающимися на "?", возвращают ноль (nil), если ничего не найдено.) Если ошибочность такова, что вам желательно остановить программу с помощью собственного сообщения об ошибке, используйте "raise":
if num.to_i8?
arr << num.to_i8
else
raise "Input is not an integer."
end
Применение этой техники к нашему конвертеру валют дает нам следующее:
__types_and_control_flow/curr_conv2.cr__
if rate.to_f?
rates[curr] = rate.to_f
else
puts "rate is no decimal number"
end
Используя элементарный "puts", программу не остановить. Альтернативой будет использование begin-rescue, механизма для обработки исключительных ситуаций:
__types_and_control_flow/getting_input_exception.cr__
puts "Enter the numbers one by one, and end with an empty line:"
arr = [] of Int8
while number = gets
number = number.strip # removes leading or trailing whitespace
if number == "" || number == "stop"
break
end
begin
arr << number.to_i8
rescue
puts "integer bigger than 255"
end
end
p arr # => for example: [78, 56, 12]
Инструкция rescue предвосхищает стандартную обработку исключительных ситуаций, так что программа не завершится аварийным отказом.
Выполнение обработчика исключений, подобного этому, влечет за собой резервирование оперативной памяти, и обычно это происходит медленно, как в ЯП Java или Ruby. Вам лучше попытаться избежать этого с помощью простых тестов, таких как "to_i?" или даже "to_f?", если возможно.
Если вам действительно необходимо использовать begin-rescue, минимизируйте количество кода между begin и rescue, чтобы избежать и чрезмерного потребления памяти, и потенциально сложной логики. Используйте исключения только для исключительных случаев, но не для ожидаемых событий!
Как и в Ruby, этот механизм может также иметь ветви "else" and "ensure". И в "rescue" вы можете использовать объект исключения "ex":
begin
# some dangerous code here
# possibly your own raise "..."
rescue ex
# execute if an exception is raised
p ex.message
else
# execute if an exception isn't raised
ensure
# this will always be executed
puts "Cleanup..."
end
_Сцепливание методов_
По-видимому, вам нравится красивый код в противном случае вы бы не продолжали чтение этой книги. Преобразование данных часто требует предсказуемой последовательности методов. Одна из прелестей языка Crystal в том, что вы можете соединять несколько методов в цепочку для создания более удобочитаемого кода. Метод может быть вызван для любого объекта, относительно которого он определен, и этот объект может быть результатом другого вызова какого-то метода.
Взгляните на следующие примеры:
__types_and_control_flow/chaining.cr__
result = (42..47).to_a # => [42, 43, 44, 45, 46, 47] (1)
.sort { |n, m| m <=> n } # => [47, 46, 45, 44, 43, 42] (2)
.reject { |n| n.odd? } # => [46, 44, 42] (3)
.map { |n| n * n } # => [2116, 1936, 1764] (4)
.select { |n| n % 4 == 0 } # => [2116, 1936, 1764] (5)
.tap { |arr| puts "#{arr.inspect}" } # => [2116, 1936, 1764] (6)
.sort! # => [1764, 1936, 2116] (7)
.any? { |num| num > 2000 } # => true (8)
1) Диапазон преобразуется в массив с "to_a".
2) Массив сортируется в обратном порядке (< = > это оператор сравнения).
3) Все нечетные числа исключаются с помощью "reject".
4) Функция "map" принимает другую функцию в качестве аргумента, чтобы возвести все элементы в квадрат.
5) Выбираются все числа, делящиеся на 4.
6) Метод "tap" касание передает объект в блок и возвращает его полезно для отладки или сжатия кода.
7) Массив сортирует себя.
8) Проверка функцией "any?" содержит ли массив число больше 2000.
Вы также имеете в своем распоряжении "reduce", который накапливает результат выполнения вычислений над элементами коллекции, в приведенном ниже случае сумму (sum) всех чисел массива arr:
__types_and_control_flow/chaining.cr__
sum = (42..47).to_a
.reduce(0) { |sum, num| sum + num }
# => 267 (= 42 + 43 +... + 47)
Метод reduce привносит функциональный подход в математические вычисления, производимые в Crystal.
Вы можете именовать свои методы в той самой манере, характерной для ЯП Ruby.
Методы, которые заканчиваются на "!" (вроде sort!) по условию изменяют объект, для которого они вызваны. (Обычно метод "pp!" показывает более полное выражение, чем "pp".)
Методы, которые заканчиваются на "?" (такие как "any?") задают логический вопрос и возвращают "true" или "false".
_Получение входных данных из аргументов командной строки_
Ввод внешних данных не всегда интерактивен. Быстрый запуск скомпилированного кода Crystal делает его отличным выбором для приложений командной строки. Что делать, если вам нужно предоставить программе данные или параметры из командной строки? Например, давайте прочитаем из консольного ввода данные о двух валютах и их относительном курсе (1 USD = 0.8432053 EUR):
$ crystal argv.cr USD EUR 0.84320536
Crystal, как и Ruby, имеет структуру ARGV верхнего уровня, чтобы управляться с подобными вещами:
__types_and_control_flow/argv.cr__
puts "Number of command line arguments: #{ARGV.size}" # => (1)
ARGV.each_with_index do |arg, i|
puts "Argument #{i}: #{arg}" # => (2)
end
p ARGV # => (3)
p "Executable name: #{PROGRAM_NAME}" # => (4)
p "Path to source file: #{__FILE__}" # => (5)
p "Folder of source file: #{__DIR__}" # => (6)
# (1) Количество аргументов командной строки: 3
# (2)
# Аргумент 0: USD
# Аргумент 1: EUR
# Аргумент 2: 0.84320536
# # (3) ["USD", "EUR", "0.84320536"]
# (4) "Имя исполняемого файла :/$ HOME/.cache/crystal/crystal-run-argv.tmp"
# или (4) "Имя исполняемого файла: ./argv"
# (5) "Путь к исходному файлу:
# / $HOME/crystal/Book/code/types_and_control_flow/argv.cr
# (6) "Папка исходного файла:
#/$HOME/crystal/Book/code/types_and_control_flow"
Вы можете запустить ARGV как массив с "each" или "each_with_index", и использовать do-block для всего, что вы хотите сделать с аргументами. Вот более реалистичный пример: если вы запускаете программу db_json, как тут:
$ ./db_json sqlite3:/ / db/sqlite3.db
, то ARGV[0] содержит строку подключения к базе данных.
Ваш код имеет доступ к имени исполняемого файла посредством константы PROGRAM_NAME. Это будет "./argv", если вы создали исполняемый файл с помощью команды (ИКС):
$ crystal build program.cr.
Когда вы запустите в командной строке что-то вроде
$ crystal program.cr
, то увидите, что исполняемый файл создается в специальной папке .cache/crystal. Здесь сохраняются все промежуточные файлы компилятора (такие как расширения макросов, файлы "линковщика" с расширением ".o", временные файлы среды Playground и тому подобное).
______________________________________
"__FILE__" и "__DIR__".
__FILE__ это специальная константа, содержащая полный путь к текущему имени файла. Константа __DIR__ сообщает директорию текущего файла.
_______________________________________
Вот версия нашего конвертера валют, которая считывает сумму в базовой валюте из командной строки, когда запускается программа, такая как $ ./curr_conv3 EUR:
__types_and_control_flow/curr_conv3.cr__
base = ARGV[0]?
base = "USD" unless base
_Использование строковых методов_
Ваши входные данные "прибывают" в виде строк, и большинство разновидностей данных могут быть представлены как строки. Работа со строками это "хлеб и масло" каждого программиста. Как и Ruby, язык Crystal хорошо экипирован для обработки строк. Вы уже использовали строки в ходе изучения этой книги, но существует больше подходов к созданию строк и больше способов работы с ними.
Crystal, как и Ruby, поддерживает мульти-линейные (многострочные) строки:
str = "What a beautiful mineral!"
str1 = "What a
beautiful mineral!" # multi-line string
Crystal также поддерживает использование обратной косой черты, чтобы отделаться от некоторых символов внутри строк. Если вам захочется создать такую же строку "str1", как приведена выше, сохраняя все содержимое в одной строке кода, можно также напечатать следующее:
str = "What a beautiful mineral!"
str1 = "What a \nbeautiful mineral!" # multi-line string
Как правило, возникает желание устранить двойные кавычки (\") или обратную косую черту, "бэкслэш" (\\). Вы можете также использовать числовые исключения. Обозначение "\u" сразу за шестнадцатеричным числом позволяет задать некий символ Юникода. Например ,\u2603 выдает юникодовского снеговика.
И когда у вас уже есть строки, Crystal предлагает вам много способов их обработки:
__types_and_control_flow/strings.cr__
curr1 = "US Dollar"
curr1[2..4] # => " Do"
curr1.reverse # => "ralloD SU"
curr1.size # => 9 # length or len do not exist
curr1.upcase # => "US DOLLAR"
curr1.capitalize # => "Us dollar"
curr1.includes? "la" # => true
curr1.count "l" # => 2
curr1.starts_with? "Us" # => false # case sensitive!
curr1.ends_with? "ar" # => true
curr1.index("a") # => 7
curr1.sub("ll", "l") # => "US Dolar"
curr1.gsub(/([aeiou])/, "*\\1*") # => "US D*o*ll*a*r"
curr2 = curr1.split("") # => ["U","S"," ","D","o","l","l","a","r"]
curr2.join("-") # => "U-S- -D-o-l-l-a-r"
Команды split и join являются типичными строковыми операторами:
split преобразует строку в массив мы использовали его в нашем конвертере валют в то время как join
выполняет обратную операцию, трансформацию массива в строку.
Строки создаются в так называемой "куче" (общедоступном массиве свободной памяти). Они неизменны, и это значит, что вы не можете изменять их содержимое напрямую:
s = "USD"
s[2] = 's' # => Error: undefined method '[]=' for String
Для эффективного использования ресурсов вам следует избегать создания избыточного количества строк. В следующем фрагменте to_s, "+" и строковая интерполяция поочередно создают строку в куче, хотя интерполяция справляется быстрее:
rate = 0.84320536
p "rate: " + rate.to_s # => "rate: 0.84320536"
# string interpolation is more efficient:
p "rate: #{rate}" # => "rate: 0.84320536"
Вы можете также использовать конструктор строк и метод "<<":
str = String.build do |io|
io << "rate: " << rate
end
p str # => "rate: 0.84320536"
По мере того как ваши проекты становятся все более сложными, эффективная обработка строк будет иметь еще большее значение.
ВАШ ЧЕРЕД 2.
1) Разбейте следующую строку названий минералов на массив, где все названия представлены заглавными буквами:
gold;topaz;apatite;wolframite;calcite;diamond. (золото; топаз; апатит; вольфрамит; кальцит; алмаз.)
(Подсказка: используйте метод "map", о котором вы узнали в разделе "Сцепливание методов").
2) Строки в сущности просто последовательности символов в кодировке UTF-8. Если вы любитель кошек, то можете сказать "hi /кракозябр/"(см. это задание в оригинал.eBook).
Выполните итерацию над этой строкой и напечатайте поочередно каждый символ, "кодовый указатель" (англ. codepoint определяет сегмент в диапазоне значениий) и байт. Для получения информации о доступных методах обратитесь к документации [https://crystal-lang.org/api/master/String.html]. Что за кодовый указатель Юникода соответствует китайскому символу для кошки, и сколько байт занимает этот символ?
________________________________________________________
Используем методы "each" ради производительности.
Используйте методы "each" для итерации: они намного быстрее простого цикла "while" или зацикливания вдоль строки путем индексации с помощью [].
_______________________________________________________
3) object_id ;
Покажите, что две постоянные строки с одинаковым значением ссылаются на один и тот же объект в "куче". (Подсказка: используйте метод object_id.) Почему же Crystal реализует это именно так?
Главная причина вот в чём: в тех случаях, когда два объекта имеют совпадающие object_id, оператор "==" возвращает значение "true", как и метод "same?". Что происходит, когда вы запрашиваете object_id для целочисленного или булева (Boolean) значения?
_Использование символов в качестве идентификаторов_
Строки, которые вы использовали в этой главе, являются гибкими, но эта гибкость требует немалой производительности. Предположим, вам нужно просеять огромный файл с миллионами минеральных объектов и дать каждому соответствующее название, по сути назначить идентификатор. Здесь вам лучше бы использовать символы, поскольку каждый экземпляр заданного символа является точно таким же объектом:
__types_and_control_flow/symbols.cr__
class Mineral
property name
def initialize(@name : Symbol)
end
end
mineral1 = Mineral.new(:talc)
mineral8547 = Mineral.new(:talc)
p mineral1.name
# => :talc
p mineral8547.name # => :talc
p :talc # => :talc
mineral8547.name = :gold
:gold.class # => Symbol
Поскольку символ хранится как уникальное значение Int32, его использование экономит память по сравнению с использованием строк. Символы также являются отличным выбором для хеш-ключей, вместо использования строкового типа (Strings).
Вот как мы можем использовать их в нашем валютообменном проекте:
__types_and_control_flow/curr_conv3.cr__
CURRENCIES = {
:EUR => "Euro",
:CAD => "Canadian Dollar",
:CNY => "Chinese Yuan",
:INR => "Indian Rupee",
:MXN => "Mexican Peso",
}
Чтобы передать больше смысла, имя символа также может закончиться на "?" или "!".
Все операторы также имеют свой собственный символ, например: "*" и "&".
ВАШ ЧЕРЕД 3.
Есть ли разница между символом и строкой, несущей такое же (точную копию) значение?
Обоснуйте ответ.
_Использование перечислений_
Иногда вам хочется ограничить функциональность кода не только символами, но и меньшим набором возможностей.
Это может оказаться весьма удобным группировать переменные в разрозненный ряд значений, таких как цвета светофора или указания компаса, в специфический тип.
Crystal поддерживает перечисления (Enums) для группирования связанных значений, в особенности, когда число отдельных значений не слишком велико:
__types_and_control_flow/enums.cr__
enum Direction
North # value 0
East # value 1
South # value 2
West # value 3
end
Direction::South # South
Direction::South.value # => 2
Перечисления хранятся внутри в виде целых чисел, но отображение их имен в коде делает его более понятным для людей. В структурированной Вселенной Crystal все перечисления наследуются от базового класса Enum, и вы можете даже задавать методы для них. У перечислений есть ещё одно важное преимущество: их проверяет компилятор. Но если вы неправильно написали символ, он будет сохранен как новый символ.
Direction::Eest # Error: undefined constant Direction::Eest
:gold
:goold
Дополнительная поддержка компилятора предоставляет преимущества перечислений в тех местах, где вы предпочли бы использовать символы в Ruby.
_Использование регулярных выражений_
Иногда требуется гибкость строк, но вам нужно еще и эффективно их обрабатывать. Поиск и замена внутри строк также может затратить много циклических операций. Регулярные выражения ("регексы" -- от англ. regex) могут значительно снизить затраты на обработку строк, благодаря высокооптимизированным моделям обработки данных.
Crystal использует синтаксис PCRE. Это реализовано как привязка к Cи-библиотеке PCRE. (Хотя Ruby использует другой механизм regex, он также использует синтаксис PCRE.) Шаблоны обычно создаются с помощью синтаксического литерала /pattern/.
Если у вас есть регекс, содержащий косую черту ("слэш"), которую вам нежелательно удалять, вы можете создать шаблон с помощью "%r(pattern)". Лишь после создания шаблона его можно применять к строкам.
Используйте сочетание "=~" или метод match, чтобы проверить, совпадают ли две строки и с какой начальной позиции.
__types_and_control_flow/regex.cr__
str = "carbonantimonygolddiamond"
pattern = /gold/
p str =~ pattern # => 14
В этом случае gold появляется в начальной позиции 14. Если же "gold" отсутствует в строке, проверка соответствия (match) вернет ноль (nil). Поскольку ноль несет значение "не истина", можно легко проверить совпадения с помощью конструкции "if materials =~pattern", запускающей выполнение некоторых задач, если есть совпадение, либо каких-то иных задач, если совпадение отсутствует.
Вы можете также извлечь дополнительные сведения о строке и ее связи с вашим регулярным выражением:
__types_and_control_flow/regex.cr__
materials = "carbonantimonygolddiamond"
pat = /(.+)gold(.+)/ # searches for gold
str = "carbonantimonygolddiamond"
str =~ pat # => 0
$1 # => "carbonantimony"
$2 # => "diamond"
str.match pat # =>
# <Regex::MatchData "carbonantimonygolddiamond"
# 1:"carbonantimony" 2:"diamond">
В этом примере начальное положение оглашено как "ноль" (и также истинно, что будет удобным для использования "if"), поскольку мы учитываем префикс (.+). Методы из класса Regex позволяют вам глубже заглянуть в результаты сопоставления.
Crystal позволяет использовать синтаксис интерполяции строк (#{}) в литералах регулярных выражений, давая вам потенциально мощные возможности на стадии выполнения кода. Конечно, это также позволяет вам случайно создавать исключения времени выполнения, если вашей вставкой интерполируется "грязный" синтаксис, так что будьте осторожны!
_Сложим все вместе конвертация валют (версия 2)_
В ходе дальнейшего развития нашего проекта мы сможем получать ежедневные курсы обмена валют из XML-файлов, размещенных на веб-сайте. Они содержат эту информацию в следующей форме:
<title> 1 USD = 0,74402487 GBP </title>
На данный момент в нашей программе известна базовая валюта "USD" и валюта обмена "GBP", и мы хотим получить обменный курс 0.74402487. Вот как мы можем сделать это:
__types_and_control_flow/curr_conv4.cr__
base_currency = "USD"
currency = "GBP"
line = "<title>1 USD = 0.74402487 GBP</title>" # exchange rate format
regex = {
:open => /<title>1 #{base_currency} = /,
:close => / #{currency}<\/title>/
}
rate = line.gsub(regex[:open], "").gsub(regex[:close], "").to_f
# => 0.74402487
Шаблон регулярного выражения /<title\>1 #{base_currency} = /
позволяет нам достичь тождества с первой частью строки "line" (перед дробным числом обменного курса).
Таким же образом шаблон /#{currency} <\/title\>/
соответствует закрывающей части. Последовательное применение метода "gsub" замещает эти шаблоны таким образом, что остается только строка обменного коэффициента вместе с "to_f", завершающим ее.
_После Хешей и Массивов: больше составных типов_
Составные типы, такие как массивы и хеши, с которыми мы познакомились в предыдущей главе, упрощают создание более сложных приложений. Crystal предлагает больше возможностей на базе других моделей данных, таких как кортежи и наборы. Кортежи группируют связанные значения для различных (но не обязательно) типов данных. Их можно создавать, записывая значения внутри {фиг.скобок}, или с помощью Tuple.new:
tpl = {42, "silver", 'C'}
tpl.class # => Tuple(Int32, String, Char)
a = Tuple.new(42, "silver", 'C')
Если вам интересно, как работают множественные назначения, например "n, m = 42, 43", то они используют кортежи. Вы можете получить доступ к элементам кортежа по индексу:
tpl[0] # => 42 (Int32)
tpl[1] # => "silver" (String)
tpl[2] # => 'C' (Char)
var = 89
tpl[var] # => Index out of bounds (IndexError)
tpl[var]? # => nil
Индекс проверяется, и в тех случаях, когда он неверен и вы использовали литералы возникает ошибка во время компиляции; простое несовпадение индекса ошибка индексирования (IndexError) во время выполнения. Как и для массивов, безопаснее будет использовать метод "[]?", который возвращает ноль в подобных случаях. Вы можете использовать все методы из класса Tuple, такие как "size, each, includes?, map" и так далее.
Поскольку кортежи создаются в стеке, они легковесны и подвижнее массивов, которым выделено место в общей "куче" ОП, особенно при размещении внутри циклов.
Использование ярлыка "||=" (синтаксис, которого мы представили ранее в параграфе "Обработка пользовательского ввода") отличный способ добавления пар данных:
h = {1 => 'A'}
h[3] ||= 'C'
h # => {1 => 'A', 3 => 'C'}
Мы могли бы использовать эту форму в нашем проекте валютной конверсии следующим образом:
rates[curr] ||= rate.to_f
Это означает, что валютный курс будет установлен первым значением; последующие значения не изменят его.
Если вы хотите больше смысла в вашей структуре данных, в Crystal также имеются именованные кортежи, которые подобны реестрам или протоколам, применяемым в некоторых ЯП:
tpl = {name: "Crystal", year: 2017} # NamedTuple(name: String, year: Int32)
tpl[:name] # => "Crystal" (String)
Поскольку кортежи имеют фиксированный размер и стековое размещение, они очень эффективны. Также компилятор может различать типы данных для каждой позиции. В этом отличие от массивов, которые могут меняться в размерах, поэтому следует отдавать предпочтение кортежам в случаях, когда размер множества постоянен и используются различные типы.
Для ключей символьного или строкового типа можно использовать именованные кортежи:
# Instead of {:key1 => 'a', :key2 => 'b'} you can use:
{key1: 'a', key2: 'b'}
# Instead of {"key1" => 'a', "key2" => 'b'} you can use:
{"key1": 'a', "key2": 'b'}
Если имена, индексы или порядок следования не важны, но необходимо сохранить уникальные значения, используйте набор:
set = Set{41, 42, 43} # => Set{41, 42, 43}
set.class # => Set(Int32)
# The above is equivalent to
set = Set(Int32).new
set << 42
set << 41
set << 43
set << 41
set # => Set{42, 41, 43}
ВАШ ЧЕРЕД 4.
Деструктуризация: Каковы значения переменных левее знака "="(присвоение)?
Это хороший способ вывести значения из массива или кортежа. Мы можем использовать его в нашей программе "Конвертер валют" для замены:
arr = input.split(" - ")
curr = arr[0]
rate = arr[1]
более компактной записью:
curr, rate = input.split(" - ")
Из предыдущих примеров ясно, что Массив, Кортеж, Хеш и Набор могут принимать различные типы данных в качестве своих элементов, в то время как их методы работают со всеми этими типами. Вы можете заметить, что они используют некий тип (T, или K, или V) в качестве параметра, например, Array (T), Hash (K, V) и так далее, где T может означать Int32, String, Char или любой другой тип. Другими словами: это общие (или универсальные) типы.
В главе 6 вы увидите, как определять собственные общие классы, которые способны работать с любым типом.
_Nil-типы_
Как нам объявить переменную определенного типа, но также могущую принять нулевое значение (nil); так называемую "nilable"? Вы видели раньше, что в определенных ситуациях Crystal присваивает объекту тип объединения во время компиляции.
Можно объявить тип nilable как тип объединения, но вы можете также объявить его тип, приписав в конце "?", как показано здесь:
__types_and_control_flow/type_nil.cr__
n : Int32 | Nil
n = 42
p typeof(n) # => Int32
a : Int32?
a = 42
p typeof(a) # => Int32
b : Int32?
b = nil
p typeof(b) # => Nil
a.try { p a + 1 } # => 43
b.try { p b + 1 } # => no error!
При использовании nil-типов метод "try" может оказаться очень кстати: компилятор не будет сигнализировать об ошибках тогда, когда значение может оказаться нулевым. Но имейте в виду, что если вы таким способом заблокируете компилятор, то дальше вы сами по себе!
Теперь пришло время работать с типами объединения самостоятельно:
ВАШ ЧЕРЕД 5.
1. Типы объединений.
Какому типу соответствует переменная mineral в следующем фрагменте кода? Напишите конструкцию if для отображения названия минерала в каждом возможном случае:
arr = ["anadite", "humite", "roselite"]
mineral = arr[4]?
2. Вот два пустых хеша, в которых присутствуют типы объединения. Каковы типы ключа и значения? Добавьте к ним пары "ключ-значение" так, чтобы использовать все типы, а также добавьте пару, которую нельзя скомпилировать.
h1 = Hash(Int32 | Char, Int32).new
h2 = {} of String | Int32 => Bool | String
3. Каков тип времени компиляции и тип времени выполнения у переменной var1, вызываемой после выражений if, следующих ниже, и почему это так?
if rand < 0.5
var1 = 7
end # branches that are not present return nil!
#
var1 = 42
if rand < 0.5
var1 = "Crystal"
end
#
var1 = 42
if rand < 0.7
var1 = "Crystal"
else
var1 = true
end
#
if rand < 0.5
var1 = 42
else
var1 = "Crystal"
end
var2 = var1 * 2
var3 = var1 - 1 # <= What does this return ?
_Контроль над исполнением кода и типами данных_
До сих пор типы, в сущности, были тем средством, что оградит наши программы от неприятностей. И хотя такая предосторожность довольно полезна, Crystal также предлагает много способов, чтобы вы могли написать код, ход выполнения которого обусловлен типами обрабатываемых данных. Это обеспечивает краткость кода, и вместе с тем распознавание множества различных видов информации.
В прошлой главе вы видели, какие управляющие конструкции образуют программную архитектуру, и вы, возможно, помните, что переменная, являющаяся условием запуска if-ветви, не должна соответствовать значению "nil" или "false". Кроме того, если имеется конструкция "if var1 && var2", то и var1, и var2 обязательно должны избегать нулевого значения. Эти теоретические основы полезны, но Crystal предлагает гораздо большие возможности.
Вместо явного использования конструкций if можно использовать более компактную троичную форму, как показано ниже:
var1 = if 1 > 2 3 else 4 end # => 4
# stated more concisely
var2 = 1 > 2 ? 3 : 4 #=> 4
Что за тип времени компиляции у переменной var1 в примере ниже?
# random choice between number and string
var1 = rand < 0.5 ? 42 : "Crystal" #=> 42 or "Crystal"
Нетрудно догадаться: тип объединения "Int32 | String". Это означает, что вызов метода abs для переменной var1 не работает:
typeof(var1) # => (Int32 | String)
var1.abs # => Error: undefined method 'abs' for String
Если вы твердо убеждены, что var1 является Int32, вы можете заставить и компилятор поверить в это, использовав "as", чтобы однозначно зафиксировать тип:
ivar1 = var1.as(Int32)
Но в данном случае нельзя знать наверняка, как же сработает "rand", и если var1 всё же окажется типом String, использование метода "as" приведет к ошибке во время выполнения:
cast from String to Int32 failed (TypeCastError)
Чтобы предотвратить это, используйте вариант "as?", который возвращает ноль вместо возникновения исключительной ситуации, так что вы можете проверить его с помощью "if ":
ivar1 = var1.as?(Int32) # => 42 or nil
# or retaining only the value 42 with if:
if ivar1 = var1.as?(Int32)
p ivar1.abs # => 42
end
Недвусмысленная проверка того факта, что var1 равен нулю, может быть выполнена следующим образом:
if var1.nil?
# here var1 is nil
end
Но это почти никогда не бывает необходимым. Просто используйте "if var1" с условным ветвлением "else" для обработки случая, когда выражение примет нулевое значение.
Вы можете проверить тип объекта с помощью метода "is_a?". Внутри конструкции "if" объект гарантированно будет соответствовать именно такому типу:
var1 = 42
if var1.is_a?(Number)
# here var1 is a Number, which can be integer or floating point
end
Если вы тестируете целый ряд типов, лучше использовать "case":
case var1
when Number
p var1 + 42
when String
p "we have a string"
else
p "var1 is not a number or a string"
end
# => 84
case это очень универсальная конструкция: она может использовать все виды переменных, включая символы, перечисления и кортежи. Она не только может тестировать тип переменной, но также может вызывать метод для нее, как показано в этом фрагменте:
num = 42
case num
when .even?
puts "you have an even number"
when .odd?
puts "you have an odd number"
end
# => you have an even number
Использование переменной кортежа вместе с "case" делает возможным сопоставление некоторых шаблонов. FizzBuzz является обыденной задачей в программировании, в которой вас просят напечатать "FizzBuzz", если число делится на 3 и 5, "Fizz" если делится на 3, и "Buzz" если делится на 5, считая от 1 до
100. И "case" позволяет написать компактную версию решения:
(1..100).each do |i|
case {i % 3, i % 5}
when {0, 0}
puts "FizzBuzz"
when {0, _}
puts "Fizz"
when {_, 0}
puts "Buzz"
else
puts i
end
end
% операция по модулю, которая возвращает остаток от целочисленного деления. Обратите внимание, что вы можете использовать один (или несколько) символов '_' при тестировании кортежа, заменяя им значение, которое не столь важно.
Другой способ разобраться с типами проверить, может ли содержимое переменной отвечать на отправленный методом вызов. Безопасное тестирование выполняется посредством "responds_to?":
var1 = "Crystal"
if var1.responds_to?(:abs) # false in this case
var1.abs
end
ВАШ ЧЕРЕД 6.
1) Какое значение у var1 после этого "if"?
if var1 = 1
puts "inside if"
var1 = 2
puts var1
end
Здесь ход выполнения отличается от характерного для многих других языков программирования!
2) Какой вывод порождает следующая программа?
if (num = 9) < 0
puts "#{num}, is negative"
elsif num < 10
puts "#{num}, has 1 digit"
else
puts "#{num}, has multiple digits"
end
3) Проверьте этот фрагмент кода и объясните его поведение. Вы видите способ улучшить его?
begin
a = 4 + 6
rescue
puts "an ex occurred"
ensure
puts a + 42
end
Подсказка: компилятор учитывает вероятность возникновения исключительной ситуации.
_Заключение_
В этой главе мы глубже изучили некоторые базовые вещи Кристального Программирования, такие как символы,
кортежи и оператор "case". Для Руби-кодера, возможно, это все не в новинку, но вы теперь лучше знакомы с компилятором Crystal и с тем, как он использует типы объединений, чтобы сделать ваш код более безопасным.
Объединенные (или комбинированные) типы составляют краеугольный камень типовой системы Кристалла. Вооружившись этим пониманием, давайте приступим к изучению методов и процедур (а также посмотрим, как здорово Crystal использует перегрузку) в нашей следующей главе.
Глава 4.
Организация кода в методах и процедурах.
Хотя вы можете писать рабочий код строка за строкой, не структурируя его, вы скорее всего не добьетесь многого до тех пор, пока не будете способны повторно использовать код и хранить логику более мудреными способами.
Методы (иногда называемые функциями) позволяют группировать код в фрагменты, и вы даете им имена, чтобы иметь возможность вызывать их. В этой главе вы увидите подход Кристалла к методам. Он схож с используемым в Ruby, хотя подход к типам данных в Crystal создает некоторые различия на пути к функциональному и читаемому коду.
Если вы чувствуете необходимость, просмотрите раздел "Использование методов", чтобы повторить введение в методы.
Поскольку в Crystal все является объектом, здесь есть методы "на все случаи жизни", даже для простых значений, таких как Int32 и Char. Операторы вроде / или <= также являются методами. Все, о чем мы поговорим в
этой главе, также применяется к тем методам, что используются в классах (см. главу 5, "Использование классов и структур").
К счастью, многое из того, что вы узнали о методах в других ЯП, применимо и здесь мы рассмотрим основы в этой главе, но стоит отметить некоторые вещи, которые могут удивить вас, если вы перешли из других языков программирования. Методы Crystal не живут внутри класса, в отличие от таких ЯП, как Java или C#. Вы можете также определить их (как мы уже много раз видели в предыдущих главах) в начале "исходников" вашей программы.
Это также распространяется на группирование связанных методов в модуле (см. Использование модулей в качестве пространств имен) для последующего встраивания в классы.
Еще Crystal отличается широким кругозором. В отличие от языков Ruby или Python, Crystal не имеет глобальных
переменных вообще. Переменные, определяемые локально в методе, не будут видны вне метода. Обратное также верно в Crystal: переменные, которые вы определяете вне метода, не будут видимы и внутри этого метода тоже.
__methods_and_procs/methods.cr__
x = 1
def add(y)
x + y # Error: undefined local variable or method 'x'
end
add(2)
_Передача аргументов_
Иногда вы будете создавать методы, которые можно вызывать, не передавая никакой информации, но чаще
программисту хочется, чтобы методы выполняли нечто особое, какую-то специфическую задачу. При вызове метода вы можете предоставить один или несколько аргументов, которые служат для предачи и последующей обработки методом информации. Если используете аргументы в определении метода, вам необходимо перечислить их в скобках, как метод add в следующем примере:
__methods_and_procs/methods.cr__
def add(x, y)
# return x + y # return is optional
x + y
end
1) add(2, 3) # => 5
2) add 2, 3 # => 5
3) add(1.0, 3.14) # => 4.14
4) add("Hello ", "Crystal") # => "Hello Crystal"
5) add(42, " times") # => Error: no overload matches 'Int32#+' with type String
6) add # => Error: wrong number of arguments for 'add'
# (given 0, expected 2)
(Вы можете уточнить это в разделе "Больше безопасности посредством Типов".)
При вызове метода можно опустить скобки (), как это сделано в строке 2. Это здорово в тех случаях, когда
у вас есть лишь несколько аргументов, но код быстро становится менее читаемым в более сложных ситуациях. В нашей книге используется бесскобочная запись, где это уместно.
Из строк 1, 3 и 4 вы видите, что мы можем с удовольствием использовать утиную типизацию, как в любом динамическом языке. В утиной типизации вы позволяете ЯП автоматически определять тип объекта на основе его содержимого. Можно использовать метод add для всех объектов "x" и "y" при условии, что их "Type" имеет суммирующий метод, который является подходящим вариантом здесь для чисел и строк.
Как вы увидите вскоре, вы можете указать типы, но Crystal обязуется никогда не вводить обязательной типизации аргументов, в отличие от большинства статически типизированных языков. Утиная типизация весьма гибка и делает ваш код пригодным для более широкого применения. Но иногда переданные параметры не подходят (как в строке 5).
Еще Crystal неустанно сокращает вероятность программных сбоев, возникающих на стадии выполнения, посредством упреждающих ошибок компилятора. А если вы (как в строке 6) передаете недостаточно аргументов, возникает другая ошибка компилятора: [неправильное количество аргументов для "add" (задано 0 аргументов, ожидается 2)].
В разделе "Больше безопасности посредством Типов" в сценариях overloading.cr, которые также используют метод add, мы выяснили, что Crystal позволяет типизировать аргументы. (Возможно, будет нелишним еще потренироваться с ними, прежде чем двигаться дальше.)
____________________________________________
Подвох.
В следующем фрагменте только аргумент "y" объявляется, как принадлежащий типу Int, но не "x":
def add(x, y : Int)
x + y
end
add 3, 4 # 7
add 2.14, 7 # 9.14
Во втором вызове метода add параметр "x" не соответствует типу Int. Если "x" и "y" оба должны соответствовать
типу Int, определите метод так: add (x: Int, y: Int).
___________________________________________
Crystal предлагает несколько более затейливых вариантов работы с аргументами. Вы можете указать для аргументов значения по умолчанию (default) в конце списка параметров на тот случай, когда параметр для этого аргумента не задан, как в этом методе show:
methods_and_procs/methods.cr__
def show(x, y = 1, z = 2, w = 3)
"x: #{x}, y: #{y}, z: #{z}, w: #{w}"
end
show 10 # => "x: 10, y: 1, z: 2, w: 3"
show 10, 10 # => "x: 10, y: 10, z: 2, w: 3"
show 10, 30, 2, 3 # => "x: 10, y: 30, z: 2, w: 3"
show 10, 20 # => "x: 10, y: 20, z: 2, w: 3"
show 10, z: 10 # => "x: 10, y: 1, z: 10, w: 3"
show 10, w: 30, y: 2, z: 3 # => "x: 10, y: 2, z: 3, w: 30"
show y: 10, x: 20 # => "x: 20, y: 10, z: 2, w: 3"
show y: 10 # Error, missing argument: x
Как можно увидеть во второй серии вызовов "show" в вышеприведенном коде, вы можете указать аргументы по имени при вызове метода. Использование именованных аргументов подразумевает, что мы не привязаны к их порядку.
Код также гораздо более читаем, если мы используем описательные имена, как в следующем примере, где
мы создаем клиент авторизации, которому необходимы значения для аргументов "host", "client_id", и "client_secret":
require "oauth2"
client = OAuth2::Client.new(
host: "martian1",
client_id: "7594",
client_secret: "W*GDFUY75HSVS#@!"
)
_Возврат значений_
Метод возвращает результирующее значение своего последнего вызова, поэтому нет необходимости напрямую запрашивать это значение или объявлять его тип. Однако, при необходимости документирования или непосредственного управления этим возвращаемым типом, вы можете особо указать тип, как в этом примере:
__methods_and_procs/methods.cr__
def typed_method : Array(Int32)
(42..47).to_a.select { |n| n % 4 == 0 }
end
typed_method # => [44]
Также можно явно использовать Nil в качестве возвращаемого типа, если необходимо указать, что это "сопутствующий побочный метод" (важность имеет лишь то, что он делает, а не значение, которое он возвращает).
Разработчикам ПО, использующим ваш метод, нужно быть готовым к этому или, по крайней мере, это не должно быть
сюрпризом.
Если необходимо вернуть несколько значений, их можно упаковать в кортеж или массив. Как распаковать
значение с помощью деструкции (вернитесь к примеру Ваш черёд 4, "Извлечение данных из структуры вроде массива" ):
__methods_and_procs/methods.cr__
# Multiple return values
def triple_and_array(s)
{s * 3, s.split}
end
# unpacking:
ret = triple_and_array("42") # => {"424242", ["42"]}
ret[0] # => "424242"
ret[1] # => ["42"]
# or:
num, arr = triple_and_array("gold")
num # => "goldgoldgold"
arr # => ["gold"]
ИСПОЛЬЗОВАНИЕ АРГУМЕНТА SPLAT *
Если вы хотите, чтобы ваш метод принимал переменное количество параметров, то вы не можете дать имя
каждому из ваших аргументов. Вместо этого используйте один аргумент с префиксом *, чтобы создать
совместно вызываемый сплат-аргумент. И тогда метод может принимать от нуля до неограниченного числа параметров, потому что все они преобразованы в один кортеж.
К примеру, вам требуется рассчитать зарплаты для неизвестного числа сотрудников:
__methods_and_procs/methods.cr__
def salaries(*employees)
employees.each do |emp|
# calculate salary
puts "#{emp}'s salary is: 2500"
end
end
salaries() # =>
salaries("Jones") # => Jones's salary is: 2500
salaries("Baudelaire", "Rogers", "Gandhi")
# =>
# Baudelaire's salary is: 2500
# Rogers's salary is: 2500
# Gandhi's salary is: 2500
Использование сплат-аргумента не означает, что вы сами должны разбираться со всеми аргументами. Crystal предлагает другой способ, используя лишь одиночный *, чтобы указать, что все аргументы после символа "*" нужно снабдить именными метками. Такой подход работает следующим образом:
def display(n, *, height, width)
"The shape has height #{height} and width #{width}"
end
display 3, height: 2, width: 5
# => "The shape has height 2 and width 5"
Можно даже присвоить именованному параметру дополнительное название (вроде домашнего прозвища) для использования в "теле" метода, чтобы ваш код читался более естественно:
__methods_and_procs/methods.cr__
def increment(number, by value)
number + value
end
p increment(10, by: 10) # => 20
Другой классической ситуацией является вульгарная проблема создания строки значений, разделенных
неким символом, но так, чтобы этот символ не мог появиться после финального значения. Например, мы хотим "1-2-3 "но не" 1-2-3- ".
Приведенное ниже решение принимает изменчивое количество значений args, размечая их разделителем вида "with joiner" (joiner внутреннее имя, используемое лишь внутри метода), а "with" обычное имя, используемое при вызове метода для передачи значения параметра:
__methods_and_procs/methods.cr__
def join(*args, with joiner)
String.build do |str|
args.each_with_index do |arg, index|
str << joiner if index > 0
str << arg
end
end
end
join 1, 2, 3, with: "-" # => "1-2-3"
join 1, 2, 3, 4, 5, with: "*" # => "1*2*3*4*5"
ВАШ ЧЕРЕД 1.
1. Итак: Напишите метод "total" для вычисления суммы произвольного количества чисел.
Измените "total" так, чтобы суммирование начиналось с предопределенного значения.
2. Сплат-кортеж:
Вы также можете распаковать кортеж (который мы уже исследовали в упражнении "Ваш черёд 4") прямиком в аргументы метода. Предположим, что вы хотите сделать это:
def add(n, m)
n + m
end
tpl = {42, 108}
add tpl
Это работает? Можете ли вы объяснить это?
Вам следует назвать это так: добавление *tpl, которое иногда называется раскладыванием кортежа. Если tpl является именованным кортежом и вы хотите использовать именованные аргументы, вам придется применить двойной сплат "**". Пробуйте этот прием.
Другой трюк состоит в использовании двойного сплат-аргумента "**argument" для захвата произвольного числа именованных параметров в именованный кортеж.
_Работа с выдачей, процедурами и блоками_
Хотя именованные методы обычно являются ядром приложения, иногда вам захочется иметь возможность более гибкого манипулирования логикой программы. Процедуры позволяют поместить логику, включая методы, внутрь схожих с переменными структур, которые можно пропускать сквозь вашу программу и вызывать напрямую или через ключевое слово "yield" (выдача). Подобно методам, процедуры (Procs) могут принимать аргументы и возвращать значения.
Это хлеб с маслом для Руби-кодера, но если вы не знаете Ruby, то сначала эти факты могут слегка сконфузить. Для незнакомых с ЯП Ruby естественным путем к усвоению темы "Using Procs" может стать более пристальная фокусировка на таких вещах в начале углубленного изучения Кристалла.
Блоки позволяют повторно использовать код без помощи формальных методов, как только вы разберетесь, как же работает синтаксис. (Вам уже случалось применять их, но они имеют ряд дополнительных возможностей.) Вы можете сгруппировать одну или несколько строчек программного кода в блок кода, охватив их фигурными скобками {} или поместив между инструкциями "do" и "end". Они не являются методами, но тоже снабжены именами для обеспечения возможности указания и вызова.
Как и в Ruby, эти кодовые блоки можно также использовать в качестве параметров при вызове метода. Например, метод по имени "testing" вы можете вызвать с помощью кодового блока:
testing do
puts "in code block"
end
Если вы создали метод с точно таким же именем, "yield" будет вызывать тот самый блок кода:
def testing
puts "at top of method"
yield
puts "back inside method"
yield
puts "at end of method"
end
# =>
# at top of method
# in code block
# back inside method
# in code block
# at end of method
Когда вы вызываете эти блоки с помощью "yield", компилятор Crystal всегда помещает их в строки из соображений производительности. Они не вызывают переход к отдельной функции во время выполнения программного кода. Можно использовать блоки для удобочитаемости или повторного использования кода, что не окажет негативного влияния на производительность. Этот механизм может пригодиться, например, если вы хотите провести итерацию коллекции или предоставить особый пользовательский алгоритм.
Ключевое слово "yield" действует как вызов метода, так что вы можете передать ему один или несколько параметров, добавив их в блок do как "do |n|", вот так:
__methods_and_procs/procs.cr__
def testing
puts "at top of method"
yield 1
puts "back inside method"
yield 2
puts "at end of method"
end
testing do |n|
puts "in code block #{n}"
end
# =>
# at top of method
# in code block 1
# back inside method
# in code block 2
# at end of method
Вы можете использовать break внутри блока, чтобы раньше выйти из метода; ключевое слово next выполняет преждевременный выход из блока, но не из метода.
Является ли блок кода тоже объектом? Технически, это неверно. Но вы можете "захватить" блок, если
напишете что-то вроде "def testing(&block)". Это приведет к созданию процедурного объекта "Proc" для блока (который вы определили ранее как "testing do ..."), который был передан. Это позволяет рассматривать его как обычный объект и включать в аргументы или возвращаемые значения. Но только вместо "yield" необходимо вызывать метод "Proc". В этом случае вам следует использовать "block.call".
__methods_and_procs/procs.cr__
def testing(&block)
puts "at top of method"
block.call
puts "back inside method"
block.call
puts "at end of method"
end
testing do
puts "in code block"
end
# =>
# at top of method
# in code block
# back inside method
# in code block
# at end of method
Если код, который требуется выполнить внутри блока кода, является кратким, вы можете предпочесть запись блока с помощью {}. Это приближает логику кодового блока к точке вызова и облегчает написание краткого и удобочитаемого кода для повторяющихся логических конструкций.
__methods_and_procs/procs.cr__
langs = %w[Java Go Crystal]
langs.map { |lang| lang.upcase } # => ["JAVA", "GO", "CRYSTAL"]
Этот блок имеет только один аргумент с единственным методом, включенным в него. В таком случае Crystal позволяет вам применить немного чудесного синтаксического сахара (в отличие от Ruby). В следующем примере метод map применяется к каждой строке в langs, повышая регистр буквенных символов. Обе линии вывода демонстрируют один и тот же результат. Вы можете видеть, что синтаксис блока и переменной замещен оператором "&".
__methods_and_procs/procs.cr__
langs = %w[Java Go Crystal]
langs.map { |lang| lang.upcase } # => ["JAVA", "GO", "CRYSTAL"]
langs.map &.upcase # => ["JAVA", "GO", "CRYSTAL"]
Метод так же успешно может сыграть роль оператора, и он тоже может принимать аргументы, таким вот образом:
__methods_and_procs/procs.cr__
nums = [42, 43, 44]
nums.map { |num| num + 2 } # (1) => [44, 45, 46]
nums.map &.+(2) # (2) => [44, 45, 46]
Аналогичным образом вы можете еще и скреплять методы (см. параграф "Сцепливание методов").
_Сложим все вместе конвертация валют (версия 3)_
Давайте посмотрим, как мы могли бы вывести несколько валютных конверсий, используя блок кода:
__methods_and_procs/curr_conv5.cr__
base = "USD"
rates = {
"EUR" => 0.84320536,
"CAD" => 1.26761115,
"CNY" => 6.61591576,
}
full_names = {
"EUR" => "Euro",
"CAD" => "Canadian Dollar",
"CNY" => "Chinese Yuan",
}
# How much is 1 US Dollar?
output = "1 #{base} = \n" + # (1)
rates.keys.map do |curr| # (2)
"\t#{rates[curr]} #{full_names[curr]}s (#{curr})"
end.join("\n") # (3)
puts output
# 1 USD =
# 0.84320536 Euros (EUR)
# 1.26761115 Canadian Dollars (CAD)
# 6.61591576 Chinese Yuans (CNY)
puts
# How much is 42 US Dollars?
amount = 42
output = "#{amount} #{base} = \n" +
rates.keys.map do |curr|
temp = sprintf("%3.2f", amount * rates[curr]) # (4)
"\t#{temp} #{full_names[curr]}s (#{curr})"
end.join("\n")
puts output
# 42 USD =
# 35.41 Euros (EUR)
# 53.24 Canadian Dollars (CAD)
# 277.87 Chinese Yuans (CNY)
(1) Начните со строки, где написано "output=".
(2) Добавьте к этой строке итог вычислений в блоке кода. Это итерация проведенная над массивом со всеми валютами, переданными через "rates.keys". Для каждой валюты "map" принимает блок кода, который выполняет вывод строки. Метод map создает новый массив с выводимыми строками для исходного массива (rates) с валютами.
(3) Соедините все элементы этого массива в одну общую строку с разделением элементов пустыми строками.
(4) Округлите выводимые числа до двух цифр после запятой, используя sprintf и строки формата "%3.2f".
ВАШ ЧЕРЕД 2.
Синтаксический сахар:
Вычислите третью степень (возведение в куб) чисел от 1 до 10. (Подсказка: используйте **.)
Сортируйте массив langs из предыдущего примера по размеру строк (Подсказка: использовать метод sort_by).
Выполните обратную сортировку всех букв из названий языков в "langs". Результат должен быть: ["vaaJ", "oG", "ytsrlaC"]. (Подсказка: Используйте формирование цепочки.)
ИСПОЛЬЗОВАНИЕ PROCS.
В предыдущем разделе вы видели, что захваченный блок кода (как вы помните &block) фактически является объектом, называемым процедурой ("Proc"), или лямбда-функцией, или анонимной функцией; вы можете думать о "Proc" как о функциональном объекте с методом "call". Процедуры можно создавать с помощью нескольких различных подходов:
1) Двухсимвольная запись "->" позволяет создать литерал процедуры. Здесь вы запишете свой метод add для двух чисел как процедуру:
__methods_and_procs/procs.cr__
fn = ->(n : Int32, m : Int32) { n + m }
typeof(fn) # => Proc(Int32, Int32, Int32)
fn.call(42, 108) # => 150
Имя процедуры находится слева от знака "=". Затем оператор "->" указывает список параметров . Предупреждаем, что здесь вам необходимо использовать типы. Дальше следует фрагмент кода между фигурных скобок {}. Во второй строке компилятор делает вывод относительно числового типа, возвращаемого процедурой.
2) Вы можете создать процедуру из существующего метода "add", используя аналогичную запись:
__methods_and_procs/procs.cr__
def add(n, m)
n + m
end
fn = ->add(Int32, Int32)
fn.call(42, 108) # => 150
3) Тот факт, что Proc является классом в стандартной библиотеке Кристалла, позволяет вам также использовать метод new:
__methods_and_procs/procs.cr__
fn = Proc(Int32, Int32, Int32).new { |n, m| n + m }
fn.call(42, 108) # => 150
Как и во многих других языках, процедуры могут работать как "замыкатели", здесь подразумевается, что они могут перехватить область контроля над переменными, которые использовались в момент создания этой процедуры. Вот как это работает на примере с процедурными литералами:
__methods_and_procs/procs.cr__
n = 42
fn = ->(m : Int32) { n + m }
fn.call(108) # => 150
n = 20
fn.call(108) # => 128
Нетрудно заметить значение "n" известно внутри процедуры, и оно так же верно опознается, когда переменной "n" придают иное значение. Захваченные блоки имеют схожее с переменными поведение:
__methods_and_procs/procs.cr__
def capture(&block : Int32 -> Int32)
block
end
n = 42
proc = capture { |m| n + m }
proc.call(108) # => 150
n = 20
proc.call(108) # => 128
Это также демонстрирует, что захваченный блок может сопровождаться примечанием, описывающим типы данных: к примеру "Int32 -> Int32" означает, что число "Int32" будет принято в качестве параметра и возвращен будет тип "Int32". Чтобы взять два таких параметра, следует написать: "Int32, Int32 -> Int32".
ВАШ ЧЕРЕД 3.
ReturnProc: Напишите метод возрастания (incr), который инициализирует счетчик цифрой "ноль" и возвращает инкрементное значение посредством процедуры "Proc". Напечатайте его тип и вызовите его несколько раз. Что происходит, когда вы даете методу новое имя и вызываете его с этим именем несколько раз?
Ваша голова не идет кругом? Всевозможные синтаксические комбинации блоков кода и процедур иногда кажутся чрезвычайно избыточными. Но существуют и весьма находчивые решения, такие как хранение процедуры в переменной экземпляра для имитации обработчика событий (см. раздел "Определение обратных вызовов").
Вознаградите себя чашкой кофе или хорошим фильмом!
_Перегрузка и многократная отправка_
Crystal позволяет вам использовать разные версии какого-либо метода под одним именем. Это называется перегрузкой. Параграф "Больше безопасности с помощью Типов" разъясняет, как можно использовать типы аргументов для ограничения методов.
Код, написанный без применения типов, может быть более универсальным и повторно используемым, но недвусмысленное указание типов может сделать исходный код гораздо более безопасным. Понимание деталей (вроде количества аргументов, их имен и способности метода принимать блок кода) позволяет вам изобретать различные версии метода, которые будут работать с таким контекстом или аргументами, которые слегка отличаются от ожидаемых.
Компилятор отдельно создает исполняемый код для каждой версии метода. И если обнаруживаются запросы к общему имени такого метода, компилятор присоединяет правильную версию кода, ориентируясь на совпадения типов для успешной передачи параметров. Вы можете добиться специализации кода, опираясь на типы данных, и при этом избежать каких-либо проверок на стадии запуска на исполнение, что значительно повысит скорость выполнения вашего кода.
Чтобы увидеть это своими глазами, попробуйте сделать следующее. Скопируйте все методы "add" и тестовые работы из файла "overloading1234.cr" (см. "Больше безопасности с помощью Типов"). Затем напишите и проверьте в работе метод add, который принимает:
Число и логическое выражение, и возвращает это самое число, если логическое значение будет "true"; в противном случае будет возвращена цифра "ноль".
Две строки, преобразует их в целые числа, а затем суммирует их (т.е. результирующим значением будет какое-то целое число).
Что произойдет, если выполнить программный код вышеупомянутых тестовых заданий? Используйте то, что вы усвоили из раздела "Обработка пользовательского ввода", чтобы разобраться с этим вопросом.
Тут есть о чем поразмыслить. После определения метода [add (x:String, y:String)] следует [исходный код, разрешающий тестовую ситуацию], напишем [add("Hello", "Crystal")], и теперь этот новый метод будет применяться вместо традиционного метода "add".
Причина заключается в том, что типы данных в этом вызове лучше соответствуют новому методу. Но теперь "to_i" запнётся на этих аргументах, что приведет к исключительной ситуации (exception). Вы можете защитить свой новый метод от подобных сбоев:
def add(x : String, y : String)
if x.to_i? && y.to_i?
add x, y # calls version 1
end
end
Но тогда метод add("Hello", "Crystal") вернет нулевое значение (nil), так что вам потребуется условное ветвление (else) для компенсации этой помехи:
def add(x : String, y : String)
if x.to_i? && y.to_i?
add x, y # calls version 1
else
x + y
end
end
Ниже исходный код этого урока приведен полностью:
__methods_and_procs/overloading.cr__
# version 1:
def add(x : Int, y : Int)
x + y
end
# version 2:
def add(x : Number, y : Number)
x + y
end
# version 3:
def add(x : Number, y : String)
x.to_s + y # convert a number to a string with to_s method
end
# version 4:
def add(x, y)
x + y
end
# new methods:
# version 5:
def add(x : Number, y : Bool)
y ? x : 0
end
# version 6:
def add(x : String, y : String)
if x.to_i? && y.to_i?
add x.to_i, y.to_i # calls version 1
else
x + y
end
end
add(2, 3) # => 5
add(1.0, 3.14) # => 4.14
add("Hello ", "Crystal") # => "Hello Crystal"
add(42, " times") # => "42 times"
add 5, true # => 5
add 13, false # => 0
add("12", "13") # => 25
Вы можете также справиться с подобной задачей, используя множественные типы (Unions) для возвращаемого значения. В новом сценарии сделайте метод add(x, y), который возвращает нулевое значение (nil), если [y равен 0], в противном случае будет возвращена сумма [x+y].
Испытайте метод, записав "n = add(2, 3)". Что это за тип? Что происходит, когда вы пытаетесь выполнить n + 10?
Добавьте проверку if, чтобы предотвратить это. Когда аргументы cоответствуют множественным типам данных, компилятор не знает, какую версию метода следует вызвать.
Только на стадии запуска выяснится реальный тип аргументов, и тогда вызовется правильная версия метода. Это называется многократной отправкой, или мульти-передачей (более точное название "диспетчерское распределение"). Но это не слишком снижает скорость, потому что версии для всех возможных типов были скомпилированы загодя. Вы можете увидеть это ниже:
__methods_and_procs/overloading3.cr__
def display(x : Number) # overloading 1
puts "#{x} is a number"
end
def display(x : String) # overloading 2
puts "#{x} is a string"
end
n = 42
display n # => 42 is a number
str = "magic"
display str # => magic is a string
r = rand < 0.5 ? n : str
typeof(r) # => (Int32 | String)
display r
В первом методе "display" используется перегрузка вызова (1). В следующем тоже (# overloading 2), оба варианта рассматриваются во время компиляции. Но для третьего вызова ситуация иная:
на стадии компиляции тип переменной "r" соответствует (Int32 | String). Какой именно тип примет "r" станет известно только после запуска на исполнение (из-за "rand"), и только тогда можно определить, какой из методов display следует вызвать!
Вы можете также использовать ограничения типа для "splat-аргумента", как показано ниже:
__methods_and_procs/overloading3.cr__
def method1(*args : Int32)
end
def method1(*args : String)
end
method1 41, 42, 43 # OK, invokes first overload
method1 "Crystal", "Ruby", "Go" # OK, invokes second overload
method1 1, 2, "No"
# Error: no overload matches 'method1' with types Int32, Int32, String
method1() # Error: no overload matches 'method1'
Обратите внимание, что последние два вызова method1 были отклонены компилятором.
_Использование укороченного синтаксиса для обработки исключений_
Помните, как мы получали пользовательский ввод данных, заполняли ими массив и предотвращали вероятную исключительную ситуацию с помощью "begin/rescue" в параграфе "Обработка пользовательского ввода"? Вы можете написать такой код гораздо более лаконично. Во-первых, давайте перепишем этот код, используя несколько методов, чтобы сделать его более структурным:
__methods_and_procs/exceptions.cr__
puts "Enter the numbers one by one, and end with an empty line:"
input_array # => for example: [78, 56, 12]
def input_array
arr = [] of Int8
while number = gets
number = number.strip # removes leading or trailing whitespace
if number == "" || number == "stop"
break
end
add_to_array(arr, number)
end
arr
end
def add_to_array(arr, number)
begin
arr << number.to_i8
rescue
puts "integer bigger than 255"
end
end
Теперь метод "add_to_array" обрабатывает потенциально возможное исключение ("exception). Вы можете написать это более сжато, отбросив ключевое слово "begin", как показано ниже:
__methods_and_procs/exceptions2.cr__
def add_to_array(arr, number)
arr << number.to_i8
rescue
puts "integer bigger than 255"
end
Это упрощение также работает применительно к "ensure" (англ. удостовериться), который обычно используется для высвобождения ресурсов или очистки.
_Использование рекурсивных методов_
Метод может вызвать сам себя например, здесь вычисляется факториал целого числа в методе "fact":
__methods_and_procs/factorial.cr__
def fact(n)
n == 0 ? 1 : n * fact(n - 1)
end
fact(5) # => 120
Этот метод с радостью принимает любой аргумент, но посмотрите, что случится, если скормить ему отрицательное целочисленное значение или строку:
fact(-2) # => Runtime error: Invalid memory access
(signal 11) at address 0x7ffbff7fdff8
fact("Crystal") # => Error: undefined method '-' for String
Вы можете сделать свой код более надежным, умело используя типы данных и обработку исключений:
__methods_and_procs/factorial2.cr__
def fact(n : Int) : Int
if n < 0
raise ("n cannot be negative!")
end
n == 0 ? 1 : n * fact(n - 1)
end
fact(5) # => 120
begin
fact(-2) # => Runtime error: n cannot be negative! (Exception)
rescue ex
p ex.message
end
# => "n cannot be negative!"
fact("Crystal") # => Error: no overload matches 'fact' with type String
Теперь ваша программа не завершится аварийно: созданное исключение перехватывается, что можно заметить из краткого сообщения.
И передача строкового аргумента блокируется на стадии компиляции с четким сообщением о несовпадении типов аргумента и параметра. Но если вы не хотите использовать громоздкую конструкцию "begin-rescue", просто напечатайте сообщение об ошибке и выйдите (exit) из программы:
__methods_and_procs/factorial3.cr__
def fact(n : Int) : Int
if n < 0
puts "n must be positive!"
exit
end
n == 0 ? 1 : n * fact(n - 1)
end
fact(5) # => 120
fact(-2) # => "n must be positive!"
Хорошая привычка проявлять разумную предусмотрительность при входе в любой метод, особенно рекурсивный.
Такой подход также позволяет вам легко выйти из глубокой рекурсии. Немедленно вернуться из метода,
если не обнаружилось необходимое условие. Например:
def some_method(n : Int)
return nil unless n > 1
# other code, here n is > 1
end
Вы можете вернуть нулевое (nil) или ложное (false) значение, или любое другое значение, которое вы считаете нужным. Некоторые люди в подобных случаях любят использовать unless, поскольку в нем содержится условие, которое должно быть выполнено, и потому оно кажется легче для понимания.
Помните, что ошибки дороги. Оставляйте шанс для потенциальных ошибок только тогда, когда несоблюдение важного условия требует особого внимания.
_______________________________________________
Свертывание очереди рекурсивных вызовов.
Некоторые языки, произошедшие от Ruby, в частности Elixir, позволяют рекурсии продлеваться все глубже и глубже, пока функция не потребует дополнительной информации для корректировки маршрута.
Если возможно, эти ЯП выполняют так называемую оптимизацию "остаточного вызова", проницательно изучая рекурсивные вызовы функции в ее последней строке и подчищая рутинные служебные данные, которыми сопровождаются вызовы.
В прямом смысле Crystal не поддерживает хвостовую оптимизацию обратных вызовов (близкий термин хвостовая рекурсия), хотя с опорой на компилятор LLVM это иногда срабатывает. Пока поддерживается длительный период рекурсии, вы можете проверить, сколько же "шагов" нужно сделать в конкретных обстоятельствах.
_____________________________________________
ВАШ ЧЕРЕД 4.
Пузырьковая сортировка:
Реализация алгоритма "Bubblesort" для сортировки массива (в виде программного псевдокода) найдется в Википедии. В вашей программе организуйте копирование вводимых данных с помощью метода "dup". Напишите две версии алгоритма "Bubblesort", для сортировки по возрастанию и убыванию, используя "yield" и блок кода для каждой версии.
_Заключение_
Мы надеемся, вы уже убедились, что Crystal весьма универсален и гибок в работе с аргументами метода. И он не испытывает затруднений, принимая и возвращая большие объемы числовых значений. Перегрузка с ограничением типов делает ваш код более надежным и часто более эффективным, в то время как разумное использование "Procs" может сделать его более адаптивным.
В следующей главе мы поговорим о классах и структурах. Поглядим, что Кристалл предложит вашему вниманию там.
Глава 5.
Использование Классов и Структур.
Если вы занимались объектно-ориентированной разработкой ПО, вы наверняка знакомы с классами. И возможно, вы уже убедились, что нюансы работы с классами отличаются в разных ЯП. В этой главе вы увидите характерные для Crystal рецепты, и вы познаете тонкости работы с классами "от и до". Вы изучите процесс построения класса и структуры и узнаете, как использовать наследование и как контролировать видимость методов. По пути вы также освоите некоторые особенности синтаксиса.
В объектно-ориентированном коде методы, как правило, не обособляются: они формируют активную компоненту класса, и они являются единственным способом взаимодействия с объектами. Вы лишь должны разведать ту часть объекта, которая представлена данными, характеризующими его внутреннее состояние, с помощью методов.
Это так же верно и для ЯП Crystal. Но, как мы видели ранее, методы также могут использоваться автономно или в модулях, демонстрируя более функциональный подход к программированию.
Вы уже знаете, что всё в Кристалле это объект. Каждый объект во время выполнения скомпилированного кода имеет определенный тип, который соответствует имени своего класса и реагирует на определенные методы.
Вы также знаете, что на стадии компилирования некоторые вещи могут быть немного сложнее: во время компиляции typeof передает тип, определяющий, какой именно код сгенерируется, и защищающий вас от досадных ошибок.
Когда производительность приобретает особую важность для вашего проекта и базовый арсенал элементарных кодерских решений выглядит некомпетентным, Crystal предлагает структуры в качестве возможной альтернативы классам.
_Переделка классов Ruby для Crystal_
Если вы разработчик Ruby, преобразование классов как раз та область, где различия между языками особенно заметны. Прогулка по мостику от Ruby до Crystal с разъяснением сообщений об ошибках, встреченных по пути, позволяет подробнее разглядеть, как подход Кристалла к типам данных меняет ход дела. (Если вы не являетесь разработчиком Ruby, можете свободно переходить к следующему разделу.)
Вот незатейливый класс языка Ruby:
__classes_and_structs/mineral.rb__
class Mineral
attr_reader :name, :hardness, :crystal_struct
def initialize(name, hardness, crystal_struct)
@name = name
@hardness = hardness
@crystal_struct = crystal_struct
end
end
def mineral_with_crystal_struct(crstruct, minerals)
minerals.find { |m| m.crystal_struct == crstruct }
end
def longest_name(minerals)
minerals.map { |m| m.name }.max_by { |name| name.size }
end
Теперь давайте добавим немного проверочных данных и посмотрим, как это сработает:
__classes_and_structs/mineral.rb__
minerals = [
Mineral.new("gold", 1, 'cubic'),
Mineral.new("topaz", 8, 'orthorombic'),
Mineral.new("apatite", 5, 'hexagonal'),
Mineral.new("wolframite", 4.5, 'monoclinic'),
Mineral.new("calcite", 3, 'trigonal'),
Mineral.new("diamond", 10, 'cubic'),
]
min = mineral_with_crystal_struct('hexagonal', minerals)
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
# => hexagonal - apatite - 5
puts longest_name(minerals)
# => wolframite
Запуск этой программы в терминале с помощью команды
$ ruby mineral.rb демонстрирует следующее:
apatite - hexagonal - 5
wolframite
Вроде бы все нормально, но что, если в вашем массиве нет минерала с указанной кристаллической структурой?
__classes_and_structs/mineral.rb__
# Runtime error:
min = mineral_with_crystal_struct('triclinic', minerals)
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
# 3.5_mineral.rb:39:in `<main>': undefined method 'crystal_struct'
# for nil:NilClass (NoMethodError)
Ruby "выстреливает" ошибкой времени выполнения, потому что метод "find" возвращает "nil", когда не может найти совпадение. Пренебрежение проверкой возвращаемого нулевого значения (nil) не всегда имеет столь очевидные последствия.
Crystal предпочел бы избавить вас от этих проблем. Переделывая исходный код Ruby для языка Crystal,
вы столкнетесь с множеством ошибок. Не волнуйтесь это всё во благо, и вы научитесь ценить характер Кристалла!
Теперь посмотрим, как Crystal справится с этим: сохраните наш код Ruby в файле "mineral.cr" и смонтируйте
командой $ crystal mineral.cr.
Синтаксис Crystal это не совсем синтаксис Ruby, поэтому вы сразу же столкнетесь с ошибкой:
<= Syntax error in mineral.cr:20: unterminated char literal,
use double quotes for strings
Mineral.new("gold", 1, 'cubic'),
^
ЯП Руби допускает как одиночные, так и двойные кавычки для строк, а Crystal нет! Вам нужно будет
заменить все строки с одинарной кавычкой на строки с двойной кавычкой и затем скомпилировать снова:
Следующая ошибка сообщает о другом различии:
Error in mineral.cr:2: undefined method 'attr_reader'
attr_reader :name, :hardness, :crystal_struct
^~~~~~~~~~~
Crystal использует ключевое слово getter (фактически это макрос, см. параграф "Кристаллизуйте ваш код с помощью Макросов") вместо "attr_reader", setter вместо "attr_writer", и property вместо "attr_accessor". Есть несколько других незначительных различий между Руби и Кристаллом, но не так уж много. Подробнее написано в Приложении 2 "Портирование кода Ruby на Crystal" (eBook).
Вы можете назначить имя для свойства. Это не обязательно должен быть символ. Замена attr_reader на "getter" и ещё одна (третья) компиляция выдает еще одну ошибку, которая теперь указывает нам на существенное различие с Руби. Это сообщение является подробным и многословным. Не волнуйтесь мы покажем его лишь один раз:
<=
Error in mineral.cr:20:
instantiating 'Mineral:Class#new(String, Int32, String)'
Mineral.new("gold", 1, "cubic"),
^~~
in mineral.cr:5:
.../Не удается определить тип переменной экземпляра "@ name" для Mineral/
Тип переменной экземпляра, если он явно не прописан с помощью "@ name: Type", будет определен в зависимости от присваиваемых ей (на протяжении всей программы) значений.
Расстановка типов должна выглядеть следующим образом:
1. "" @ name = 1 "" (или другие символьные константы), подходящий тип подбирается путем логичных умозаключений.
2. "" @ name = Type.new "", тип явно задается словом перед точкой (Название.new).
3. "" @ name = Type.method "", где метод ("method") возвращает примечание с названием типа, из него выводится тип.
4. '@ name = arg', где 'arg' это аргумент метода с закрепленным типом ('Type'), здесь тип однозначно зафиксирован заранее.
5. '@ name = arg', где 'arg' является аргументом метода с неопределенным значением, тип выводится с использованием вышеупомянутых способов 1, 2 и 3.
6. "@ name = неИнициализированный Тип", тип явно указан последним словом в кавычках (Type).
7. "@ name = LibSome.func ", где "" LibSome" это библиотека, а тип логически вытекает из func.
8. 'LibSome.func (out @ name)', где 'LibSome' является библиотекой, а тип выводится из аргумента функции "func".
Все прочие присвоения значений (не совпадающие с этими 8 форматами) не влияют на тип переменной экземпляра.
Can't infer the type of instance variable '@name' of Mineral
(Не удается определить тип переменной экземпляра @name для "Mineral")
@name = name
^~~~~
Здесь Crystal не может достоверно выяснить тип переменной экземпляра @name, и ему нужно, чтобы мы
указали его. Мы указали @name = name, и этого недостаточно. Вы можете исправить это, объявив
тип @name следующим образом: "getter name : String". Вы должны сделать это для других свойств таким же манером:
getter name : String
getter hardness : Int32
getter crystal_struct : String
Всё же появляется еще одна ошибка времени компиляции:
<= Error in mineral.cr:25: instantiating
'Mineral:Class#new(String, Float64, String)'
Mineral.new("wolframite", 4.5, "monoclinic"),
^~~
in mineral.cr:8: instance variable '@hardness' of Mineral
must be Int32, not Float64
@hardness = hardness
^~~~~~~~~
Итак, вольфрамит имеет твердость 4,5 и,как видите, это не целочисленное значение. Вы можете заменить
объявление "@hardness" на "getter hardness: Float64", но затем компилятор жалуется, что твердость для других минералов все еще Int32. Лучше преобразовать единицы твердости в числа с плавающей точкой.
Другой прогон компиляции показывает нам новую ошибку среди данных структурного "шестигранника":
<= Error in mineral.cr:31: undefined method 'crystal_struct'
for Nil (compile-time type is (Mineral | Nil))
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
^~~~
Rerun with --error-trace to show a complete error trace
Это напомнит вам о нулевой (nil) ошибке во время исполнения кода, которую вы уже получали от Ruby. Crystal дает вам больше подробной информации, когда вы компилируете следующим образом:
$ crystal mineral.cr --error-trace
# //ошибка, также называемая Nil-trace:
Nil trace:
mineral.cr:30
min = mineral_with_crystal_struct("hexagonal", minerals)
^~~
mineral.cr:30
min = mineral_with_crystal_struct("hexagonal", minerals)
^~~~~~~~~~~~~~~~~~~~~~~~~~~
mineral.cr:13
def mineral_with_crystal_struct(crstruct, minerals)
^~~~~~~~~~~~~~~~~~~~~~~~~~~
mineral.cr:14
minerals.find { |m| m.crystal_struct == crstruct }
^~~~
/opt/crystal/src/enumerable.cr:409
def find(if_none = nil)
/opt/crystal/src/enumerable.cr:410
each do |elem|
^
/opt/crystal/src/enumerable.cr:413
if_none
^~~~~~~
/opt/crystal/src/enumerable.cr:409
def find(if_none = nil)
^
Трассировка Nil откатывается через исходный код, начиная от метки неопределенного метода до того места,
где появился тип-нарушитель. И это произошло в методе find, в файле "enumerable.cr:
def find(if_none = nil)
Этот код показывает, что find возвращает значение "nil" в качестве значения по умолчанию на тот случай, когда ничего не найдено.
_______________________________________
Кристалл написан на языке Crystal.
Кстати, стандартная библиотека Crystal целиком реализована возможностями самого Crystal. Например,
вы можете посмотреть, как Crystal кодирует все методы "Enumerable, если угодно, на сайте
https://github.com/crystal-lang/crystal/blob/master/src/enumerable.cr. Дерзайте, ученье подождет.
______________________________________
Компилятор сигнализирует о возможном возникновении исключения с нулевой ссылкой, но без запуска
программного кода. Это позволяет избежать жалоб клиентов на жуткие ошибки (ну, по крайней мере, на ошибки такого рода). Это характерная особенность Crystal.
Сообщение об ошибке также подсказывает, что типом времени компиляции является "Mineral | Nil".
Это тип объединения (см. "Использование массивов"): обычно переменная min ссылается на объект Mineral,
но если в совокупности ваших данных нет минерала, соответствующего этой специфической кристаллической структуре, min равно нулевому (nil) значению. Crystal проверяет, доступен ли каждый метод, вызываемый с помощью "min", для всех типов внутри объединенного Типа. Если нет бац: ошибка.
Вы можете исправить ее тем же путем, что используется в ЯП Ruby:
if min
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
else
puts "No mineral found with this crystal structure!"
end
Если это выглядит странно для вас, пересмотрите нашу дискуссию об истинности и фальши в разделе "Управление Ходом Исполнения". Когда min равно nil, это соответствует false и первая ветвь конструкции if не выполняется. Но, оказавшись внутри первой ветви, мы понимаем, что тип "min" это Mineral, а не специфический тип nil. В другой ветви всё наоборот, типом будет nil, а не Mineral.
Наконец, все работает и вы получаете тот же результат, что и в Ruby:
apatite - hexagonal - 5
wolframite
Теперь давайте применим замечательный краткий синтаксис кода (заимствованный из CoffeyScript). Вместо:
def initialize(name, hardness, crystal_struct)
@name = name
@hardness = hardness
@crystal_struct = crystal_struct
end
вы можете писать:
def initialize(@name, @hardness, @crystal_struct)
end
Переменные экземпляра обретают свои значения непосредственно при создании объекта, @name принимает gold, "@hardness" получает значение 1 и так далее:
Mineral.new("gold", 1.0, "cubic")
Можно также прописать свойства в методе "initialize" вместо использования формулы "getter", например:
def initialize(@name : String, @hardness : Float64, @crystal_struct : String)
Это не так уж плохо, верно ведь?
Ниже полностью приведен код Crystal для нашей программы:
__classes_and_structs/mineral.cr__
class Mineral
getter name : String
getter hardness : Float64
getter crystal_struct : String
def initialize(@name, @hardness, @crystal_struct)
end
end
def mineral_with_crystal_struct(crstruct, minerals)
minerals.find { |m| m.crystal_struct == crstruct }
end
def longest_name(minerals)
minerals.map { |m| m.name }.max_by { |name| name.size }
end
minerals = [
Mineral.new("gold", 1.0, "cubic"),
Mineral.new("topaz", 8.0, "orthorombic"),
Mineral.new("apatite", 5.0, "hexagonal"),
Mineral.new("wolframite", 4.5, "monoclinic"),
Mineral.new("calcite", 3.0, "trigonal"),
Mineral.new("diamond", 10.0, "cubic"),
]
min = mineral_with_crystal_struct("hexagonal", minerals)
if min
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
else
puts "No mineral found with this crystal structure!"
end
# => hexagonal - apatite - 5
min = mineral_with_crystal_struct("triclinic", minerals)
if min
puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"
else
puts "No mineral found with this crystal structure!"
end
# => "No mineral found with this crystal structure!"
puts longest_name(minerals)
# => wolframite
Crystal уверенно ведет вас и ожидает более качественного и скрупулезного кодирования, чем Ruby. При использовании языка Ruby необходимо больше полагаться на совершенство вашей тестовой подсистемы, внедренной в исходный код, чтобы обнаруживать и предотвращать наиболее вероятные ошибки.
_Структурирование класса_
В предыдущем параграфе и в разделе "Организация кода в Классах и Модулях" вы видели простой класс "Минералы". Вот исходный код класса, так сказать, собственной персоной, без всякой дополнительной логики:
class Mineral
getter name : String
getter hardness : Float64
getter crystal_struct : String
def initialize(@name, @hardness, @crystal_struct) # constructor
end
end
Этот класс имеет три переменных экземпляра, доступных только для чтения: name, hardness и crystal_struct. Предоставление им типов навязано компилятором Crystal. Но также вы можете расставить типы данных, используя метод initialize:
__classes_and_structs/classes.cr__
class Mineral
getter name, hardness, crystal_struct
def initialize(@name : String,
@hardness : Float64,
@crystal_struct : String)
end
end
Значения по умолчанию можно распределить следующим образом:
def initialize(@name : String = "unknown", ...)
end
Некоторые люди используют символы вроде :hardness для именования свойства, но это не обязательно. Свойство с неустановленным типом должно иметь значение по умолчанию. Или вы можете дать ему значение при инициализации (initialize). Вам не нужно определять переменные в начале класса. Здесь метод "new" создаёт объект Mineral:
min1 = Mineral.new("gold", 1.0, "cubic")
min1 # => #<Mineral:0x271cf00 @crystal_struct="cubic",
# => @hardness=1.0, @name="gold">
min1.object_id # => 41012992 == 0x271cf00
typeof(min1) # => Mineral # compile-time type
min1.class # => Mineral # run-time type
Mineral.class # => Class # all classes have type Class
Запомните, new это метод класса, который создается автоматически для каждого класса. Он резервирует место в оперативной памяти, вызывает метод "initialize", а затем возвращает вновь созданный объект. Объект создается в так называемой "куче" (общем массиве свободной ОП), и он снабжен уникальным идентификатором (object_id), сообщающим адрес ячейки памяти.
Когда он получает новое имя или передается методу, фактически передается лишь ссылка на объект. Это означает, что объект сам изменится в том случае, если будет изменен в методе. Если вы не уверены, какие типы данных примет ваш метод "initialize", вы также можете использовать групповые типы вроде "T", как это делается в классе Mineralg:
___classes_and_structs/classes.cr___
class Mineralg(T)
getter name
def initialize(@name : T)
end
end
min = Mineralg.new("gold")
min2 = Mineralg.new(42)
min3 = Mineralg(String).new(42)
# => Error: no overload matches 'Mineralg(String).new' with type Int32
Именование переменных экземпляра сопровождается приписыванием префикса "@". Для переменных класса используйте символы "@@", например @@planet, откуда и происходят наши минеральные породы. Все объекты, построенные с помощью этого класса, будут иметь общий доступ к этой переменной, и ее значение будет одинаковым для каждого такого объекта. (Впрочем подклассы, которые вы увидите в следующем разделе, все получают собственную копию переменной класса со значением, общим для всех членов отдельного подкласса.)
Чтобы дать имя свойству, которое может изменяться, например, "quantity" (количество) в следующем примере, припишите перед ним КС "property". Для свойств, которые доступны лишь для записи и потому не считываются, используйте префикс "setter", например, id в нижеследующем исходном коде.
Попытка вывести на дисплей нечитаемое свойство завершается ошибкой:
__classes_and_structs/classes.cr__
class Mineral
@@planet = "Earth"
getter name, hardness, crystal_struct
setter id
property quantity : Float32
def initialize(@id : Int32, @name : String, @hardness : Float64,
@crystal_struct : String)
@quantity = 0f32
end
def self.planet
@@planet
end
end
min1 = Mineral.new(101, "gold", 1.0, "cubic")
min1.quantity = 453.0f32 # => 453.0
min1.id # => Error: undefined method 'id' for Mineral
Mineral.planet # => "Earth"
min2 = min1.dup
min1 == min2 # => false
Необходимо удостовериться, что ваши свойства всегда будут инициализированы либо в методе инициализации (initialize), либо после вызова метода new. Названия методов, вызываемых в самом классе, предваряют префиксом "self.", как было сделано в вышеприведенном примере с методом "planet".
Используйте метод "dup" для создания "упрощенной" копии объекта: копия min2 является другим объектом, но если оригинал содержит поля, которые сами являются объектами, то они не войдут в состав копии. Если же вам нужна "подробная" копия, вам следует определить метод "clone. Вы можете также дополнительно написать метод "finalize" для класса, который будет автоматически вызываться, когда объект подбирается мусоросборщиком:
def finalize
puts "Bye bye from this #{self}!"
end
Но это создает лишнюю работу для процесса сбора мусора. Вы должны использовать его, только если вы хотите освободить занятые внешними библиотеками ресурсы, которые сборщик мусора Crystal не освободит самостоятельно.
Добавьте этот фрагмент кода, чтобы увидеть работу завершающего метода, но будьте осторожны: вы исчерпаете свободную память вашего компьютера, копая столько золота. Так что сохраните все, что вам нужно, прежде чем запустить его.
loop do
Mineral.new(101, "gold", 1.0, "cubic")
end
Как и в Ruby или в C#, вы можете повторно открыть класс, что означает создание дополнительных определений этого класса: ведь все они объединены в один класс. Это также работает для встроенных классов. Насколько это круто определять свои собственные новые методы для существующих классов, таких как "String" или "Array"? (Да, иногда такой подход дерзко называют "обезьяний патчинг", и это не всегда хорошая идея.)
ВАШ ЧЕРЁД 1.
1. Сотрудник: Создайте класс Employee с извлекателем (так называемым "getter") "name" и свойством "age". Создайте объект класса Employee и попытайтесь изменить его имя.
2. Приращение: Создайте класс Increment со свойством "amount" и двумя версиями метода increment: один, который добавляет 1 к amount, и другой, который добавляет значение "inc_amount".
_Применение наследования_
Как и во всех объектно-ориентированных языках, и прежде всего в Ruby, в Crystal предусмотрено одиночное наследование, обозначаемое: подкласс < суперкласс. Перемещениее свойств и методов, объединенных в несколько классов, в один суперкласс позволяет всем им совместно использовать функциональные возможности. Таким образом, в подклассе вы можете использовать все переменные экземпляра и все методы суперкласса, включая конструкторы.
Это можно увидеть в следующем примере, где PDFDocument наследует "initialize", "name" и "print" из класса Document:
__classes_and_structs/inheritance.cr__
class Document
property name
def initialize(@name : String)
end
def print
puts "Hi, I'm printing #{@name}"
end
end
class PDFDocument < Document
end
doc = PDFDocument.new("Salary Report Q4 2018")
doc.print # => Hi, I'm printing Salary Report Q4 2018
Вы можете также переопределить любой унаследованный метод в подклассе. Если подкласс определяет
собственные методы initialize для инициализации, они больше не наследуются. Если вы хотите использовать функциональность суперкласса после его переопределения, можно вызвать любой метод суперкласса с помощью "super":
__classes_and_structs/inheritance.cr__
class PDFDocument < Document
def initialize(@name : String, @company : String)
end
def print
super
puts "From company #{@company}"
end
end
# doc = PDFDocument.new("Salary Report Q4 2018")
# => Error: wrong number of arguments for 'PDFDocument.new' (given 1,
# => expected 2)
doc = PDFDocument.new("Salary Report Q4 2018", "ACME")
doc.print
# => Hi, I'm printing Salary Report Q4 2018
# From company ACME
Система типов языка Crystal дает вам больше возможностей здесь. Вместо перестановки типов можно формировать специализированные методы с использованием ограничений для типов данных, например "print" в PDFDocument:
__classes_and_structs/inheritance.cr__
class PDFDocument < Document
def initialize(@name : String, @company : String)
end
def print(date : Time)
puts "Printing #{@name}"
puts "From company #{@company} at date #{date}"
end
end
doc = PDFDocument.new("Salary Report Q4 2018", "ACME")
doc.print(Time.now)
# => Printing Salary Report Q4 2018
# From company ACME at date 2017-05-25 12:12:45 +0200
ИСПОЛЬЗОВАНИЕ АБСТРАКТНЫХ КЛАССОВ И ВИРТУАЛЬНЫХ ТИПОВ.
ЯП Ruby, в отличие от Java или C#, не имеет собственной поддержки интерфейсов и абстрактных классов. И в Ruby, и в Crystal концепция интерфейса реализована посредством модулей, как вы увидите в следующей главе. Но в Кристалле также существует понятие абстрактного класса, так что если вы Руби-кодер, то ожидающий впереди материал будет вам в новинку.
Не всем классам суждено производить объекты, и абстрактные классы являются хорошим примером. Вместо этого они служат концептуальным планом для подклассов, чтобы реализовать их методы. Здесь вы видите класс Rect (описывающий прямоугольники), вынужденный реализовать все абстрактные методы из класса Shape:
__classes_and_structs/inheritance.cr__
abstract class Shape
abstract def area
abstract def perim
end
class Rect < Shape
def initialize(@width : Int32, @height : Int32)
end
def area
@width * @height
end
def perim
2 * (@width + @height)
end
end
s = Shape.new # => can't instantiate abstract class Shape
Rect.new(3, 6).area # => 18
Если один из методов (скажем, perim) не реализован, компилятор выдает ошибку, похожую на следующее:
error: "abstract `def Shape#perim()` must be implemented by Rect"
Это позволяет вам создавать иерархии классов, в которых можно быть уверенным, что все необходимые методы проработаны должным образом. Так же легко вы можете создавать более сложные структуры. В следующем примере класс Document можно назвать виртуальным типом, потому что он сочетает различные типы из определенной иерархии типов в данном случае это различные документы:
__classes_and_structs/virtual.cr__
class Document
end
class PDFDocument < Document
def print
puts "PDF header"
end
end
class XMLDocument < Document
def print
puts "XML header"
end
end
class Report
getter doc
def initialize(@name : String, @doc : Document)
end
end
salq4 = Report.new "Salary Report Q4", PDFDocument.new
taxQ1 = Report.new "Tax Report Q1", XMLDocument.new
Этот виртуальный тип указывается компилятором как тип Document+, подразумевая тем самым, что все типы наследуют от класса Document, в том числе и сам Document. Он вступает в игру в ситуациях вроде той, что приводится ниже, где вы ожидаете, что "d" относится к объединенному типу данных (PDFDocument | XMLDocument):
if 4 < 5
d = PDFDocument.new
else
d = XMLDocument.new
end
typeof(d) # => Document
Но, вопреки ожиданиям, d соответствует типу Document. Внутри компилятор использует его как виртуальный тип Document+ вместо объединенного типа (PDFDocument | XMLDocument), поскольку типы объединений быстро становятся очень сложными в классовых иерархиях.
Если вызвать метод в подклассе, созданном для Document, возникает ошибка:
salq4.doc.print # => Error: undefined method 'print' for Document
Чтобы устранить эту ошибку, просто сделайте класс Document абстрактным.
__classes_and_structs/virtual.cr__
abstract class Document
end
salq4.doc.print # => PDF header
ВАШ ЧЕРЁД 2.
Фигура: Подкласс Shape с классами Square и Circle. (Подсказка: Примените PI из модуля Math, используя запись: "include Math".)
__Управление видимостью__
Видимые объекты можно прочитать или даже изменить с помощью внешнего кода, и это является заурядной причиной ошибок и сюрпризов. Ограничение видимости (так называемая инкапсуляция кода) очень часто применяется в объектно-ориентированных языках. Все это связано с концепцией пространства имен в программном коде некой (номенклатурной) области в коде, которая обозначена определенным именем. Класс является простым примером: всё внутри описания класса формирует часть его пространства имен.
По умолчанию объект действительно будет узнаваемым для всего пространства имен, в котором он определен:
он является общедоступным в пределах этого пространства имен, но при этом невидимым и недоступным вне этого пространства имен. Мы поговорим об этом более подробно в следующей главе, посвященной модулям.
В Crystal методы по умолчанию являются публичными. То есть они пригодны для использования внутри и снаружи того класса, в котором они определены. Чтобы ограничить видимость, вы можете подставить к формулировке метода специальное слово: либо "private" (имя говорит само за себя), либо "protected" (то есть защищенный).
ОБЪЯВЛЯЕМ МЕТОД ЧАСТНЫМ.
Частные методы больше похожи на вспомогательные: их можно использовать только внутри самого класса или в его подклассах, и их нельзя вызвать для объекта.
__classes_and_structs/private.cr__
class Document
property name
def initialize(@name : String)
end
private def print(message)
puts message
end
def printing
print "Hi, I'm printing #{@name}"
# self.print "Printing with self does not work"
# => Error: private method 'print' called for Document
end
end
class PDFDocument < Document
def printing
super
print "End printing PDFDocument"
end
end
doc = Document.new("Salary Report Q4 2018")
doc.printing # => Hi, I'm printing Salary Report Q4 2018
pdoc = PDFDocument.new("Financial Report Q4 2018")
pdoc.printing # =>
# Hi, I'm printing Financial Report Q4 2018
# End printing PDFDocument
# doc.print("test") # => Error: private method 'print' called for Document
Такие типы, как перечисления (enums), тоже могут быть частными. Тогда вы можете их использовать только внутри того пространства имен, в котором они определены.
И далее точно так вы можете использовать только методы или типы верхнего уровня, которые помечены как приватные в текущем файле с исходным кодом. Можно также использовать служебное слово "private" с классами, модулями и константами, а также с псевдонимами и библиотеками, которые вы увидите позже.
ОХРАНЯЕМЫЕ МЕТОДЫ.
Защищенные методы устроены неcколько хитрее: они могут делать те же вещи, что и их приватные собратья, но ещё они могут вызываться для объекта. Этот объект должен быть того же типа, что и текущий (используемый сейчас) тип данных, например, класс, в котором мы теперь находимся, или класс из того же пространства имен. А вот пример, чтобы сделать вышесказанное более конкретным:
__classes_and_structs/protected.cr__
class Document
property name
def initialize(@name : String)
end
protected def print(message)
puts message
end
def printing
print "Hi, I'm printing #{@name}"
self.print("This works too: self is a Document")
doc = Document.new("Taxes")
doc.print("This also: doc is a Document")
end
end
class BankAccount < Document
def printing
doc = Document.new ("TestDoc")
doc.print "inside BankAccount"
end
end
class BankAccount2
def printing
doc = Document.new ("TestDoc")
doc.print "inside BankAccount2"
end
end
doc2 = Document.new "Audit 2017"
doc2.printing
# => Hi, I'm printing Audit 2017
# => This works too: self is a Document
# => This also: doc is a Document
doc2.print "Audit" # => Error: protected method 'print' called for Document
ba = BankAccount.new "test"
ba.printing # => inside BankAccount
ba2 = BankAccount2.new
# ba2.printing # => Error: protected method 'print' called for Document
Защищенный метод "print" работает внутри класса BankAccount, который сам является подклассом, произошедшим от Document, и потому он существует в том же пространстве имен. Но этот метод не работает внутри класса BankAccount2, лежащего за пределами пространства имен класса Document, и в результате возникает ошибка:
"error: protected method 'print' called for Document"
ПЕРЕГРУЖАЕМЫЕ ОПЕРАТОРЫ.
Перегрузка, которую вы наблюдали в предыдущем упражнении, также работает для операторов. Следующий фрагмент исходного кода демонстрирует перегрузку оператора "==". Допустим, что два образца минералов считаются равнозначными, когда у них одинаковый ID (т.е. идентификатор).
Они оба должны принадлежать классу Minerals, поэтому мы используем здесь "self" в качестве ограничителя типов. Та же логика выражается в применении метода "compare":
__classes_and_structs/classes.cr__
class Mineral
getter id, name, hardness, crystal_struct
property quantity : Float32
def initialize(@id : Int32, @name : String, @hardness : Float64,
@crystal_struct : String)
@quantity = 0f32
end
def ==(other : self) # self is Mineral
id == other.id
end
def ==(other)
false
end
def self.compare(m1 : self, m2 : self)
m1.id == m2.id
end
end
m1 = Mineral.new(101, "gold", 1.0, "cubic")
m2 = Mineral.new(108, "gold", 1.0, "cubic")
m3 = Mineral.new(101, "gold", 1.0, "cubic")
m1 == m2 # => false
m1 == m3 # => true
Mineral.compare(m1, m2) # => false
Обратите внимание, что синтаксические схемы Ruby вроде "class << self", используемые с внутриклассовыми методами, недопустимы в ЯП Crystal. Внутриклассовые методы Кристалла это методы с префиксом "self".
Теперь спросите себя: может ли класс иметь несколько методов инициализации? (В Ruby ответ будет "нет".)
И да, такое бывает в Crystal это возможно благодаря перегрузке. Вот пример, чтобы убедить вас:
__classes_and_structs/classes.cr__
class Mineral
getter id, name, hardness, crystal_struct
property quantity : Float32
def initialize(@id : Int32, @name : String, @hardness : Float64,
@crystal_struct : String)
@quantity = 0f32
end
def initialize(@id : Int32)
@quantity = 0f32
@name = "rock"
@hardness = 0
@crystal_struct = "unknown"
end
end
m1 = Mineral.new(101, "gold", 1.0, "cubic")
m4 = Mineral.new(42)
# => #<Mineral:0x271bd40
# @crystal_struct="unknown",
# @hardness=0,
# @id=42,
# @name="rock",
# @quantity=0>
___________________________________________________________________________________
Программа.
В Ruby программа верхнего уровня представляет собой экземпляр (с именем main) класса Object:
# Ruby code!
puts self # main
puts self.class # Object
Если обходиться с ней как с кодом Crystal, мы получим ошибку компилятора: "здесь отсутствует self (т.е. его нет в зоне видимости)". Но внутри класса self это всего лишь название класса. Но внутри экземплярного метода self это "текущий экземпляр", как и в языке Ruby:
__classes_and_structs/program.cr__
def display
puts "Top-level display"
end
class Mineral
puts self # => Mineral
getter name
getter hardness
getter crystal_struct
def initialize(@name : String, @hardness : Float64,
@crystal_struct : String)
end
def display
::display # => Top-level display
p self # => <Mineral:0x271cf00 @crystal_struct="cubic",
# @hardness=1.0, @name="gold">
end
end
min1 = Mineral.new("gold", 1.0, "cubic")
min1.display
Для вызова метода верхнего уровня (такого как display) в методе внутри класса принято добавлять к названию префикс пару двоеточий (::). В ЯП Crystal нет классического "main", но зато есть своего рода анонимное первичное пространство имен, называемое "the Program", где живут такие методы, как "puts, "p", "raise", "spawn" и другие, а с ними и некоторые макросы.
Программа Crystal это пространство имен, в котором можно определять (и вызывать) методы, типы, константы, локализованные в файле переменные, классы... и много чего ещё.
____________________________________________________________________________
_Работа со структурами_
Объекты, созданные из классов, занимают память в "общей куче" и сборщику мусора приходится освобождать эту область памяти. Как вы уже видели ранее на примере кода цикла в разделе "Структурирование класса", создание множества объектов может быстро истощить ресурсы ЭВМ.
Для повышения производительности в таких случаях можно использовать своего рода упрощенную разновидность классов, которая называется "struct" и наследует признаки от класса "Struct". Структуры помещаются в память стека, поэтому структура копируется (а не перемещается), когда её переназначают или передают в метод. Другими словами: осуществляется передача по значению. На первый взгляд они очень похожи на классы: имеют свойства, конструкторы и методы, и даже могут быть универсальными.
Следующий пример определяет структуру для обустройства пользовательских данных:
__classes_and_structs/structs.cr__
struct User
property name, age
def initialize(@name : String, @age : Int32)
end
def print
puts "#{age} - #{name}"
end
end
d = User.new("Donald", 42)
d.name # => Donald
d.age = 78
d.print # => 78 - Donald
_____________________________________________________
Изменение структур в методах.
Поскольку структуры копируются при передаче, вам нужно подумать о возвращаемом значении и его переопределении после возврата. В следующем фрагменте метод no_change не изменяет структуру, работает только метод "change":
__classes_and_structs/structs.cr__
def no_change(user)
user.age = 50
end
def change(user)
user.age = 50
user
end
d = User.new("Donald", 78)
d.print # => 78 - Donald
no_change(d)
d.print # => 78 - Donald
d = change(d)
d.print # => 50 - Donald
_________________________________________________
Структуры лучше всего подходят для неизменяемых (также называемых неизменными) порций данных, особенно когда структура небольшая и у вас есть несколько таких. В качестве примера можно привести стандартную библиотеку, где комплексные числа реализованы посредством структуры Complex.
Наследование также может быть задано для структуры, но только от абстрактной структуры.
_____________________________________________________
Используйте структуры для производительности.
Попробуйте превратить класс в структуру в вашем программном коде и проверьте, повысится ли производительность. Если программа работает быстрее и это не вредит функциональности, напрашивается выбор в пользу структуры.
___________________________________________________
ВАШ ЧЕРЕД 3.
Vec2D - Перегрузка оператора до структуры: Предположим, что вам нужно добавить много двухмерных векторов. Определите структуру Vec2D для выполнения этого действия и перегрузите оператор "+". Затем
определите ещё несколько похожих структур, и работайте с этими векторами. (Подсказка: Ограничьте операцию "+", пользуясь "self" для добавления дополнительных структур Vec2D.)
_Осмотр иерархии типов_
Теперь уже вы, вероятно, подозреваете, что за каждой программой Crystal стоит целая иерархия классов.
Её можно даже визуализировать с помощью инструмента Кристальной Иерархии. Например:
$ crystal tool hierarchy virtual.cr
показывает все классы и структуры из вашей программы, а также стандартную библиотеку Кристалла. Если вы хотите лишь фрагмент этого дерева (например, только ваши собственные классы), используйте флаг -e:
$ crystal tool hierarchy -e Document virtual.cr
Этот запрос производит следующий результат:
\РИС. (eBook)\
Давайте сделаем схему для наиболее важных типов, которая включает и классы, и структуры (например, Int32), которые могут быть абстрактными или обычными. Каждому сдвигу вправо соответствует очередной субклассовый слой (т.е. подкласс), как показано на рисунке ниже.
\РИС. Схема\
Первичным суперклассом является, конечно, Object, от которого каждый объект наследует совокупность методов, в том числе таких как "==" и "to_s". Далее, у нас есть резкое отличие между объектами в ветви наследования от Value, которые создаются в памяти стека, и веткой наследования от Reference, объекты которой размещаются в "куче общей памяти".
Память стека реагирует гораздо быстрее "обще-кучной памяти" и не нуждается в сборщике мусора, но все значения передаются методом копирования, и следует помнить, что размер стековой памяти ограничен. Использование общей памяти позволяет программе пройтись по ссылкам, указывающим на оригинальные значения, не касаясь самих значений, а "мусор" будет собран автоматически.
Память в "общей куче" обычно используется для таких вещей, как динамично растущие массивы. Подкласс "Exception" это тоже объект из "общей кучи". Вы можете дать типу или группе типов имя-синоним с помощью псевдонима (), как было сделано ниже для использования укороченного наименования:
__classes_and_structs/types.cr__
alias PInt32 = Pointer(Int32)
_Несколько классных трюков_
Теперь, когда у вас есть базовое понимание того, как работают классы и структуры Crystal, пришло время изучить некоторые вещи, которые упрощают их использование.
TO_S (IO).
Удобный класс должен иметь метод, преобразующий его объекты в строку, чтобы охарактеризовать самого себя.
Разработчику Java знаком такой метод, как toString(). Руби-программисту известен to_s, который наследуется от "Object". Метод "to_s" также применяется и в Crystal. Однако лучше не использовать эту форму, а вместо этого переопределять метод "to_s(IO)". Из предыдущего параграфа вы узнали, что "String" создается в "обще-кучной памяти": если вам нужно создать много таких строк, это навредит производительности вашей программы, потому что все они должны быть подобраны "сборщиком мусора".
В общем-то, следует избегать создания слишком большого количества временных строк или объектов. Вместо этого
добавляйте ваши объекты с помощью "<<" непосредственно к объекту IO без создания промежуточных строк посредством интерполяции, используя "to_s" или конкатенацию, как в первом из следующих to_s методов:
__classes_and_structs/useful.cr__
class Mineral
getter name, hardness
def initialize(@name : String, @hardness : Float64)
end
# Good
def to_s(io)
io << name << ", " << hardness
end
end
min1 = Mineral.new("gold", 42.0)
io = IO::Memory.new
# To see what io contains, use to_s:
min1.to_s(io).to_s # => "gold, 42.0"
IO это модуль для ввода и вывода в Crystal, совместимый с множеством различных посредников: с оперативной памятью, файлом, сокетом и т.д.
ИСПОЛЬЗОВАНИЕ ИСКЛЮЧЕНИЯ В КАЧЕСТВЕ КЛАССА.
Зная, что "Exception" является классом, вы можете сделать вывод, что оно имеет много подклассов, таких как
"IndexError", "TypeCastError", "IO::Error" и т.п. Ещё вы можете дополнительно определить собственные подклассы:
class CoolException < Exception
end
raise CoolException.new("Somebody pushed the red button")
# => Somebody pushed the red button (CoolException)
Лучше избавиться от этого! Можно использовать несколько спасательных ветвлений, каждое из которых принимает определенный тип исключения, для страховки поставив в конце "ловушку" для неопознанной rescue-ветви:
__classes_and_structs/useful.cr__
ex = begin
raise CoolException.new
rescue ex1 : IndexError
ex1.message
rescue ex2 : CoolException | KeyError
ex2.message
rescue ex3 : Exception
ex3.message
rescue # catch any kind of exception
"an unknown exception"
end # => "ex2"
Вот более реалистичный пример обработки исключений во время чтения файла, за которым следует попытка синтаксического анализа и перевода его в формат JSON, а затем запись в другой файл:
__classes_and_structs/useful.cr__
require "json"
path = "path/to/file"
begin
if File.exists?(path)
raw_file = File.read(path)
map = JSON.parse(raw_file)
File.write(path, "ok")
:ok
end
rescue JSON::ParseException # Parsing error
raise "Could not parse file"
rescue ex
raise "Other error: #{ex.message}"
end
ОПРЕДЕЛЕНИЕ ОБРАТНЫХ ВЫЗОВОВ.
Используя общие представления о процессах, полученные из параграфа "Работа с Выдачей, процессами и блоками", мы находим отличную возможность для определения серии обратных вызовов. Обратный вызов это метод, который должен сработать при осуществлении определенного события. В этом случае каждый вызов "after_save" добавляет новый обратный вызов, и когда событие "save" наконец происходит, каждый из обратных вызовов будет затребован в свою очередь:
__classes_and_structs/useful.cr__
class MineralC
def initialize
@callbacks = [] of ->
end
def after_save(&block)
@callbacks << block
end
# save in database, then execute callbacks
def save
# save
rescue ex
p "Exception occurred: #{ex.message}"
else
@callbacks.each &.call
end
end
min = MineralC.new
min.after_save { puts "Save in DB successful" }
min.after_save { puts "Logging save" }
min.after_save { puts "Replicate save to failover node" }
min.save # =>
# Save in DB successful
# Logging save
# Replicate save to failover node
ВАШ ЧЕРЕД 4.
1) Заново открыть класс:
Теперь вы знаете, почему следующий код ведет к аварийной ситуации:
x = rand < 0.0001 ? 1 : "hello"
x - 1 # => Error: undefined method '-' for String
Определите новый метод для "String" путем перегрузки оператора '-'. Он должен брать какое-либо число и отрезать от строки столько символов, сколько указывает число.
2) Заново открыть метод:
Предскажите, что будут показывать операторы "p в нижеследующем коде, сначала в Crystal, а затем и в Ruby. Выполните это и объясните разницу:
class A
def b
41
end
end
# this can also be written on 1 line as: class A; def b; 41; end; end;
p A.new.b
class A
def b
42
end
end
p A.new.b
Метод может быть переопределен и эффективно перезаписан, но можно вызвать более раннюю версию путём переопределения с помощью "previous_def ".
_Заключение_
Классы, иерархии и перегружаемые методы больше не должны быть загадочными. Используйте ограничения видимости с "private и "protected" или наследуйте от обычных и абстрактных типов, чтобы сжимать ваш исходный код. В определенных ситуациях структуры могут дать преимущество в производительности. В следующей главе мы рассмотрим модули, которые представляют собой структурный код ещё более высокого уровня.
Глава 6.
Работая с Модулями.
Когда вы впервые изучаете язык программирования, большинство простых примеров помещаются в один файл. Когда же вы садитесь за реальные проекты, возникает нужда сгруппировать несколько файлов с исходным кодом и держать свой рабочий проект в порядке, избегая чрезмерного усложнения и запутывания. В этой главе (и в последующих главах) будет освещена методика, необходимая для создания ремонтопригодных масштабных проектов Crystal.
_Объединение файлов с помощью "Require"_
До сих пор мы помещали весь наш код в один исходный файл, и это здорово работало для маленьких
учебных примеров. Однако же, когда объем кода вырастает и там фигурирует множество классов, вам, пожалуй, захочется разбить ваш код на несколько файлов-исходников. Вы можете объединить их в один файл, используя "require", который записывается в следующем формате:
require "path"
Здесь "path" это путь к файлу (или нескольким файлам), который требуется включить в состав вашего проекта. Когда компилятор видит "require", он выполняет поиск файлов по этому пути и копирует их содержимое в тот текстовый файл, с которым в данный момент работаете. И таким образом все исходные файлы соединятся в один большой файл. Это делается только один раз: повторное использование "require не даст ожидаемого эффекта.
Существует несколько форм этой команды, и это разнообразие может немного озадачить поначалу. Что ж, мы проиллюстрируем это шаг за шагом. В исходниках, прилагаемых к данной книге, демонстрируется, что каждый комплектный файл отчитывается о своем местоположении. Здесь же показано, как выглядит структура директории с файлами-исходниками:
fileA.cr
dirA
dirB.cr
fileA.cr
fileA2.cr
fileA3.cr
dirB
dirB.cr
fileB.cr
dirC
fileC1.cr
fileC2.cr
ПРОСМОТР ТЕКУЩЕЙ ПАПКИ.
Выражение вида require "./part" выполняет поиск part.cr или, если он не обнаружен, part/part.cr (здесь "part/" является подкаталогом) в текущей папке. Вот несколько примеров:
__working_with_modules/require.cr__
require "./fileA"
# I am from fileA.cr in the current folder
# OR (if present): fileA.cr in subfolder fileA
require "./dirA/fileA"
# I am from fileA.cr in dirA
require "./dirA/dirB"
# I am from dirB.cr in dirA
# OR: I am from dirB.cr in dirB in dirA
Чтобы легче было увидеть разницу, вы можете прямо написать require "./part.cr", самостоятельно назначая
нужный файл.
ПРОСМОТР РОДИТЕЛЬСКОЙ ПАПКИ.
Добавив пару точек перед слэшем, мы получим вариант [require "../part"], который работает подобно предыдущей форме записи, за исключением того, что на сей раз компилятор выполняет поиск в родительской папке.
__working_with_modules/require.cr__
require "../fileA"
# I am from fileA.cr in the parent folder
require "../dirA/fileA"
# I am from fileA.cr in dirA in the parent folder
Вы также можете перейти на несколько уровней выше, вот так:
require "../../part".
ЗАГЛЯНЕМ НА НЕСКОЛЬКО УРОВНЕЙ ВГЛУБЬ.
Вложенные формы также работают, это выглядит так:
__working_with_modules/require.cr__
require "./dirA/dirB/fileB"
# I am from fileB.cr, in dirB in dirA
ГРУППОВЫЕ СИМВОЛЫ ДЛЯ ФАЙЛОВ ИЛИ ПОДПАПОК.
С помощью подстановочного символа (*) выражение require "./dirA/*" ищет все файлы ".cr" в папке dirA, но не во вложенных папках.
__working_with_modules/require.cr__
require "./dirA/*"
# I am from fileA2.cr in dirA
# I am from fileA3.cr in dirA
Если вы также хотите проверять вложенные папки с помощью спецсимвола (*), можно использовать команду require "./dirA/**". Она проверяет наличие всех файлов ".cr" в папке dirA, а также во вложенных папках, таких как dirC.
__working_with_modules/require.cr__
require "./dirA/**"
# I am from fileC1.cr in dirC in dirA
# I am from fileC2.cr in dirC in dirA
ИСПОЛЬЗОВАНИЕ ПАПОЧНЫХ СТРУКТУР в Crystal.
Можно также добавить файлы, используя спецдиректорию Crystal, применив формулировку [require "file"].
__working_with_modules/require.cr__
require "file"
Здесь разыскиваются "file.cr" или файл "file/file.cr" по указанному пути, состоящему из двух частей. Первая часть это расположение стандартной библиотеки, которая в Linux лежит в каталоге /opt/crystal/src. Вам не понадобится помещать сюда что-либо, но компилятору нужно знать адрес каталога, чтобы обнаружить все исходные файлы Crystal.
Вторая это папка библиотеки lib относительно текущей рабочей папки. В работе с масштабными проектами надежная структура, которую предлагает этот подход, кажется довольно привлекательной.
Вы cможете использовать этот прием для включения в проект внешних библиотек. В следующей главе вы увидите, как в стандартном проекте Crystal по умолчанию применяются эти правила.
_____________________________________________
Прелюдия.
Вы когда-нибудь задавались вопросом, почему не нужно применять "require", работая с Булевыми значениями, символами, строками, массивами и прочими типами, а также с модулями?
Это потому, что все они уже мобилизованы в сценарии "prelude", который зачастую можно найти в Linux в директории /opt/crystal/src/prelude.cr или в папке, выбранной установщиком Crystal. Вы также можете найти последнюю версию prelude.cr в репозитории GitHub.
__________________________________________
_Использование модулей в качестве пространств имен_
Различные библиотеки иногда используют одни и те же имена для методов или переменных, что приводит к противоречивым совпадениям. Чтобы отличить их друг от друга, нужно дополнительно использовать ещё одно имя название того модуля, в котором определен искомый объект.
Модули структурируют исходный код путем создания именованных пространств (наподобие доменов). Вы можете использовать модуль, чтобы реализовать комплекс функционально связанных компонентов, включающий в себя константы, методы, классы и даже другие модули. Первый пример вы видели в разделе "Организация кода в классах и модулях". Вот еще один:
__working_with_modules/namespaces.cr__
module Crystals
class Rhombic
end
class Triclinic
end
end
t = Crystals::Rhombic.new
typeof(Crystals) # => Class
Обратите внимание, как обозначено пространство имен в предпоследней строке ("Crystals :: Rhombic.new"). Далее вы видите, что тип модуля опознан как "Class". Но можно ли вывести объект из модуля? Попробуйте, прежде чем двигаться дальше.
Теперь вы можете понять, как же разрешается проблема "перекрытия" имен (два или более методов с тем же именем, которые существуют в разных модулях). Тот факт, что модуль очерчивает свое личное пространство имен, помогает избежать конфликта:
вы можете предварять совпадающие названия методов именем модуля (что-то вроде Module.method), и тогда компилятор не столкнётся с неоднозначностью. Фактически, как автор библиотеки, вы всегда должны приписывать название модуля, предвидя возможную путаницу с именами.
Вот еще одна, немного извращенная, иллюстрация:
__working_with_modules/trig.cr__
module Trig
PI = 3.141592654
def self.sin(x)
puts "Calculating the sin of #{x}"
end
def self.cos(x)
# ..
end
end
Файл trig.cr описывает модуль Trig для неких тригонометрических вычислений, в частности, функцию sin.
Наш пример, несомненно, нуждается в моральной оценке (по меньшей мере, логичность этих конструкций выглядит сомнительно). Поэтому в файле moral.cr у нас есть модуль "Moral", тоже с методом sin.
__working_with_modules/moral.cr__
module Moral
VERY_BAD = 0
BAD = 1
def self.sin(badness)
puts "Assessing the sin of #{badness}"
end
end
Методы верхнего уровня в модулях подобны методам классов они имеют префикс self и в исходном коде выглядят как словоформа "Module.method" (имена модуля и метода, сцепленные точкой).
Давайте используем "require" для структурирования кода и переноса в другой файл:
__working_with_modules/namespaces.cr__
require "./trig"
require "./moral"
y = sin(Trig::PI/4) # => Error: undefined method 'sin'
y = Trig.sin(Trig::PI/4) # => Calculating the sin of 0.7853...
wrongdoing = Moral.sin(Moral::VERY_BAD) # => Assessing the sin of 0
Как видите, тут предварительно пишется имя модуля, за которым следует имя метода (например, Trig.sin или Moral.sin), это нужно для безошибочной работы компилятора.
ВАШ ЧЕРЕД 1.
Используйте встроенную функцию Sin:
Мы не могли устоять перед соблазном показать всю порочность использования одноименных синонимов, но в реальном проекте, конечно, вы бы использовали встроенный математический модуль для вычисления тригонометрических функций. Перезапишите код таким образом, чтобы он использовал штатный модуль дистрибутива Crystal, где уже подготовлены методы вроде "sin". (Подсказка: поскольку модуль Math является совместно используемым, вам не потребуется "require". Ищите файл "math_sin.cr".)
Правила видимости, применяемые для классов в предыдущей главе, также работают и здесь. К примеру, вы можете использовать "protected внутри модуля:
__working_with_modules/visibility.cr__
module Languages
class Crystal
protected def shout
puts "Hello, I am written in Crystal"
end
end
class Ruby
def shout
Crystal.new.shout
end
end
end
Languages::Ruby.new.shout # => Hello, I am written in Crystal
Languages::Crystal.new.shout
# => Error: protected method 'shout' called for Languages::Crystal
Первое сообщение демонстрирует, что классы Crystal и Ruby живут в одном и том же пространстве имен, называемом "Languages". Но второе уведомляет об ошибке компилятора, потому что "shout" в классе Crystal отгорожен от прочих, поэтому его нельзя использовать как простую функцию верхнего уровня.
_Позволь модулям расширить себя_
Префикс self для методов верхнего уровня кому-то может показаться довольно обременительным. Вы можете избавиться от него, сделав модуль саморасширяющимся:
__working_with_modules/moral2.cr__
module Moral
extend self
VERY_BAD = 0
BAD = 1
def sin(badness)
puts "Assessing the sin of #{badness}"
end
end
Вы увидите, что это обычная для модулей практика например, модуль Math использует его в своем коде. Когда включен "extend", модуль считается пространством имен, и можно обозначать методы словоформой "Module.method", (например, Math.sin). И если для вас это выглядит как классовый метод, так оно и есть! Запомните: модуль относится к типу Class.
__working_with_modules/namespaces.cr__
require "./moral2"
y = Math.sin(Math::PI/4) # => 0.70710678118654746
wrongdoing = Moral.sin(Moral::VERY_BAD) # => Assessing the sin of 0
Если вы применили "include" для модуля, можно даже вызывать его методы, не указывая пространство имен до тех пор, пока не случится совпадение имен:
__working_with_modules/namespaces2.cr__
require "./moral2"
include Moral
y = Math.sin(Math::PI/4) # => 0.70710678118654746
wrongdoing = sin(Moral::VERY_BAD) # => Assessing the sin of 0
_Смешивание в модулях_
Модули составляют из методов функциональные группы, и потом вы можете использовать их потенциал в различных классах. Это возможно, если код модуля станет "коммунальным" для других экземпляров, как в ЯП Ruby или Dart. Микширование осуществляется с помощью ключевого слова include; тогда происходит смещение и перекрытие зон видимости (и доступности).
Когда в коде класса находится инструкция "include ModuleName", его объекты могут использовать методы
включенного модуля так же, как и свои собственные методы! Вы видели пример в параграфе "Организация кода в классах и модулях". Вот еще один пример:
Оба класса (DVD и BlueRay) включают в себя модуль Debug, поэтому объекты, образованные от этих классов, могут использовать его метод "who_am_i?" запросто:
__working_with_modules/mixins.cr__
class Basic
def initialize(@name : String)
end
def to_s
@name
end
end
module Debug
def who_am_i?
"#{self.class.name} (\##{self.object_id}): #{self.to_s}"
end
end
class DVD < Basic
include Debug
# ...
end
class BlueRay < Basic
include Debug
# ...
end
dv = DVD.new("West End Blues")
br = BlueRay.new("Attack of the Martians")
dv.who_am_i? # => DVD (#40886016): West End Blues
br.who_am_i? # => BlueRay (#40885984): Attack of the Martians
Видите сочетание символов "\#" в третьей секции примера? Это для тех случаев, когда в отправляемую на вывод строку требуется поместить символ #.
Чтобы использовать методы модуля для самого класса, нужно расширить модуль с помощью extend:
__working_with_modules/mixins.cr__
module DebugC
def who_am_i?
"#{self.class.name}: #{self.to_s}"
end
end
class CD < Basic
extend DebugC
# ...
end
cd = CD.new("Bach's Cello Suites")
cd.who_am_i? # => Error: undefined method 'who_am_i?' for CD
CD.who_am_i? # => "Class: CD"
Модули также могут содержать абстрактные методы. И каждый класс, который применяет "include" в отношении абстрактных методов, должен сформировать свой собственный вариант реализации для этих методов. Вы увидите примеры этого в разделе "Применение встроенных модулей".
Класс может включать (и расширять) множество модулей, таким образом импортируя различные способности и заимствуя нехарактерные линии поведения; стало быть, этот механизм является разновидностью множественного наследования. Но постойте не станет ли это предпосылкой для вполне вероятных коллизий имен? Нет, и в следующем разделе показано, почему этого не случится.
_Как компилятор находит методы_
Как же среди всевозможных комбинаций модулей и классов компилятор всегда находит правильный метод? Вот в чем секрет:
Каждый класс (или тип) снабжен списком предков, где перечислены родительские классы сверху донизу, начиная с первичного (суперкласса) и вплоть до конечного объекта. Компилятор ищет методы в этом списке, просматривая содержимое конкретного класса до принадлежащего ему объекта (иначе говоря, экземпляра класса). Если метод не найден, вы получите ошибку компиляции вроде этой:
error: undefined method 'method_name' for 'class_name
Это очередное отличие от языка Ruby, где вы получаете ошибку времени выполнения NoMethodError. Что происходит, когда модули включены в класс, и один из них имеет метод с тем же именем, что и метод в одном из родительских классов? Модули приобщаются к "списку предшественников", причем метод модуля получает высший приоритет. Посмотрите, что произойдет в следующем примере:
__working_with_modules/ancestors.cr__
module M1
def meth1
41
end
end
class C1
def meth1
42
end
end
class D1 < C1
include M1
end
class E1 < D1
end
E1.new.m1 # => Error: undefined method 'm1' for E1
E1.new.meth1 # => 41 # meth1 from module M1 is called
Будет вызван meth1 из модуля M1, а не "meth1 из класса C1. Если свериться со списком предков, нетрудно заметить: E1 является родоначальником D1, который является предком M1, который является предшественником C1, который является классом, непосредственно порождающим объект.
_Применение встроенных модулей_
Изучая стандартную библиотеку Crystal, вы можете заметить, что многие сегменты кода в ней на самом деле модули. Давайте взглянем на некоторые из них.
ВЫПОЛНЯЕМ СОПОСТАВЛЕНИЕ.
Предположим, вы хотите иметь возможность упорядочивать типичные экземпляры, используя операторы "<", ">=", и ещё "==". Вы бы сами определили все эти операторы сравнения? Конечно нет, потому что универсальный модуль Comparable(T) уже сделал это для вас. Подключив этот модуль, вы получите готовые операторы без лишних усилий.
Есть лишь один "подводный камень": Comparable имеет один абстрактный метод, реализацию которого ваш класс должен иметь. Это так называемый "оператор космического корабля": abstract def <=> (other : T).
Все остальные сравнения выполняются с помощью "<=>". Ваш код должен работать так: сравнить свойство текущего объекта со свойством другого, а затем вернуть "-1", "0 или "1", в зависимости от того,
свойство текущего объекта "меньше, "равно" или "больше" свойства другого объекта. Давайте рассмотрим
на конкретном примере, где мы сравниваем объекты Person по их возрасту:
__working_with_modules/comparable.cr__
class Person
include Comparable(Person)
getter name, age
def initialize(@name : String, @age : Int32)
end
def <=>(other : self) # other must be of type self, which is Person
if self.age < other.age # here self is the current Person object
-1
elsif self.age > other.age
1
else # == case
0
end
end
end
Поскольку оператор "<=>" также годится и для чисел, мы можем сократить наш код следующим образом:
__working_with_modules/comparable.cr__
def <=>(other : self) # other must be of type self, which is Person
self.age <=> other.age
end
Непременно гляньте на исходный код в файле "comparable.cr" [Сайт github.com/crystal-lang/crystal/blob/master/src/comparable.cr] он написан полностью на языке Crystal! Видишь "Т"? Это говорит о том, что модуль является "внутриклановым" (т.е. общегрупповым): он работает для любого типа "T", способного реализовать "<=>". Вот почему нам понадобилось написать в примере выше:
include Compatible (Person).
ИСПОЛЬЗОВАНИЕ ПЕРЕЧИСЛЕНИЙ.
Добавление этого модуля предоставляет коллекционируемым типам солидный набор методов для запрашивания, поиска и фильтрации данных. Единственное требование состоит в том, что тип обязан реализовать метод each, который возвращает последующие элементы собранными в коллекцию, используя выдачу (yield).
Этот фрагмент кода определяет класс "Sequence", который содержит все целые числа от нуля до определенного числа "@top". Класс Sequence включает в себя модуль Enumerable:
__working_with_modules/enumerable.cr__
class Sequence
include Enumerable(Int32)
def initialize(@top : Int32)
end
def each
0.upto(@top) do |num|
yield num
end
end
end
seq = Sequence.new(7)
# using some methods of module Enumerable:
seq.to_a # => [0, 1, 2, 3, 4, 5, 6, 7]
seq.select &.even? # => [0, 2, 4, 6]
seq.map { |x| x ** 2 } # => [0, 1, 4, 9, 16, 25, 36, 49]
РАБОТА С ИТЕРАТОРАМИ.
Большинство методов Enumerable возвращают данные в виде массива: они эффектно обрабатывают коллекцию. Это здорово работает для небольших объемов данных, но создает проблемы производительности по мере того, как объем информации растет.
Если количество предметов в коллекции велико и вам не нужно обрабатывать их все одновременно, вам нужна ленивая альтернатива, которая не охватывает за раз все члены коллекции. Именно для этого был создан Итератор. Он подключает модуль Enumerable, в котором переопределяет множество методов. Чтобы реализовать "Iterator", классу нужен программный код метода "next". Но вы также можете использовать классовый метод "of" (работающий аналогично "next"), как в этих примерах:
__working_with_modules/iterator.cr__
n = 0
inc = Iterator.of do
n += 1
n
end
inc.next # => 1
inc.next # => 2
inc.next # => 3
n = 0
m = 1
fib = Iterator.of { ret = n; n = m; m += ret; ret }
fib
.select { |x| x.even? }
.first(10)
.to_a # => [0, 2, 8, 34, 144, 610, 2584, 10946, 46368, 196418]
Хотя ряд Фибоначчи бесконечен, вычисляются лишь те элементы (первые 10), которые вам нужны.
ВАШ ЧЕРЁД 2.
Как вы измените пример comparable.cr, если потребуется сравнить пользователей, следуя алфавитному порядку их имён?
_Заключение_
Модули позволяют лучше организовать исходный код, используя пространства имен, и упростить создание единого источника общих функциональных возможностей, распространяемых между классами. По мере укрупнения ваших проектов, представления о модулях изменятся: от "просто полезные" до "необходимые".
Всегда используйте модуль для оформления проекта. Извлеките свойственные методу функциональные возможности в модуль, чтобы его можно было многократно использовать, включая в различные классы. Уделите немного времени на ознакомление с модулями, которые распространяются со стандартной библиотекой Crystal; и тогда вы сможете повторно использовать их для написания программ, или же, опираясь на изученные примеры, смоделировать модуль собственного изготовления (так сказать, по образу и подобию).
Мы займемся этим прямо в следующей главе, используя штатные инструменты Crystal для создания проектов и управления ими.
Глава 7.
Управление проектами.
В этой главе вы увидите, как организована технология разработки в среде Crystal. Вы сможете настраивать инструменты Crystal для создания, расширения, тестирования и документирования своих программных проектов. Возможно, вы также захотите управлять производительностью ваших проектов, поэтому мы также рассмотрим подготовку исходного кода к бенчмаркингу (эталонному тестированию). Этот проектный конвейер поможет навести глянец, необходимый для создания эффектного исходного кода, который захотят использовать другие люди; и ещё это поможет сэкономить ваше драгоценное время!
_Создадим Shard_
Ознакомьтесь с некоторыми проектами Crystal на GitHub, такими как "sentry tool" или оператор MongoDB. Вы заметите, что все они имеют одинаковую простую структуру, которую мы быстренько разберем. Это сходство не совпадение: все они были созданы по стандартной методике Crystal. После клика по метке "New Application" штатный инструментарий создает базовую структуру каталогов для будущей программы, чтобы
на этом остове вырос новый проект. Так что шаблонные проекты Crystal, шарды (перевод. осколки, фрагменты), имеют общую структуру это делает разбор их кода легким и доступным.
Команда для создания нового проекта имеет следующую структуру:
$ crystal init TYPE NAME [Имя типа кристалла [DIR]
TYPE может выглядеть как "app" (для приложения) или "lib" (когда вы приступаете к написанию библиотеки). NAME это, конечно, название проекта. Необязательный аргумент DIR это папка, в которой будет сгенерирован шаблон вашего проекта. Начиная с этой записи, параметр TYPE для разных задач требует подходящий "shard.yml": при создании приложения будет добавлена секция "targets".
Попробуем создать приложение с названием "proj1":
$ crystal init app proj1
Изучите структуру данных, выведенных в результате этой команды:
<= $ crystal init app proj1
create proj1/.gitignore
create proj1/.editorconfig
create proj1/LICENSE
create proj1/README.md
create proj1/.travis.yml
create proj1/shard.yml
create proj1/src/proj1.cr
create proj1/spec/spec_helper.cr
create proj1/spec/proj1_spec.cr
Initialized empty Git repository in ~/proj1/.git/
Как видим, было создано несколько взаимосвязанных вещей:
Папка с одноименным названием (в данном случае proj1).
Файлы README и LICENSE.
Файл .travis.yml, который легко интегрируется с Travis для последовательной компоновки и сборки.
Файл shard.yml для управления зависимостями (во многом похожий на "gemfile" в ЯП Руби).
Папка "src", содержащая исходный код приложения.
Папка спецификаций "spec", содержащая тесты приложения.
Инициализированный и пустой репозиторий Git, облегчающий контроль над версиями (дубликатами одноименных исходников) и публикацию вашего проекта на GitHub или подобных сайтах.
Файл .editorconfig, отвечающий за согласованную обработку пробелов для многих редакторов, которые гипотетически могут использоваться в работе над вашим исходным кодом.
После добавления внешних библиотек (что мы и займемся прямо сейчас), скажем "lib1" и "lib2", более подробный вид структуры будет таким:
proj1
bin
lib
lib1
lib1.cr
lib2
lib2.cr
src
proj1.cr
spec
proj1_spec.cr
spec_helper.cr
Внешние библиотеки содержатся в папке "lib". Все необходимые исполняемые файлы будут находиться в папке bin.
Рассмотрим сгенерированный код. Мы видим, что был создан модуль с именем проекта. Исходник "proj1.cr" является нашим основным файлом и содержит следующий код:
# TODO: Write documentation for `Proj1`
module Proj1
VERSION = "0.1.0"
# TODO: Put your code here
puts "app proj1 is started"
end
Мы добавили puts, чтобы вывод в пользовательский интерфейс сопровождал запуск приложения. Папка спецификации для тестов содержит файл "proj1_spec.cr" с первой строкой следующего содержания:
require "./spec_helper"
Поиск приведет к обнаружению файла spec_helper.cr, который содержит следующий код:
require "spec"
require "../src/proj1"
Первой строке требуется сектор стандартной библиотеки, "spec". Вторая строка требует основной файл проекта "proj1.cr". Чтобы протестировать приложение, необходимо иметь доступ к его коду, верно? И как теперь запустить наше приложение? В окне терминала перейдите к папке "proj1" и введите следующее:
$ crystal src/proj1.cr
Crystal пройдется по вышеописанным ключевым позициям для запуска приложения. Вы должны увидеть сообщение App 'proj1' is started, значит программа стартовала успешно.
Чтобы создать исполняемый файл, используйте эту команду:
$ crystal build src/proj1.cr
Тогда в папке "proj1" появится двоичный файл "proj1", который вы сможете запустить командой:
$ ./proj1
Это объединяет все описанные вами узловые точки проектного скелета в один исполняемый двоичный файл. Можно переместить этот двоичный файл (иначе бинарный файл, бинарник) туда, где вы хотите запустить свою программу. Исполнение кода не должно осложниться какими-либо зависимостями (просто попробуйте!). Если ваше приложение создано для реальной работы под нагрузкой, при сборке исполняемого файла поставьте флаг "-- release" для включения оптимизации.
Отыщите исходный код драйвера MongoDB для Crystal [github.com/datanoise/mongo.cr.]. Проанализируйте структуру этого проекта и убедитесь, что вы понимаете все сформулированные строками "require" требования (т.е. привязки ко всем необходимым компонентам).
ВАШ ЧЕРЕД 1.
С нуля создайте новый проект, называемый "mineral", (воспользуйтесь командой "crystal init") и попытайтесь включить в него из предыдущих глав (2 "Основы ЯП Crystal, 5 "Использование классов и структур") столько кода, сколько сможете.
_Форматирование кода_
Наилучший подход к форматированию и разметке исходного кода всегда был важным и принципиальным вопросом для любого ЯП с момента появления дисциплины "Программирование". Следуя примеру успешного внедрения "gofmt" (в ЯП Go) и Rustfmt (в ЯП Rust), Crystal обзавелся собственным агентом разметки для форматирования вашего кода сообразно рекомендуемому стилю.
Он автоматически обрабатывает все файлы с расширением ".cr" в текущем каталоге и даже форматирует образцы кода, встреченные в технической документации, поставляемой с языком Crystal. Вызов форматировщика выглядит так:
$ crystal tool format file.cr
Если не дописать имя файла в командной строке, авторазметчик Crystal разделается со всеми исходными файлами в папке. И хоть об этом можно и позабыть, все больше текстовых редакторов (вроде Sublime Text) и сред разработки (таких как Visual Studio Code), включают "tool format" самовольно, выполняя форматирование автоматически при сохранении файла. Попробуйте сотворить малопонятный исходный код, основательно испортив форматирование в каком-нибудь текстовом файле, содержащем побольше кода (например, classes_and_structs/classes.cr), и посмотрите, как форматировщик Кристалла восстановит изначальную красоту.
_Документирование проекта_
Вы регулярно документируете свой код, правда? Crystal делает это легко и непринужденно: он без проблем распознаёт документы с разметкой "Markdown". В ней нет какого-либо специального синтаксиса, не считая символа "#", и потому все ваши комментарии будут считаться кусочками технической документации.
Сначала прочтите руководство [crystal-lang.org/docs/conventions/documenting_code.html].
Теперь давайте добавим несколько пространных комментариев к вашему проекту "mineral" (вы ведь сделали последнее упражнение, не так ли?). Вот пример:
__managing_projects/mineral/src/mineral.cr__
require "./mineral/*"
module Mineral
puts "app mineral is started!"
module Hardness
def data
mohs = {"talc" => 1, "gold" => 2.5, "calcite" => 3,
"apatite" => 5, "corundum" => 9}
end
def hardness
data[self.name]
end
end
# Every Mineral has **hardness** (see the `Hardness` module).
#
# To create a standard rocky Mineral:
#
# ```
# min1 = Mineral.new(108)
# min1.to_s
# ```
#
# The above produces:
#
# ```text
# "This is a mineral with id 108 and is called rock"
# ```
#
# Checks the hardness with `#hardness`.
class Mineral
include Hardness
getter id, name
setter crystal_struct
# Creates a mineral with given parameters
def initialize(@id : Int32, @name : String, @crystal_struct : String)
end
# Creates a mineral with name "rock", 0 hardness and "unknown" structure
def initialize(@id : Int32)
@name = "rock"
@crystal_struct = "unknown"
end
# Prints out a description of this mineral
def to_s
puts "This is a mineral with id #{id} and it is called #{name} "
puts "It has #{crystal_struct} as crystal structure"
end
# Returns object properties in csv-format
def to_csv
"#{id},#{name},#{hardness},#{crystal_struct}"
end
def ==(other : self)
id == other.id
end
def ==(other)
false
end
# Returns crystal structure of this mineral
def kind_of_crystal
@crystal_struct
end
# :nodoc:
class Helper # no docs are created for this class
end # neither for private or protected classes
end
end
Crystal поставляется со встроенным генератором документации, который также используется для собственного API этого языка. Чтобы сгенерировать документацию для вашего проекта, запустите терминал и перейдите внутрь корневой папки проекта. Затем напечатайте следующее:
$ crystal docs
Это сотворит папку doc, где документация вашего проекта будет организована наподобие веб-сайта. Лицезреть её можно, запустив файл "doc/index.html". На рисунке видна часть веб-страницы, посвященной классу "Mineral".
\РИС.\
Используйте слова-указатели вроде BUG, DEPRECATED, FIXME, NOTE, OPTIMIZE и TODO в своих документах. Как сигнальные флажки, они помогут вам или другим читателям понять, что же ещё предстоит сделать.
_Написание тестов с помощью Spec_
Теперь, когда у нас есть хорошо отформатированный и документированный проект, давайте посмотрим, как писать тесты для нашего кода. Помните: лишенный тестов проект как бы напоминает о бедствии, ожидающем своего часа. Тесты гарантируют, что написанный вами код работает, и вы не боитесь изменять свой код, потому что это не подпортит его нынешнюю функциональность. Ещё тесты помогут другим пользователям заметить нюансы работы вашего программного кода.
Crystal располагает собственным фреймворком для модульного тестирования (называемым "spec"), который похож на "RSpec", используемый в Ruby. На практике "spec" предлагает разработчику набор единичных тестов, проводимых с файлом-исходником, который тестируется с помощью соответствующего файла с окончанием "_spec".
Давайте рассмотрим простейший файл "mineral/spec/mineral_spec.cr" стандартного вида (без оператора require):
describe Mineral do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end
Если вы изучали Ruby, то, возможно, опознали ключевые слова "describe, it" и "should", которые можно встретить в спецификациях DSL:
"describe" позволяет сгруппировать родственные спецификации.
"it" используют, чтобы охарактеризовать спецификацию. Заключите её имя в двойные кавычки, а тестовый код разместите между "do и end".
"should" для того, чтобы выразить ваши ожидания и допущения относительно спецификации.
Все они оформляются по правилам блочного синтаксиса, который мы рассмотрели в главе "Работа с выдачей, процессами и блоками".
У нас есть один тест под названием "works", который, видимо, играет роль заглушки и скорее всего неработоспособен. Вы можете запустить тестирование вводом команды "spec", открыв терминал в корневой папке своего проекта:
$ crystal spec
Вот снимок экрана:
\РИС.\
Разница между тем, что мы ожидали (should), и тем, что мы получили (got), сообщает красным шрифтом об исключительной ситуации: тест Mineral works завершился аварийно.
Как правило, каждый найденный тест запускается на исполнение. При каждом неудачном испытании будет показан F, сигнализирующий о провале.
После каждого успешного тестирования вы получаете точку (.). Чтобы запустить только один тестовый файл (при том, что у вас их несколько) передайте его имя как аргумент для "crystal spec". Чтобы запустить один из внутрифайловых тестов, пропишите номер строки (nm) для его обнаружения, таким вот образом:
$ crystal spec spec/file_spec.cr:nm
Вы можете создать столько тестов, сколько захотите, настолько многословных и пространных, сколь вам будет угодно, не затрагивая эффективной производительности проекта. Тесты не войдут в состав исполняемого файла, который будет скомпилирован для вашего проекта.
Кроме того, когда вы используете "spec" без флага "--release", компиляция проходит гораздо быстрее и не задерживает процесс разработки, даже в самых крупных проектах. Используйте флаг "--release" только при развертывании приложения. Если вы устранили сбой в шаблонном тесте (вроде рассмотренного "works") и сверяете повторные результаты тестирования, вы должны увидеть зелёную точку и текст 0 failures (ноль ошибок).
Код из предыдущего раздела, который мы теперь будем тестировать, находится в папке "spec", и теперь тестовый код требует доступ к исходному коду проекта. Давайте напишем несколько тестов для нашего минерального приложения.
Мы должны проверить работоспособность нижеследующего:
Создание минерала, где некоторым параметрам уже заданы значения по умолчанию;
Создание минерала со всеми расставленными параметрами;
Создание CSV с помощью метода "to_csv";
Считывание свойства "hardness" из модуля Hardness;
Возврат оператором "==" значения "true" после сопоставления двух одинаковых минералов;
Возврат оператором "==" значения "false" при сравнении разных минералов;
Возврат отработавшим методом "kind_of_crystal" правильного значения;
А вот исходный код, который тестирует все эти функции:
__managing_projects/mineral/spec/mineral_spec.cr__
require "./spec_helper"
describe Mineral do
# TODO: Write tests
# it "works" do
# false.should eq(true)
# end
it "creates a default mineral" do
min1 = Mineral::Mineral.new(108)
min1.id.should eq(108)
min1.name.should eq("rock")
min1.crystal_struct.should eq("unknown")
end
it "creates a mineral with parameters" do
min1 = Mineral::Mineral.new(42, "apatite", "hexagonal")
min1.id.should eq(42)
min1.name.should eq("apatite")
min1.crystal_struct.should eq("hexagonal")
end
it "creates correct csv format" do
min1 = Mineral::Mineral.new(101, "gold", "cubic")
min1.to_csv.should eq("101,gold,2.5,cubic")
end
it "gold has hardness 2.5" do
min1 = Mineral::Mineral.new(42, "gold", "cubic")
min1.hardness.should eq(2.5)
end
it "== works for same mineral" do
min1 = Mineral::Mineral.new(42, "gold", "cubic")
(min1 == min1).should eq(true)
end
it "== works for different mineral" do
min1 = Mineral::Mineral.new(42, "gold", "cubic")
min2 = Mineral::Mineral.new(43, "corundum", "trigonal")
(min1 == min2).should eq(false)
end
it "kind_of_crystal works" do
min1 = Mineral::Mineral.new(42, "gold", "cubic")
(min1.kind_of_crystal).should eq("cubic")
end
end
Продолжайте дорабатывать и запускать программный код из папки "spec", а также играть с тестирующим исходным кодом.
_Использование внешних библиотек_
Когда проектируешь собственное приложение, не хочется заново "изобретать колесо". И чаще всего оказывается, что потребные вам функциональные возможности уже заложена в актуальных пакетах или библиотеках Crystal. Обычно называемые "shards", они служат аналогом самоцветов (gems) из мира Руби.
Термин "shards" это ещё и название программы, которая управляет зависимостями проекта, она работает подобно упаковщику в ЯП Ruby. Как можно приказать вашему приложению загрузить один или несколько внешних шард-пакетов?
Ваше приложение может перечислить свои зависимости в файле shard.yml (аналог gem-файла в Руби), который мы рассмотрим в подробностях. Для проекта в начальной стадии он выглядит примерно так:
name: mineral
version: 0.1.0
authors:
- Your-Name <your-email-address>
targets:
mineral:
main: src/mineral.cr
crystal: 0.22.0
license: MIT
В нем указан файл для запуска приложения, и ещё несколько пунктов, в общих чертах описывающих проект. Чтобы "привязать" внешние пакеты, нам придется создать новую секцию Зависимости.
ДОБАВИМ SHARD.
Давайте разберёмся с этой задачей, добавив функцию лог-мониторинга в наше приложение. Мы используем программу "katip" (автор - Guven Cenan) для ведения лог-файла. Чтобы сохранить наш основной проект простым и незамутненным, мы перейдем к проекту "mineral_log", в который скопирован весь исходный код из проекта "mineral".
Два шага нужно, чтобы добавить шард-пакет к проекту Crystal.
Для начала отредактируем файл shard.yml в домашней папке "mineral_log", добавив нижеследующие строки, а затем сохраним изменения:
dependencies:
katip:
github: guvencenanguvenal/katip
Проследить зависимости позволяет наглядная связь между именем внешней библиотеки и адресной ссылкой на GitHub; это безошибочно приведет к нужному исходному коду, поэтому нет необходимости лезть в центральное хранилище Crystal.
Ниже ссылки вы можете также уточнить версию используемой библиотеки:
version: 0.1.2
Crystal, как правило, не изменяет версии без вашего ведома, но упоминание конкретной версии поможет вашим коллегам понять, что именно вы сделали. Если же вам нужны последние изменения (наипозднейшая версия библиотеки), добавьте в "shard.yml" ещё такую строку:
branch: master
Теперь второй шаг следует установить библиотеку и добавить ее в текущий проект. Перейдите в корневой каталог проекта и введите в командной строке:
$ crystal shards
или ещё короче:
$ shards
Это должно произвести такой эффект:
Updating https://github.com/guvencenanguvenal/katip.git
Installing katip (version: 0.1.0)
Если добавляемый шард имеет зависимости от других шард-пакетов, они тоже будут установлены. Вы можете посмотреть, какие же библиотеки были установлены впридачу:
$ shards list
И в нашем случае выводится ответ:
Shards installed:
* katip (0.1.0)
Просмотрев внутреннюю структуру проекта, вы можете заметить, что новенького там появилось:
Папка lib, содержащая субкаталог для каждой из установленных зависимостей. Обратите внимание, что
здесь размещён исходный код, а не исполняемые файлы.
Скрытая папка ".shards", содержащая вложенную папку "Git" для каждой из установленных зависимостей.
Текстовый файл "shard.lock", где ведётся учет всех установленных шард-пакетов и их версий.
______________________________________________
Изменчивые зависимости.
Требования одной из зависимостей могут измениться, возможно потому, что вам потребовались другие версии шардов, от которых зависит ваше приложение. Проверьте это с помощью команды:
$ shards check
Если все в порядке, вы увидите сообщение: Dependencies are satisfied (т.е. все запросы удовлетворены).
В противном случае задействуйте такую команду:
$ shards update
___________________________________________
Наконец, когда шард установлен должным образом, можно начать его эксплуатацию. Во-первых, вы должны поручить своему приложению загрузить нужный код. Сделайте это, добавив в начало файла "src/mineral_log.cr" следующую строку:
require "katip"
Как вы могли видеть в параграфе "Combining Files with Require", поиск исходного кода будет осуществлен внутри папки lib. Подключить нужный "shard это лишь начало работы по его интеграции. Для Katip также потребуется немного конфигурационного кода, который сделает запуск возможным, например:
LOGGER = Katip::Logger.new
LOGGER.configure do |config|
config.loglevel = Katip::LogLevel::DEBUG
config.logclassification = Katip::LogClassification::DATE_DAY
config.path = "src/katip/logfiles"
config.info.description = "This is the Mineral Log project."
config.info.project = "Mineral Log."
config.info.version = MineralLog::VERSION # project version
end
Обратите внимание, как был создан журнальный объект, мы взяли константу LOGGER, чтобы иметь доступ к нему из модуля "MineralLog" и класса "Mineral".
Теперь всё настроено и готово, и мы можем приступить к накоплению лог-сообщений, регистрируя события вроде запуска программы, или создания объекта "Mineral, или вызова "to_csv", и многие другие:
__managing_projects/mineral_log/src/mineral_log.cr__
module MineralLog
LOGGER.info("app mineral_log is started!")
min1 = Mineral.new(101, "gold", "cubic")
puts min1.to_csv
end
class Mineral
getter id, name
property crystal_struct
def initialize(@id : Int32, @name : String, @crystal_struct : String)
LOGGER.debug("A new mineral is created!")
end
def initialize(@id : Int32, logger)
@name = "rock"
@crystal_struct = "unknown"
LOGGER.debug("A new default mineral is created!")
end
def to_s
puts "This is a mineral with id #{id} and is called #{name} "
puts "It has #{crystal_struct} as crystal structure"
end
def to_csv
LOGGER.debug("to_csv method is called")
"#{id},#{name},#{crystal_struct}"
end
end
Теперь лог-файл журнала можно найти в каталоге "src/katip/logfiles/*.json". Посмотреть на работу регистратора и оценить состояние журналов можно, запустив "mineral_log/lib/katip/katipviewer.html" в адресной строке браузера:
\РИС.\
Логгер Katip классифицирует сообщения, ориентируясь на характер события: "info", "warn"(Предупреждение), "debug", "error" и "fatal"(Непоправимая ошибка), позволяя вам выбрать, что именно регистрировать и когда желательно просматривать логи.
___________________________________________________
Нет коллективных зависимостей.
В ЯП Crystal каждое приложение имеет свою личную копию исходников для всех шард-пакетов, от которых оно зависит. Все они компилируются в готовый к запуску исполняемый файл. И не возникнет ни одной общей зависимости для двух (и более) приложений до тех пор, пока исходный код пишется на чистом Crystal (без фрагментов кода от других ЯП). Такой расклад имеет несколько плюсов:
Никакого бардака: нет необходимости регулярно сверять версии шардов! Вы просто знаете, с какими версиями работает ваше приложение, и файл "shard.lock" поможет удостовериться, какой именно экземпляр шард-пакета используется теперь.
Вместе с приложением удаляются и все его зависимости, не оставляя после себя никакого кода.
Если есть проблема с зависимостью, вы можете отладить её или даже переделать, не затрагивая другие приложения (которые могут требовать тех же самых внешних библиотек).
________________________________________________
_Сравнительный анализ производительности_
Иногда вам нужно исследовать проблему производительности в Crystal-программе, или вы хотите узнать
какой из двух возможных алгоритмов является самым быстрым или более эффективным. В этом случае используйте штатный модуль "Benchmark", характерными чертами которого являются удобство, простота и отзывчивость. Он может показаться знакомым, если раньше вы пользовались бенчмаркинговым модулем в Ruby.
В "to_s(io)" (в Главе 5) вы узнали, что добавление простого объекта намного быстрее, чем добавление
строки, выполняемое вставкой "#{}" или "to_s". Теперь, когда мы можем удостовериться в этом на практике, давайте сравним эти три задачи. Как вы уже знаете, мы работаем с объектами IO::Memory, чтобы повысить
эффективность. Давайте посмотрим, как это происходит:
__managing_projects/benchmarking.cr__
require "benchmark"
IOM = IO::Memory.new
Benchmark.ips do |x|
x.report("Appending") do
append
IOM.clear
end
x.report("Using to_s") do
to_s
IOM.clear
end
x.report("Interpolation") do
interpolation
IOM.clear
end
end
def append
IOM << 42
end
def to_s
IOM << 42.to_s
end
def interpolation
IOM << "#{42}"
end
Мы обращаемся к модулю "Benchmark", который будет обнаружен в стандартной библиотеке Кристалла. Затем мы вызываем метод "Benchmark.ips", который отсчитывает итерации в секундном интервале. Каждая задача объединена в блок вида "do... end" и выполняется огромное количество раз, накапливая данные для сравнения. Задача вызывается как метод, что не является оптимальным и эффективным подходом; тем не менее, это делается для приближения к условиям реальной эксплуатации приложения. После этого метод "report" выдает результаты в удобном виде.
Постройте исполняемый код путем компиляции исходника:
$ crystal build benchmarking.cr --release
и запустите готовую программу с помощью
$ ./benchmarking
Вы получите примерно такие результаты:
Appending 34.06M ( 29.36ns) ( 3.97%) fastest
Using to_s 12.67M ( 78.92ns) ( 7.55%) 2.69 x Slower
Interpolation 2.8M (356.75ns) ( 3.84%) 12.15 x Slower
Теперь мы убедились, что добавление объекта действительно проходит быстрее.
Ещё вам может пригодиться метод bm, который выдает отчёт о затраченном времени:
__managing_projects/benchmarking.cr__
Benchmark.bm do |x|
x.report("Appending bm") do
IOM.clear
10_000_000.times do
append
end
end
end
user system total real
Appending bm 0.240000 0.000000 0.240000 ( 0.243686)
Пользуясь этим методом, программист каждый раз задает цикл (так сказать, "по своему хотению").
ВАШ ЧЕРЕД 2.
1) ArrayLastElem: Чтобы получить последний элемент массива, какой способ является самым быстрым: использование индекса "-1" или вызов метода "last"?
2) Building_vs_Concat: В главе 3 ("Использование строковых методов") мы утверждали, что создание строки намного эффективнее конкатенации (т.е. объединения). Докажите это посредством бенчмаркинга, применяя метод bm. Используйте строки для объединения или добавления.
_Презентация приложения Crystal_
Завершив тестирование и бенчмаркинг, вы наверняка захотите поделиться рабочей версией своего блестящего приложения. Это легко: просто распространяйте бинарный файл! Поскольку все нужные компоненты компилируются вместе, на выходе будет лишь один исполняемый файл. Убедитесь, что флаг "-release" был установлен для компиляции готовой версии вашего приложения (как описано в параграфе "Compiling Code"), тогда компилятор сработает в режиме для оптимизации производительности.
_Заключение_
Поздравляем! Пробравшись через дебри этой главы (завершающей вторую часть нашей книги), вы сделали большой шаг от привычного кодерства на Crystal к тому, чтобы сотворить свой собственный shard.
И более того, теперь вы знаете как стандартизировать свой код, документировать его, проводить тестирование и сравнительную оценку производительности и, наконец, в полной мере использовать шард-пакеты из мира Crystal. В части III расширятся ваши познания о Crystal: как писать макросы, получать доступ к базам данных и создавать параллельно исполняемый программный код.
ЧАСТЬ 3.
Продвинутый Crystal-кодинг.
В этой части мы изучим передовые концепции программирования, которые могут потребоваться для реализации неоторых проектов: использование макросов, многопоточность, взаимодействие с базами данных и привязка к библиотекам Си. Многим приложениям также требуется веб-фреймворк, и здесь Crystal выделяется своими яркими и действенными решениями. А ещё мы глубже заглянем в экосистему Shard'ов.
Глава 8.
Передовые возможности Кристалла.
Теперь, когда вы познали основы языка Crystal, давайте рассмотрим дополнительно те грани, которые очень пригодятся в реальной разработке ПО:
Макросы позволяют избежать шаблонного кода;
Каждый современный язык программирования, который уважает себя, способен их использовать. Кристалл уважает себя, и он может.
Связывание с библиотеками C;
Зачастую хорошая библиотека Cи, реализующая ваши потребности, уже давно написана и просто ждет, когда вы захотите ею воспользоваться. С Кристаллом это несложно: выпишите имена и типы функций Cи, и можно приступать.
Распараллеливание: одновременное выполнение нескольких фрагментов кода. Применяя его с осторожностью, мы улучшим "скорострельность" и КПД своих программ.
Доступ к базе данных;
Большинству приложений приходится где-то хранить свои данные, стало быть, без лишних раздумий и колебаний признайте вам это тоже нужно. В Crystal это делается единообразно (один универсальный способ).
Все эти штуки помогут вам сохранить данные приложения и повысить его быстродействие. Вы сможете повторно использовать функциональность, которая есть в языке Cи, и вы сделаете свой код более толковым и лаконичным с помощью метапрограммирования. Хоть эти темы не связаны напрямую, их разумное применение выведет вас на следующий уровень Crystal-кодерства.
_DRY Ваш код с макросами_
Предшественник и вдохновитель Кристалла, язык Ruby, является мастером самоанализа в режиме исполнения кода, и также ловко манипулирует программным кодом; всё это называется метапрограммированием. Метапрограммирование является "золотым ключиком" мудрёного фреймворка "Ruby on Rails", но в ЯП Ruby нет макросов. Crystal же компилируемый язык, поэтому у него отсутствует метод "eval" для создания нового кода во время прогона программы (т.е. в режиме Runtime). Он должен идти другим путем, компетентная система макросов достроит код во время компиляции и это потребует больше времени.
Когда пишете исходный код, вы можете вдруг обнаружить себя пишущим методы, которые фактически дублируют друг друга, всё различие в именах и параметрах. Что может помочь в подобных случаях? Такой подход, когда можно сгенерировать весь этот код автоматически, используя одну макро-версию метода.
Макрос это функция, вызываемая при компиляции исходного кода. В результате работы макроса образуется ещё больше кода, который тоже компилируется. Макросы помогают программисту сделать больше, написав меньше кода.
Меньший объем кода это значит меньше ошибок, как правило. Не повторяйтесь, лучше следуйте принципу DRY (Dont Repeat Yourself)!
Давайте, к примеру, попробуем реализовать макрос, который умеет возвращать значения переменных. Начнем с простого класса Mineral с атрибутами "name" и "hardness", как будто мы знать не знаем о "getter":
__advanced_features/macros.cr__
class Mineral
def initialize(@name : String, @hardness : Float64)
end
def name
@name
end
def hardness
@hardness
end
end
min1 = Mineral.new("gold", 2.5)
"#{min1.name} - #{min1.hardness}" # => "gold - 2.5"
Как видим, код дублируется: для обоих атрибутов создан одноименный метод, который выводит соответствующее значение. Но мы хотим сотворить нечто подобное, используя новый макрос под названием get, так ведь?
class Mineral
def initialize(@name : String, @hardness : Float64)
end
get name
get hardness
end
### Можно записать и проще, в одну строку:
#get name, hardness
Давайте сделаем это пошагово:
1) Сначала скопируйте метод name в макрос "get", вот так:
__advanced_features/macros.cr__
macro get
def name
@name
end
end
class Mineral
def initialize(@name : String, @hardness : Float64)
end
get
def hardness
@hardness
end
end
min1 = Mineral.new("gold", 2.5)
"#{min1.name} - #{min1.hardness}" # => "gold - 2.5"
Этот код по-прежнему работает: теперь макрос "get" вставит в тело исходника код для метода "name". Макрос определен почти как обыкновенный метод. Но вместо "def" используется ключевое слово "macro".
2) Поскольку мы хотим макроопределения для всех атрибутов, мы должны обобщить код:
__advanced_features/macros.cr__
macro get(prop)
def {{prop}}
@{{prop}}
end
end
class Mineral
def initialize(@name : String, @hardness : Float64)
end
get name
get hardness
end
min1 = Mineral.new("gold", 2.5)
"#{min1.name} - #{min1.hardness}" # => "gold - 2.5"
Макрос get теперь принимает параметр, prop, так что его можно использовать для каждого атрибута. В теле
макроса часто встречаются выражения {{}}. Они раскрываются при клонировании кода из макроса во время компиляции: каждое выражение внутри {{}} подставляет альтернативные значения в генерируемый код. Так что теперь можно использовать "get" для каждого атрибута.
3) Как мы можем сократить код еще больше, чтобы получилось "get name, hardness"? Поскольку мы не знаем, сколько всего атрибутов будет, мы используем Splat-вставку *. Поочередно пройтись по атрибутам нам поможет синтаксис "for ... in":
{% for prop in props %}
# code
{% end %}
Финальная версия выглядит следующим образом:
__advanced_features/macros.cr__
macro get(*props)
{% for prop in props %}
def {{prop}}
@{{prop}}
end
{% end %}
end
class Mineral
def initialize(@name : String, @hardness : Float64)
end
get name, hardness
end
min1 = Mineral.new("gold", 2.5)
"#{min1.name} - #{min1.hardness}" # => "gold - 2.5"
Язык Crystal экипирован множеством встроенных макросов, таких как getter, setter и property, которые находятся в определении класса. Эти термины не являются ключевыми словами. Они представляют собой макросы, определенные в классе "Object". Есть даже описывающий макрос "record", который может за вас определить структуру:
__advanced_features/macros.cr__
record Mineral, name : String, hardness : Float64
min1 = Mineral.new("gold", 2.5)
"#{min1.name} - #{min1.hardness}" # => "gold - 2.5"
Макросы отличный способ, чтобы усовершенствовать ЯП, и незаменимы при написании DSL (Domain Specific Languages), т.е. специфичных для предметной области языков.
Наряду с конструкцией {% for in %} применяется ещё и {% if %} {% else %}. Вы, кстати, можете использовать оба элемента и снаружи макрос-определения. Внутри макроса можно получить доступ к типичному экземпляру "type" с помощью связующей переменной @type.
Макросы могут обитать внутри модулей или классов. Они могут вызывать друг друга, и ещё макрос может рекурсивно вызвать самого себя. Однако будьте осторожны необходимо прописать определение макроса, прежде чем обращаться к нему.
Как работают макросы? Компилятор делает несколько дополнительных шагов, чтобы сконструировать актуальный исполняемый код. На этапе обработки Абстрактного синтаксического дерева (AST) наш код, содержащий синтаксис макросов, обрабатывается соответственно узловым развилкам AST.
После доработки он прирастает до нормального кода Crystal, который уже можно компилировать обычным способом. Как видите, вмешавшись в процесс компиляции, вы можете совершить несколько хитроумных маневров, и это не затормозит выполнение программы, как иногда бывает в Ruby!
ВАШ ЧЕРЁД 1.
||> def_method: Создайте макрос "define_method, который принимает имя метода (mname) и его тело, чтобы сотворить определение этого метода. Испытайте макрос, сгенерировав код для метода "greets", который печатает Hi, и для метода "add, который возвращает (1 + 2).
______________
МАКРОС-монтаж.
Из курса Ruby вы могли узнать, что возможно создать код после запуска программы (стадия Runtime) в случае, когда вызываемый метод невозможно обнаружить. Вы бы сделали это, определив "method_missing" внутри класса. В Crystal вы делаете кое-что очень похожее с помощью макроса, но здесь метод создаётся во время компиляции:
__advanced_features/macros.cr__
class Mineral
getter name, hardness
def initialize(@name : String, @hardness : Float64)
end
macro method_missing(call)
print "Unknown method: ", {{call.name.stringify}},
" with ", {{call.args.size}}, " argument(s): ",
{{call.args}}, '\n'
end
end
min1 = Mineral.new("gold", 2.5)
min1.alien_planet?(42)
# => Unknown method: alien_planet? with 1 argument(s): [42]
В предыдущем примере метод "alien_planet?" не существует в классе Mineral. Обычно это приводит к ошибке компиляции:
undefined method 'alien_planet?' for Mineral
С помощью макроса "method_missing" мы получаем доступ к имени метода и его аргументам, и в результате
увидим нечто более информативное, чем рутинное сообщение о неопознанном методе.
Тем же образом вы можете определить следующие макросы, вызываемые во время компиляции:
inherited: Если подкласс определен, @type это класс, который наследует (т.е. потомок другого класса).
included: Если модуль включен, @type это класс, включающий в себя модуль.
extended: Если модуль расширен, @type это приобщённый класс (т.е. союзный для этого модуля).
Используя эти макросы, вы сможете программировать на мета-уровне. Хотя макросы являются мощным подспорьем программиста, кодить становится сложнее. А потому не применяйте их без необходимости. Если вы действительно думаете, что вам нужен макрос, сначала напишите код без него и удостоверьтесь, что у вас получается дублированный код. Если это так, то исправьте ситуацию, написав макрос. Продолжайте углубляться в тему и вы найдете больше нюансов и возможностей в документации о макросах.
_Низкоуровневое программирование и привязки к языку Cи_
В отличие от Ruby, Crystal имеет ряд особенностей, весьма полезных в разработке встраиваемых приложений и ПО для Интернета Вещей. Например, вы можете работать с указателями любого типа, как показано тут:
__advanced_features/low_level.cr__
ptr = Pointer(UInt8).malloc(20) # malloc allocates memory
ptr.value = 42_u8 # 42 here is of type u8
ptr # => Pointer(UInt8)@0x271dfe0
ptr.value # => 42
ptr.class # => Pointer(UInt8)
# Converting between pointer types with as:
# Int8* is the same as Pointer(Int8)
ptr.as(Int8*) # => Pointer(Int8)@0x271dfe0
n = 42
ptr_n = pointerof(n)
ptr_n.value = 108
n # => 108
Указатель вообще не проверяется: такой код считается небезопасным, стало быть, порча данных в памяти, сдвиги сегментов, или другие ошибки и сбои вполне возможны. Вы сами отвечаете за последствия, компилятор здесь не помощник. Тип "Slice(T)" нечто вроде калиброваного указателя (с размером), который проходит фиксирующую проверку, поэтому его безопаснее использовать, чем обычный указатель. Часто используемый тип "Bytes" является псевдонимом для "Slice(UInt8)".
Такая открытость может показаться странной выходкой компилятора Crystal, который обычно страхует программиста, ограничивая свободу самовыражения. Зато она позволяет исследовать дальние рубежи в мире Crystal, а также облегчает взаимодействие с кодом Cи. Благодаря этим особенностям и принадлежности Кристалла к языкам КОМПИЛИРУЕМЫМ, вы можете эксплуатировать всё разнообразие библиотек Cи (компенсируя скудность Crystal-библиотек), не утруждая себя написанием дополнительного Cи-кода. (Это совершенно излишне, даже в погоне за быстродействием: Crystal в этом отношении не уступит Cи.)
В ЯП Crystal привязка к библиотеке Си настолько проста, насколько это возможно, тогда как в Ruby вам придется ещё написать Cи-код. Cи это сильно типизированный язык, как и Crystal. Вот почему они могут непринужденно беседовать. В следующем примере давайте посмотрим, как вызвать несколько функций Cи из Crystal:
__advanced_features/c_bindings.cr__
lib LibC
fun rand : UInt32
fun sleep(seconds : UInt32) : UInt32
fun atoi(str : UInt8*) : Int32
fun printf(format : UInt8*, ...) : Int32
fun cos(val : Float64) : Float64
fun exit(status : Int32) : NoReturn
end
LibC.rand # => 1804289383
LibC.sleep(1_u32) # => wait 1 second
LibC.atoi("28").class # => Int32
a = 1
b = 2
LibC.printf "%d + %d = %d\n", a, b, a + b # => 1 + 2 = 3
LibC.cos(1.5) # => 0.0707372016677029
LibC.exit(0) # => NoReturn
puts "hello" # this will never be executed
Вы видите, что сама библиотека объявлена как "lib LibC". Секция "lib" группирует функции и типы Cи,
которые принадлежат определенной библиотеке Cи допустим, "nmo", которая будет названа "LibNMO" (как это принято в Crystal). Внутри секции программист объявляет нужные ему функции Cи как "fun cfname". Необходимо указать имя, тип аргумента, а также тип данных, возвращаемый нужной функцией, точно так же, как это делается в ЯП Cи. Функции Си затем вызываются в Crystal как методы класса, входящего в библиотеку, вроде LibC.atoi("28").
Предыдущий пример был особенно простым, поскольку "LibC" определен в стандартной библиотеке и можно сказать, что он уже надежно (хоть и неявно) привязан. Для других внешних библиотек например, библиотеки SDL, применяемой в написании компьютерных игр, вам следует использовать аннотацию @[Link("")], которая может передавать флаги компоновщику для поиска библиотеки.
_______________________________________________________
Типы, используемые в привязках к Си.
Вы можете использовать примитивные типы, такие как "Int8" и "Int64", или их беззнаковые эквиваленты, или же "Float32" и "Float64". Можно также использовать типы "Pointer", например, "Pointer(Int32)", который также разрешено записывать как "Int32*".
Возвращаемое значение "Void" означает в ЯП Crystal то же самое, что и "Nil" (т.е. ноль), а "NoReturn" означает конец исполнения кода (т.е. выход из программы).
Cи-типу "char" соответствует тип "UInt8" в языке Crystal; "LibC::Char" это псевдоимя. В Crystal тип "Char" имеет размер в 4 байта, так что для Cи это совсем как "Int32". Кроме того, указательные типы языка Cи (вроде char* или const char*) в Crystal будут типом "UInt8*".
______________________________________________________
__advanced_features/c_libsdl.cr__
@[Link("SDL")]
lib LibSDL
INIT_TIMER = 0x00000001_u32
INIT_AUDIO = 0x00000010_u32
fun init = SDL_Init(flags : UInt32) : Int32
end
value = LibSDL.init(LibSDL::INIT_TIMER) # => 0
В этом примере @[Link("SDL")] передает флаг "-lSDL" компоновщику. Вы также видите, как можно сделать
простые имена функций для языка Crystal; вот короткое "init" взамен "SDL_Init". Теперь выполним простой пример, чтобы воочию узреть принцип работы линкинга (т.е. проведения связующей ссылки); "greet.c" содержит функцию "greet", которая говорит "Привет!" кому попало.
__advanced_features/greet.c__
#include <stdio.h>
void greet(const char* name){
printf("Hello %s!\n", name);
}
Скомпилируйте этот код Cи в объектный файл "greet.o", используя командную строку:
$ gcc -c greet.c -o greet.o
Теперь в коде Кристалла мы направим компоновщик туда, где он может найти объектный файл, вот так:
__advanced_features/greet.cr__
@[Link(ldflags: "#{__DIR__}/greet.o")]
lib LibSay
fun greet(name : LibC::Char*) : Void
end
LibSay.greet("Ary") # => Hello Ary!
Затем скомпилируйте и выполните привычным путем, вводя команды:
$ crystal build greet.cr
$ ./greet
Должно отобразиться приветствие.
Поскольку использование Cи-привязок небезопасно по своей сути, лучше всего написать для них безопасную обёртку, которая проверяет нулевые указатели и соответствие индексов разрешенным диапазонам. Более подробные сведения (англ.) о взаимодействии с ЯП Си ищите здесь:
https://crystal-lang.org/docs/syntax_and_semantics/c_bindings/
Если необходимо выйти за рамки обычных возможностей Crystal, но весь потенциал языка Cи вам не нужен сейчас, Crystal может обратиться к операционной системе с помощью высокоуровневой команды "system", запуская команду, предложенную ОС, во вложенной оболочке, как в этом примере, где "gedit" открывает файл "test.txt" для редактирования:
file = "test.txt"
system("gedit #{file}")
Это простое и мощное средство подвергает ваш код некоторым рискам, вытекающим из особенностей работы операционной системы. Оно также создает зависимости, которые вам придётся уладить.
_Создание мульти-синхронного кода_
Для распараллеливания Crystal использует ту же самую CSP-модель (Сообщающиеся Последовательные Процессы), что и ЯП Go: легковесные процессы, называемые волокнами, которые обмениваются данными сквозь каналы. И происходит это быстрее, с меньшей затратой ресурсов ЭВМ, чем в Node.js, к примеру. Вы узнали о волокнах и каналах из третьей главы. Перед тем, как погрузиться в многочисленные детали, давайте рассмотрим схему выполнения запускаемой в Crystal программы.
КАК ВЫПОЛНЯЕТСЯ ПРОГРАММА В СРЕДЕ Crystal?
Когда стартует исполняемый файл Кристалла, запускается главная Нить (для краткости main) для выполнения программного кода верхнего уровня (иначе говоря, сначала строится "передний план"). В этом коде можно породить много дополнительных волокон, которые выстроятся в очередь.
На заднем плане Crystal незримо дирижирует, обеспечивая минимальное время выполнения, которое слагается из нескольких стадий:
1) Диспетчер Времени Активности неустанно заботится о своевременном запуске каждой очередной нити в нужный момент. Нить, которая не может продолжать работу (поскольку ожидает завершения ввода/вывода), работает в коллективном режиме:
она просит планировщик либо перейти к запуску следующей нити, либо взять обратно недо-выполненную задачу вместе с промежуточным итогом вычислений. Планировщик также координирует обмен данными через каналы.
2) Неблокируемый цикл событий (Event Loop) это просто особенное волокно. Он отвечает за все, что связано с вводом/выводом асинхронные задачи (например, чтение файлов, пересылка сетевых пакетов, таймеры и тому подобное). Event Loop пребывает в ожидании трудоемких и длительных задач, в то время как другие волокна выполняются синхронно.
3) Сборщик мусора (Garbage Collector) очищает память, которую программа больше не использует. В 2019 году это был стандартный "Boehm GC" (в режиме "пометка-сметание").
Вы можете представить запущенную программу Crystal как кооперативный многозадачный механизм, распределяющий рабочую нагрузку между множеством маломощных линий. Своевременное переключение задач между этими фигурально выражаясь, "конвейерными лентами" позволяет балансировать, избегая непроизводительной растраты мощностей в процессе обработки данных.
Это здорово работает при интенсивной обработке ввода/вывода. Он немного менее приспособлен для интенсивной обработки данных Центральным Процессором, как во время масштабных задач по перемалыванию чисел.
Каждое волокно стартует с размером стека 4 килобайта, который максимально может вырасти до 8 мегабайт типичная квота памяти для одного потока. Если надо, вы можете создать их много, разве что наткнётесь на ограниченный потенциал устаревшего "железа". На 64-разрядной машине можно вызвать миллионы волокон. Но на 32-разрядном ПК вы сможете наделать лишь 512 штук.
Всё кристальное хозяйство (т.е. приложение плюс все причиндалы), исключая мусоросборщик (GC), выполняется в одном потоке, созданном вашей ОС. Вплоть до завершения оригинальной книги (eBook) ЯП Crystal работал только с одним потоком выполнения, но подлинная многопоточность (как в языке "Go") была уже на подходе. Сборка мусора выполняется независимо от запущенного приложения Crystal, в отдельном потоке ОС.
Присутствие встроенного планировщика подразумевает, что вам не придется самостоятельно запрограммировать распределение и переключение нитей в очереди. Это похоже на ограничительную меру, но это также значит, что вы не окажетесь в сущем аду обратных вызовов, как бывет в "Node.js" с JavaScript. Полагаясь на планировщик, вы просто пишете свой код так, будто выполняться он будет в линейном порядке.
ОБРАЗОВАНИЕ КАНАЛОВ МЕЖДУ ВОЛОКНАМИ.
Знайте, что каждое волокно создается с помощью конструкции "spawn do [какой-то код...] end",
и этот программный код будет выполняться новым волокном. Как вы думаете, что выведет следующий фрагмент?
__advanced_features/main_fiber1.cr__
puts "Before start fiber"
spawn do
puts "Hello from within fiber"
end
puts "After fiber"
Он выведет лишь две строчки (первую и последнюю):
Before start fiber
After fiber
Почему так? Главная нить (main) исчезает, когда программа завершается, и так же пропадают все дочерние нити, порожденные ею. Когда дочерняя нить появляется, она не будет тут же запущена в работу. И в этом случае она не получит шанса отобразить своё приветственное сообщение. Мы можем исправить это, добавив инструкцию "sleep 1.second" в исходный код. Тогда главная нить будет приостановлена на одну секунду, давая время новорожденной нити вывести свое сообщение. Другой (более естественный) путь добавить "Fiber.yield", который напомнит планировщику о наличии в очереди готовых нитей.
Для программиста работа с переменными общей памяти это верная дорога к ошибкам. Вместо этого предоставьте нитям возможность передавать друг другу полезные данные через типизированные каналы.
___________________________________________________
Каналы и общая память.
НЕ выстраивайте коммуникации посредством ОБЩЕЙ ПАМЯТИ. Это значит НЕ использовать общедоступные переменные для обмена данными!
Вместо этого РАЗДЕЛИТЕ ПАМЯТЬ для наладки информационного сообщения. Что значит: "давайте же отправлять и принимать данные по каналам!"
_________________________________________________
Что это за каналы? Они являются объектами класса Channel(T) и, несомненно, Channel является групповым классом. Они могут пересылать значения установленного типа из одного волокна в другое. Схема, выполненная Станиславом Козловским [https://hackernoon.com/crystal-the-ruby-youve-never-heard-of-57bad2efac9c], иллюстрирует параллелизм в Crystal:
\РИС.\
___________________
Синхронизация потоков данных.
Важна не только доставка информации по каналам безопасным путем, но также и то, что регулярная отправка и прием данных стали естественной основой для осуществления синхронизации. И канал реализует внутри себя все блокирующие механизмы, предохраняющие от аварийных рывков при доставке данных. Взгляните на этот пример вы увидите, что нить "main" посылает по каналу значение 42, которое должно достигнуть принимающей нити. Каким будет результат?
__advanced_features/main_fiber2.cr__
ch = Channel(Int32).new
spawn do
puts "start fiber"
n = ch.receive # fiber will block here if nothing on the channel yet
puts "fiber received #{n}"
end
puts "before send" (1)
ch.send 42
puts "main has send 42" (4)
# =>
(1) before send
(2) start fiber
(3) fiber received 42
(4) main has send 42
Посмотрите на программный вывод:
Сначала идет линия (1), потому что нить ещё не стартовала. Затем первичная нить "main" отправляет значение, но сама будет заблокирована до момента получения значения. Это означает, что вторичная нить может появиться на второй стадии (2), приобретая значение "42" на стадии (3), после чего будет остановлена. Теперь нить "main" может быть продолжена, и печатается последняя строка (4).
Аналогичным образом, нить, которая выполняет прием данных по каналу, блокируется до тех пор, пока значение не двинется по этому каналу. Отправка порции данных немедленно переключает в активный режим нить, ожидающую доставку на этом канале, и затем выполнение кода продолжается в этой активированной нити.
Отправка и прием значений (даже значения "nil") естественным образом синхронизирует совместную работу волокон.
ВАШ ЧЕРЁД 2.
1) "main_fiber3": Предоставьте стандартному волокну отправить значение для главного, и "main" (должно быть) получит его. Разберите и растолкуйте выведенную программой информацию.
2) "main_fiber4": Теперь пусть "main" отправляет числа от 1 до 10 по каналу, пока другое волокно печатает их
на пользовательский вывод. Затем снова реверсируйте прием и отправку (т.е. main опять получатель, второе волокно отправитель).
_________________________
Ожидание в Цикле Событий.
В следующем фрагменте кода можно увидеть цикл событий в действии: дочерняя нить ожидает ввода от клавиатуры, а "main" ждёт доставки значения (т.е. застыла в положении получателя):
__advanced_features/main_fiber5.cr__
ch = Channel(String).new
spawn do
while line = gets
ch.send(line)
end
end
puts ch.receive
# For example:
# =>
# hello
# hello
(Скомпилируйте и запустите код примера в командной строке.)
Этот пример подходит к любому вводу/выводу, например, к поступлению данных через сетевой сокет или считыванию из файла.
_______________________________
Нить, осуществляющая метод.
Зачастую нити для исполнения будет предоставлен метод вместе со значениями аргументов, и это записывается в одну строку, примерно так: "spawn method1 argumentlist". Фактически, здесь "spawn" используется как макрос. Взгляните на следующий пример:
__advanced_features/spawn_method.cr__
def pname(name)
3.times do |i|
puts "#{name} - #{i}"
end
end
spawn pname "spawned" # started on another fiber (in background)
pname("normal") # started by main fiber
Fiber.yield
# =>
# normal - 0
# normal - 1
# normal - 2
# spawned - 0
# spawned - 1
# spawned - 2
Метод "pname" сначала вызывается в обычной нити, а потом внутри "main".
________________________
Сигналы остановки.
Вот еще один пример он показывает, как рабочая нить сигнализирует о завершении своего выполнения, отсылая через канал значение, которое примет главная нить ("main"):
__advanced_features/synchronizing.cr__
# # Synchronization of channels:
# background worker signals on channel when it is done
# main fiber only continues when that signal is received
def worker(done : Channel(Bool))
puts "worker: working"
sleep 2
puts "worker: done"
done.send true
end
done = Channel(Bool).new
spawn worker(done)
done.receive # main blocks here
puts "main: next"
# =>
# worker: working
# worker: done
# main: next
____________________________
Буферизованные каналы.
Каналы, которые мы использовали до сих пор, могли вмещать в себя только одно значение. Вот почему переключение режимов (отсылка/приём) для нитей происходит мгновенно. Но ещё существуют буферизованые каналы, которые могут содержать несколько значений, указанных при инициализации канала:
__advanced_features/buffered_channel.cr__
ch = Channel(Char).new(2) # A buffered channel of capacity 2
spawn do
puts "Before send 1"
ch.send('\u03B1')
puts "Before send 2"
ch.send('\u03B2')
if ch.empty?
puts "Channel is empty"
else
puts "Channel is not empty"
end
puts "Before send 3"
ch.send('\u03C9')
puts "After send"
end
3.times do |i|
puts ch.receive
end
# =>
# Before send 1
# Before send 2
# Channel is not empty
# Before send 3
#"альфа"
#"бета"
# After send
#"омега"
И здесь переключение на другую нить произойдёт только когда буфер окажется полон. Вот почему "альфа"и "бета" будут напечатаны одно за другим. Можно проверить, продолжает ли канал удерживать значение, с помощью метода "empty?". Можно закрыть канал, если вы уже закончили с ним, а затем проверить его с помощью метода "closed?".
____________________
Выбор волокна.
Предположим, несколько волокон работают одновременно, и вы хотите сделать что-нибудь сразу же, как только одно из них выдаст результат. В Crystal имеется оператор "select when", который делает именно это:
__advanced_features/channel_select.cr__
def generator(n : T) forall T
chan = Channel(T).new
spawn do
loop do
sleep n
chan.send n
end
end
chan
end
ch1 = generator(1)
ch2 = generator(1.5)
ch3 = generator(5)
loop do
select
when n1 = ch1.receive
puts "Int: #{n1}"
when f1 = ch2.receive
puts "Float: #{f1}"
when ch3.receive
break
end
end
# Output:
# Int: 1
# Float: 1.5
# Int: 1
# Float: 1.5
# Int: 1
# Int: 1
# Float: 1.5
Здесь создаются три волокна, каждое со своим каналом. Каждое волокно спит в течение скольких-то секунд (время задается аргументом), а затем отправляет этот аргумент по каналу. Волокно повторяет эти действия в
бесконечном цикле. Поскольку секундный аргумент задается и как целочисленное, и как дробное ("float"), мы напишем групповой метод "generator". В первой строке обратите внимание на добавленное ключевое слово "forall T", которое объявляет метод общедоступным.
И теперь "main" получает значения из каналов в бесконечном цикле посредством конструкции "select when". Цикл остановлен инструкцией "break" через пять секунд, когда канал "ch3" доставлял значение получателю.
_______________________
Работа с файлами.
Последний пример также учит вас работать с файлами в контексте волокон. Можете ли вы сообразить, что делает следующий код перед запуском на исполнение?
__advanced_features/lines_files.cr__
ch = Channel(Int32).new
total_lines = 0
files = Dir.glob("*.txt")
files.each do |f|
spawn do
lines = File.read_lines(f).size
ch.send lines
end
end
files.size.times do
total_lines += ch.receive
end
puts "Total number of lines in text files: #{total_lines}"
# => Total number of lines in text files: 7
Вы, вероятно, догадались: метод "Dir.glob" принимает образец и возвращает массив с именами всех файлов, соответствующих этому образцу, в данном примере это файлы с расширением ".txt". Затем волокно размножается индивидуально для каждого текстового файла, все строки которого считываются в массив; и размер массива, разумеется, совпадает с количеством строчек текста.
Наконец, волокно передает этот результат по каналу. Главное волокно "main" накручивает циклы поочередно (сколько файлов столько и циклов понадобится), получая число строк для каждого текстового файла и накапливая общую сумму.
Это обычная идиома в параллельном программировании:
Определить несколько отдельных задач.
Выделить волокно для каждой задачи.
Предоставьте главному волокну (main) объединить и выдать результаты.
Теперь обратимся к другому важному аспекту многих программных приложений: базам данных.
_Доступ к базам данных_
Crystal может работать с множеством баз данных SQL и NoSQL. Среди них SQLite, MySQL (или MariaDB), Postgres, MongoDB, Redis, и ReThinkDB.
Команда Crystal осознала важность доступа к базам данных для языка программирования, поэтому был создан БД-модуль, распространяемый в пакете "crystal-db", который обеспечивает общий унифицированный API для баз данных. Знайте: "crystal-db" без проблем работает вместе с драйверами SQLite, MySQL и "Postgres". Вам даже не нужно запрашивать эти БД-драйверы. Для других баз данных необходимо добавить специальные драйверы.
Рассмотрим основные операции с базами данных, используя пробную базу данных SQLite, chinook.db. Эта
база данных представляет музыкальный магазин и содержит таблицы "artists" и "albums" среди прочих. Поскольку мы используем "crystal-db", то стиль записи команд и операций, по существу, стандартен и подходит для других баз данных.
Сначала создайте проект Crystal (назовите его "crchinook") с помощью команды
"$ crystal init app crchinook".
Затем надо добавить шард "crystal-sqlite3", отредактировав файл shard.yml следующим образом:
dependencies:
sqlite3:
github: crystal-lang/crystal-sqlite3
Затем выполните команду "$ shards", чтобы установить эту зависимость. Вы следует переписать весь код
в файл "src/crchinook.cr". Не забудьте добавить строку require "sqlite3" вверху. В следующем фрагменте кода
вы можете увидеть, как осуществляются заурядные действия с базой данных:
__advanced_features/crchinook/src/crchinook.cr__
require "./crchinook/*"
require "sqlite3" ##
DB.open "sqlite3://../chinook.db" do |db|
sql = "SELECT artistid, name FROM artists ORDER BY name ASC;" #(1)
db.query sql do |rs|
p "#{rs.column_name(1)} (#{rs.column_name(0)})"
rs.each do # perform for each row in the ResultSet
artistid = rs.read(Int32)
name = rs.read(String)
p "#{name} (#{artistid})"
# => Name (ArtistId)
# => A Cor Do Som (43)
# => AC/DC (1)
# => Aaron Copland & London Symphony Orchestra (230)
# => ...
end
end
sql = "SELECT name FROM artists WHERE artistid = 231;" #(2)
p db.query_one sql, as: String
# => "Ton Koopman"
sql = "SELECT MIN(birthdate) FROM employees;" #(3)
oldest = db.scalar sql # => "1947-09-19 00:00:00"
sql = "SELECT firstname, lastname FROM employees WHERE birthdate = ?;" #(4)
firstname, lastname = db.query_one sql, oldest, as: {String, String}
p "#{firstname} #{lastname}" # => "Margaret Park"
db.exec "insert into artists values (?, ?)", 276, "Scott Ross" #(5)
args = [] of DB::Any
args << 277
args << "Bernard Foccroules"
db.exec "insert into artists values (?, ?)", args
sql = "SELECT name FROM sqlite_master WHERE type='table';" #(6)
db.query_all(sql, as: String)
# =>
# [ "albums",
# "sqlite_sequence",
# "artists",
# "customers",
# "employees",
# ...,
# "sqlite_stat1"
# ]
end
1) Прочитайте всех артистов из БД.
2) Прочитайте одного артиста посредством "artistid (идентификатора, ID).
3) Прочитайте одно значение, заданное скалярной величиной.
4) Читать старейшего сотрудника путем замены переменной.
5) Используйте инструкции "exec" для DDL (язык "Data Definition", для БД).
6) Прочтите все имена в таблице.
Постройте приложение с помощью "$ crystal build src/crchinook.cr" и запустите его с помощью "$ ./crchinook".
Установите соединение с базой данных, передав "DB.open" с подключением "URI" в качестве аргумента. Вот разные формулировки для подходящих баз данных:
sqlite3:///path/to/data.db
mysql://user:password@server:port/database
postgres://server:port/database
Если применить "DB.open" в блоке "do end", соединение с базой данных автоматически закроется после строки "end". Если это не срабатывает, необходимо использовать такое вот решение:
db = DB.open "sqlite3://../chinook.db"
begin
# ... use db to perform queries
ensure
db.close
end
Как видим, "db" (или "DB.open") создает объект "DB::Database", который автоматически предоставляет вам связной узел то есть для обработки запросов доступно несколько подключений к базе данных. Этот своеобразный коммутатор можно конфигурировать, устанавливая максимальное количество подключений, время проверки, число повторов, длительность паузы перед повторной попыткой.
____________________________________________________
Выяснение типа базы данных.
Тип базы данных можно извлечь из URI, принадлежащих этой базе данных, как показано ниже в "db.uri.scheme. Это удобно, когда вам нужно настроить SQL в соответствии с базой данных. Просто используйте "case...when", как показано здесь:
sql = case db.uri.scheme
when "postgres"
# SQL for postgres
when "mysql"
# SQL for mysql
when "sqlite3"
# SQL for sqlite3
else
raise "SQL not implemented for #{db.uri.scheme}"
end
__________________________________________________
Выборочные запросы выполняются с помощью метода "query", который возвращает объект "ResultSet". Можно выполнить итерацию над этим объектом, используя метод "each" для получения и обработки каждой строки последовательно. Вы должны указать тип каждого поля, как в "rs.read(Int32)", поскольку во время компиляции здесь отсутствуют
сведения о доступных полях базы данных. Минимальный набор поддерживаемых типов
задается в "DB::Types" и включает в себя: Nil, String, Bool, Int32, Int64, Float32, Float64, Time, Bytes.
Тип "DB::Any" является псевдонимом для любого из них, потому что представляет собой объединение всех этих типов.
К именам столбцов можно обращаться по индексу, как в "rs.column_name(1)", или возвращать в виде массива
с переменными "column_names". Много столбцов можно прочитать за раз: "artistid, name = rs.read(Int32,
String)". Используйте "query_one" для чтения одной строки, "scalar" для чтения первого значения первой строки, а для возврата массива "query_all" уточнённый инструкцией "as: Type".
Запросы (4) и (5) в листинге "crchinook" показывают, как можно интерполировать значения в SQL-строку, используя один или много символов "?", для БД SQLite. Для Postgres вам следует использовать "$1" и "$2". У каждой базы данных свой собственный подход к этому вопросу. Использование параметров в инструкциях SQL позволяет избежать вредоносных SQL-инъекций.
Для запросов, создающих объекты БД или меняющих значения (вставка, обновление, удаление), используйте метод "exec", как в строке (5). Вы видите, что для вставки инструкций можно просто заполнить массив и заменить его символами подстановки "?", играющими роль знаков-заполнителей.
Затем в строке (6) мы используем "query_all" для чтения всех табличных имен из базы данных, поэтому рекомендуем применять его, только если вы не против получить все записи скопом;
query_all выполняет запрос и возвращает массив, в котором значение первого столбца каждой строки
считывается как заданный тип. Здесь [http://crystal-lang.github.io/crystal-db/api/latest/DB/QueryMethods.html] содержится документация обо всех доступных методах для обращений к БД.
Инструкции SQL, которые должны выполняться вместе, могут также быть заключены в транзакцию.
_____________________________________________________________
Обработка исключений в работе с базами данных.
Взаимодействие с базой данных может сопровождаться исключительными ситуациями, возникающими по разным причинам. Поэтому в рабочих исходниках все строки с инструкциями для БД следует охватывать блоком кода
begin
rescue
ensure
end
____________________________________________________________
Если вы предпочитаете сделать шаг вверх по Лестнице Абстракции (т.е. на следующий уровень абстракции), то фреймворк ORM (объектно-реляционный мэппинг) в вашем распоряжении. Crystal предлагает много альтернатив, которые вы можете исследовать на примере Восхитительного Кристалла [https://github.com/veelenga/awesome-crystal#ormodm-extensions] или кристальных осколков [https://crystalshards.org/?filter=orm].
Начальные точки для ваших изысканий:
Топаз, [https://github.com/topaz-crystal/topaz] который является простой обёрткой БД.
Crecto, [https://github.com/Crecto/crecto] вдохновленный структурой Ecto в ЯП Elixir, создан для использования в более крупных проектах. В его наборе есть даже панель администрирования.
Granite-orm, [https://github.com/amberframework/granite-orm] который используется с веб-фреймворком Amber, ориентирован на БД Postgres, MySQL и SQLite.
Вы можете разузнать ещё больше о множестве применимых к БД методов и объектов, просмотрев документацию [http://crystal-lang.github.io/crystal-db/api/latest/].
Завершая главу...
В этой главе вы познали могущество макросов, прибегая к которому следует проявлять дальновидность и осмотрительность. Вы видели, как легко прирастить Си-программу к исходнику ЯП Crystal. Мы ещё ближе подошли к разгадке идиомы: почему же Кристалл работает подобно нитеволоконному комбинату. Адекватно используя распараллеливание там, где это уместно, вы можете повысить производительность работающего кода в самых разных проектах. Ещё вы узнали, как легко можно связывать свои программы с подходящей базой данных.
В следующей (и последней) главе вы увидите, чем Crystal примечателен в королевстве веб-фреймворков.
Мы закончим главу обсуждением важнейших шард-пакетов, специфичных для неких разновидностей ПО.
Глава 9.
Использование веб-фреймворков & Shards.
До сих пор вы изучали ЯП Crystal в деталях. Здесь нечто большее о Crystal, чем языковые спецификации, с обширным арсеналом библиотек и фреймворков, могущих выдернуть вас из конкретных затруднений.
Эта глава даст вам базовые знания для построения собственных веб-приложений в Crystal с помощью Kemal. Мы также совершим краткую экскурсию по растущей и развивающейся экосистеме шардов, указывая на особо важные шард-пакеты, которые сделают ваши кристальные проекты более успешными. Совсем не обязательно делать всю работу самому!
_Создание веб-приложений с помощью фреймворков Kemal и Amber_
Всплеск популярности Ruby во многом заслуга Rails, выдающегося веб-фреймворка. И раз уж Crystal прямой потомок Руби, неудивительно, что разработчики Кристалла немало сил вложили в веб-фреймворки. Если просмотреть отчеты компании, знакомых с ЯП Crystal, можно убедиться, что многие из них используют Kemal.
ВСТРОЕННЫЙ ВЕБ-СЕРВЕР.
Стандартная библиотека Crystal содержит HTTP-модуль для базовых веб-операций, таких как обработка статичных файлов, ведение журнала, обработка ошибок, данных формы, веб-сокетов и т.д. Он также содержит встроенный веб-сервер, класс "HTTP::Server". Вы можете "завести" его буквально в несколько строк кода и увидеть базовую сводку о работе запущенного веб-сервера:
__web_frameworks_and_shards/web_server.cr__
require "http/server"
server = HTTP::Server.new do |ctx| (1)
ctx.response.content_type = "text/plain" (2)
ctx.response.print "Crystal web server: got #{ctx.request.path}" (3)
end
server.bind_tcp 8080
server.listen (4)
puts "Crystal web server listening on http://localhost:8080"
# => in browser with the URL http://localhost:8080/
# "Crystal web server: got /"
Чтобы начать им пользоваться, нам нужно обратиться с КС "require" к исходному коду HTTP-модуля. Затем создайте экземпляр класса "Server" в строке (1). Волокна размножаются для обработки каждого входящего запроса, так что веб-сервер осуществляет полностью параллельное исполнение. Для исполнения волокну будет предоставлен код в блоке "do ... end". В конце строки (1) имеется параметр "ctx" (объект класса "Context"), который содержит информацию из запроса; например, "path" укажет адрес, запрошенный клиентом. Вместе с блоком может быть обработан список HTTP-манипуляторов (см. документацию [https://crystal-lang.org/api/latest/HTTP.html]).
В ответ на запрос активное волокно сформирует ответ сервера, уточняя "content-type" (тип запрашиваемого содержимого) в строке (2).
Поскольку это чрезвычайно краткий ответ, здесь только "text/plain". На этом этапе вы также можете задать свойства "status" и "header". После этого волокно напишет (используя "print") в ответ, в строке (3). Сервер отправит это веб-клиенту (возможно, браузеру), который обработает полученное содержимое и покажет пользователю.
Сам сервер привязывается к IP-адресу и порту с помощью методов bind_tcp, а затем запускается с методом "listen" в строке (4).
Если добавить "true" в конец списка аргументов для "bind_tcp", веб-сервер разрешит повторное использование портов. Это означает, что нескольким соединениям разрешено связываться с одним и тем же портом на одном из хостов, имитируя таким образом многопоточное поведение.
Чтоб описать параметрами адрес сервера так же, как и порт, вам следует выполнить привязку сервера следующим образом:
ip = "0.0.0.0"
port = 8080
server.bind_tcp ip, port # ...
Скомпилируйте этот сценарий командой "$ crystal web_server.cr". И появится такое вот:
Crystal web server listening on http://localhost:8080
сообщение в серверной консоли.
Если открыть браузер с этим адресом, вам покажут ответ сервера:
" Crystal web server: got / "
Остановите сервер с помощью комбинации "CTRL+C" или примените "close" в исходном коде.
Этот штатный веб-сервер имеет отличную производительность, но поскольку программист должен сам добавить кое-какие функциональные базовые блоки, вам придется написать гораздо больше кода. Например, назначение подходящих ответов для конкретных запросов. Вам могут потребоваться инструменты, чтобы справиться с этим.
И "router_cr" [https://github.com/tbrand/router.cr] является изящным и простым связующим звеном для веб-сервера Crystal.
В следующих разделах рассматриваются другие фреймворки, сконструированные поверх базового http-сервера. Сравнительные тесты показывают, что веб-фреймворки Crystal являются одними из самых быстрых, по меньшей мере (см. раздел "Speeding Up the Web" Ускорение работы в Web-пространстве).
ВАШ ЧЕРЕД 1.
1) XML-сервер Времени: напишите веб-сервер, настроенный на порт 5000, который показывает время и приветствие сообразно времени суток. Для вывода используйте формат XML. (Подсказка: Тип содержимого задайте text/xml.)
2) Обслуживание индексной веб-страницы: создайте веб-сервер, который занимается обработкой ошибок, ведением журнала и обслуживает страницу "index.html", когда браузер запрашивает URL-адрес "http://127.0.0.1:3000/public".
(Подсказка: Используйте документацию для обработчиков (Handlers), и потом скопируйте страницу для вывода с помощью "IO.copy").
ЛЁГКОВЕСНЫЙ ВЕБ-ФРЕЙМВОРК "Kemal".
Kemal [http://kemalcr.com/] это стремительный веб-фреймворк, разработанный Сердаром Догруйол, и де-факто является стандартным в мире Кристалла. Это преемник экспериментального фреймворка по имени Frank, написанного командой Crystal.
Kemal имеет модульную архитектуру. Многие его идиомы происходят от Sinatra (легкого веб-фреймворка из Ruby), вот почему простота является одной из его отличительных черт. Его требования к ЦП и оперативной памяти (+- 1MB) очень скромны: один сервер может обслуживать десятки тысяч клиентов.
______________________________
Базовое Kemal-приложение.
Для начала давайте построим с нуля пример из главы 1.
Вы уже знаете последовательность действий:
Создайте новое приложение командой "crystal init" (назовите его "simple_kemal_app").
Добавьте следующий сегмент в файл "shard.yml":
dependencies:
kemal:
github: kemalcr/kemal
Выполните команду "shards".
Вывод продемонстрирует, что, помимо "kemal", ещё два зависимых шард-пакета ("radix", используемый для
маршрутизации, и "kilt", используемый для шаблонного представления) также установлены:
Installing kemal
Installing radix (0.3.8)
Installing kilt (0.4.0)
Помните: этот шаг устанавливает исходный код Кемаля в папку "/lib", и это на 100% Crystal-код, так что вы можете самолично разведать, как всё это работает!
Теперь добавьте в "src/simple_kemal_app.cr" следующий код:
__web_frameworks_and_shards/simple_kemal_app/src/simple_kemal_app.cr__
require "./simple_kemal_app/*"
require "kemal" (1)
get "/" do (2)
"My first Kemal app is alive!"
end
Kemal.run (3)
Проведите пробный пуск приложения с помощью "crystal src/simple_kemal_app.cr" (или используйте "crystal build" и запустите исполняемый файл). Появится следующее сообщение:
" [development] Kemal is ready to lead at http://0.0.0.0:3000 "
Откройте браузер с адресом "http://localhost:3000/" (порт по умолчанию - 3000) и вы увидите сообщение: Мое первое Kemal-приложение живет!
Между тем, на консоли Кемаля появится что-то подобное:
2017-10-04 10:10:17 +0200 200 GET / 165.0s
2017-10-04 10:10:17 +0200 404 GET /favicon.ico 204.0s
...
^CKemal is going to take a rest!
Это журналирование выводимых данных (работающее по умолчанию) можно отключить, установив "logging false". Кроме того, вы можете задать свой собственный режим ведения журнала, если захотите. Чтобы сменить номер порта, установленный по умолчанию, (на 8080, к примеру), используйте код "Kemal.config.port = 8080".
Программный код для ответа на запросы элегантен и прост:
(1)
Затребовать (require) шард Кемаля.
(2)
Подобрать для запроса (GET http ://localhost: 3000/) подходящую строку на вывод, которую вернём клиенту.
(3)
Запустите веб-сервер, путем вызова метода "run".
Если вам хочется, чтобы в ответ на запрос браузер посетил индекс-страницу, вы можете сделать это так:
get "/" do |env|
env.redirect "index.html"
end
Здесь "env" это переменная среды и должна быть передана блоку обработчика в качестве параметра,
поэтому (get "/" do) должно в итоге превратиться в (get "/" do |env|). Переменная "env" полезна тем, что может получить доступ ко всей информации о запросе и параметрах (полученных через URL, обращение, или как отсылаемые параметры формы), также она может сформировать ответ, заполнив его данными. Ещё "env" работает с методами "set" и "get", которые помогут сохранить значения во время чередующихся запросов и ответов.
___________
Примечание:
* Параметр формы Элемент <form> HTML-разметки, устанавливает на веб-странице поле для текстового ввода.
Кемаль умеет обращаться со всеми методами HTTP, такими как GET, POST, PUT, PATCH и DELETE:
обработчик маршрутизации это всегда блок кода "do end", который может иметь контекстную переменную. Это позволяет применить в ваших программах архитектуру RESTful (ReST Образ Формулировки Передаётся) для веб-сервисов.
Маршруты обрабатываются поочередно (по принципу "кто поспел тот и съел"): первый обнаруженный маршрут, который соответствует запрашиваемому URL, будет активирован.
________________________________________
Визуализация страниц с помощью ECR.
Давайте начнем строить простой веб-сайт, попутно исследуя характерные особенности Кемаля. Для отображения динамических страниц, частично сформированных посредством программного кода, "Kemal" может использовать модуль ECR (Embedded Crystal) [https://crystal-lang.org/api/latest/ECR.html].
Это шаблонный язык из стандартной библиотеки, используемой для встраивания кода Crystal в текст вроде HTML, как это делает ERB в языке Ruby. Однако, в отличие от ERB, все шаблоны компилируются последовательно один за другим, что по-своему эффективно: ваш ПК не столкнется с дефицитом производительности во время слишком интенсивной обработки шаблонов. Шаблоны ECR также являются типо-безопасными.
Но довольно теории приступим же. Сначала создайте проект по имени "appviews". Чтобы облегчить разработку вы можете в командной строке запустить "sentry" [https://github.com/samueleaton/sentry],
инструмент для ИКС, написанный Сэмюэлем Итоном (Samuel Eaton).
Он просматривает файлы-исходники и автоматически собирает и запускает приложение, в котором было обнаружено изменение. Таким образом, вам не нужно останавливать и перезапускать приложение при каждом изменении! Установите его в домашнюю папку вашего приложения, вот так:
$ curl -fsSLo- https://raw.githubusercontent.com/samueleaton/sentry/master/install.cr | crystal eval
(Обратите внимание, как здесь используется "crystal eval", оценивая сценарий "install.cr".)
Чтобы применить этот инструмент, введите команду "$ ./sentry". Он начинает следить:
\РИС.\
Чтобы вывести "sentry" и некоторые исполняемые файлы из-под наблюдения за исходниками, добавьте в ".gitignore" следующее:
/doc/
/lib/
/bin/
/.shards/
/dev/
sentry
appviews
Теперь повторите последовательность шагов для установки Кемаля из предыдущего параграфа. Также напишите его исходный код внутри "src/appviews.cr": поскольку "sentry" продолжает дежурство, он увидит это, скомпилирует ваше приложение и запустит Kemal в работу!
Теперь нам, вероятно, захочется добавить несколько статичных файлов, вроде скриптов JavaScript и таблиц стилей. Мы будем использовать "Bootstrap" здесь (используйте мини-версию для сборки рабочего экземпляра программы), и ещё таблицы стилей для нашего приложения в файле "appviews.css". Все эти ресурсы размещаются в папке "public" следующим образом:
appviews/
src/
appviews.cr
public/
images/
fonts/
js/
bootstrap.js
appviews.js
css/
bootstrap.css
appviews.css
index.html
Кемаль автоматически обнаружит всё это хозяйство в общей папке "public", где он окажется всякий раз,
мимоходом направляясь в каталоги вроде "/js/appviews.js". Если у вас нет статических файлов, можно отключить их обслуживание, установив "serve_static false" в вашем коде.
Либо вы можете изменить расположение, примерно так: "public_folder src/public". И в этом случае мы не будем использовать файл "index.html".
Вместо простого приветствия пришло время показать домашнюю страницу, используя ECR. Мы можем сделать это, изменив обработчик "get" в "src/appviews.cr" таким вот образом:
get "/" do
title = "Home"
page_message = "Your app's home page"
render "src/views/home.ecr"
end
Здесь представлены две переменные: "title" и "page_message". Впервые мы видим, как используется ECR:
вы проводите рендеринг ("render") шаблона ".ecr", который находится в каталоге "src/views", служащем пристанищем для таких файлов-шаблонов (иначе говоря образов). Прежде чем проделать это, вам необходимо подготовить файл ECR. В папке отредактируйте новый файл "home.ecr", добавив такие строки:
<div class="jumbotron">
<h2><%= page_message %></h2>
<p>Your home page text starts here ...</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Learn
more »</a></p>
</div>
Теперь "sentry" компилирует, и ваш браузер (по адресу "http ://localhost: 3000") демонстрирует это :
\РИС.\
Что произошло здесь? В "<%= page_message %>" значение "page_message" было вставлено в кадр "home", что и привело к появлению "<% =% >". Ещё вы можете поместить код Crystal в "<% %>", чтобы контролировать исполнение кода. Для общей навигации, верхнего колонтитула, нижнего колонтитула и прочего можно использовать схематичный макет (один или несколько).
Эти страницы тоже имеют расширение ".ecr" и размещаются в подпапке "layouts" (внутри src/views/). Создайте "main_layout.ecr" в этой папке. Главным образом, это HTML-код, который мы не станем здесь демонстрировать (возможно, он найдется в файлах с примерами кода, прилагаемых к оригинальной книге eBook).
Этот макет будет использоваться, если предыдущая линия рендеринга будет заменена следующей:
render "src/views/home.ecr", "src/views/layouts/main_layout.ecr"
Как это работает? Страница макета задает заголовок веб-страницы посредством "<%= title %>"; также она содержит такую секцию:
<div class="container">
<%= content %>
</div>
Файл "home.ecr" отображается на странице макета, потому что в Кемале переменная "content" захватывает
содержимое первого аргумента для рендеринга.
Вариативность наполнения и оформления зачастую требует различных сценариев JavaScript, или же отрывков HTML-кода и фрагментов таблиц стилей (CSS). В этом случае "content_for" и "yield_content" будут очень кстати; больше подробностей вы отыщете в руководстве для Kemal [http://kemalcr.com/guide/].
Также мы видим сегмент кода, который находится в "<div> navbar":
<li <% if env.request.path == "/" %>class="active"<% end %>>
<a href="/">Home</a></li>
<li <% if env.request.path == "/about" %>class="active"<% end %>>
<a href="/about">About</a></li>
<li <% if env.request.path == "/contact" %>class="active"<% end %>>
<a href="/about">Contact</a></li>
Этот код, <% if [condition] %>[effect]<% end %>, подбирает соответствующий запросу путь доступа с помощью переменной "env", назначая подходящий класс CSS, если замечено совпадение. На этой стадии компиляция снова будет успешной, и наш сайт начинает приобретать сходство с дизайнерским эскизом:
(\РИС.\)
Добавьте страницы "about.ecr" и "contact.ecr" в папку "views и добавьте два маршрутных обработчика "get"
в "appviews.cr". Наши две новые страницы унаследуют один и тот же облик:
get "/about" do |env|
title = "About"
page_message = "Your app's description page"
render "src/views/about.ecr", "src/views/layouts/main_layout.ecr"
end
get "/contact" do |env|
title = "Contact"
page_message = "Your app's contact page"
render "src/views/contact.ecr", "src/views/layouts/main_layout.ecr"
end
Кликните (Register Регистрировать) или (Login Вход в аккаунт), и Kemal подскажет вам, что таких эскизов пока еще нет. Отсюда можно начать обустраивать свой собственный, идиосинкретический веб-сайт!
_________________________________________
Стриминг базы данных с помощью JSON.
Иногда вы просто хотите отправить данные, без всяких HTML-страниц. Начните потоковую передачу информации БД в формате JSON от сервера до клиента; нижеследующий пример вдохновлен статьей
[https://manas.tech/blog/2017/01/16/from-a-db-to-json-with-crystal.html] Брайана Кардиффа.
Мы объединим то, что узнали из раздела "Доступ к базам данных", с проделанной работой над той самой sqlite3-базой, "chinook.db". Подготовительную часть рекомендуется сделать самостоятельно:
создайте приложение, назовите "db_json", затем добавьте шард-пакеты "kemal" и "sqlite3".
Логическая структура приложения выглядит следующим образом:
Откройте подключение к базе данных.
Сформулируйте обработчики маршрутов GET:
1) Запрос "/"(т.е. "root" корень иерархии каталогов) возвращает список имен всех таблиц.
2) Запрос типа "/:table_name", например "/artists", возвращает все записи в формате JSON.
Запустите веб-сервер Kemal.
Закройте подключение к БД.
Первый обработчик незамысловат и конкретен. Сначала задайте тип серверных ответов с помощью "env.response.content_type". Потом нужно получить данные. Выполните SQL-запрос с помощью "query_all". Его результат окажется массивом, поэтому для него можно использовать метод "to_json". Так будет создана строка JSON-представления и отослана клиенту. Вот и всё!
get "/" do |env|
env.response.content_type = "application/json"
tables = table_names(db)
tables.to_json
end
def table_names(db)
sql = "SELECT name FROM sqlite_master WHERE type='table';"
db.query_all(sql, as: String)
end
А вот и браузерный вывод:
\РИС.\
Если вы предпочитаете видеть эти табличные имена в HTML-представлении, измените обработчик на:
get "/" do |env|
tables = table_names(db)
render "src/views/tables.ecr"
end
и добавьте образ "tables.ecr", который зацикливает массив "tables":
<header>
<h1>All tables</h1>
</header>
<body bgcolor="#DDE1C2">
<!--#include virtual="/header.raw"-->
<table BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%" style="margin-bottom: 5px;margin-top: 5px;">
<tr><td>
<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=4 BGCOLOR="#E9EAD6" WIDTH="100%">
<TR BGCOLOR="#C7CBB1"><TD><FONT COLOR="#000090">
<b><a href="http://www.opennet.me/docs/">Каталог документации</a> /
Раздел "<a href="http://www.opennet.me/docs/124.shtml">Программирование, языки</a>"
</b></FONT>
</td><td align="right">
<small>(<a href="crystal-prog.tar.gz">Архив</a> |
<a href="crystal-prog.html.gz">Для печати</a>)</small>
</TD></TR>
</TABLE>
</TD></TR>
<TR BGCOLOR="#B0B190"><TD><IMG SRC="/p.gif" HEIGHT=3 WIDTH=1 ALT=""></TD></TR>
</TABLE>
<% if tables %>
<% tables.each do |table| %>
<p><%= table %></p>
<% end %>
<% end %>
<!--#include virtual="/footer.raw"-->
</body>
Как показано на рисунке, переменные из сценария (вроде "tables") были автоматически опознаны в ходе отображения, а затем выведены на дисплей браузером.
\РИС.\
Второй обработчик немного сложнее.
Имя таблицы передается как параметр в URL. Его можно прочитать посредством "env.params.url", как сделано в строке (1) нижеследующего примера. Чтобы избежать SQL-инъекций, мы в строке (2) проверяем, что параметр соответствует реально существующей таблице. Мы выполняем запрос в строке (3) и организуем цикл для каждой записи в строке (4). И отправляем JSON-строку клиенту методом "write_json" (5).
__web_frameworks_and_shards/db_json/src/db_json.cr__
get "/:table_name" do |env|
env.response.content_type = "application/json"
table_name = env.params.url["table_name"] (1)
# avoid SQL injection by checking table name
unless table_names(db).includes?(table_name) (2)
# ignore if the requested table does not exist.
env.response.status_code = 404
else
db.query "select * from #{table_name}" do |rs| (3)
col_names = rs.column_names
rs.each do (4)
write_json(env.response.output, col_names, rs) (5)
# force chunked response even on small tables
env.response.output.flush (6)
end
end
end
end
Алгоритм второго обработчика производит потоковую передачу по одной строке за раз, минимизируя потребление памяти сервером. И клиенту приходится обрабатывать данные по мере их поступления.
Это делается путем применения метода "flush" к буферу вывода в строке (6), и тогда отправка строки JSON клиенту происходит незамедлительно; иногда это называют отрывистым или фрагментарным ответом.
Строка JSON конструируется в методе "write_json", который вызывается для каждой записи:
__web_frameworks_and_shards/db_json/src/db_json.cr__
def write_json(io, col_names, rs)
JSON.build(io) do |json| (1)
json.object do
col_names.each do |col|
json_encode_field json, col, rs.read (2)
end
end
end
io << "\n"
end
Здесь преобразование в JSON результирующего набора с именами столбцов и значениями полей несколько усложнено, так что нам нужно будет использовать модуль JSON [https://crystal-lang.org/api/latest/JSON.html].
"Crystal-db" изначально ориентирован на скорость и обеспечивает прямой доступ к значениям БД в памяти,
избавляя программиста от возни с временными массивами или хешами. (Правда, если вы создаёте немало своих собственных временных массивов и хешей, это не особо поможет.) Мы создаём объект "JSON:: Builder" в строке (1) в предшествующем коде, который записывается непосредственно в "io" и по сути является выводом веб-сервера, прокомментированным в строке (5) предыдущего примера.
Чтобы сделать это более конкретным, вот JSON-вывод для первой записи из таблицы "Артисты": {"ArtistId":1,"Name":"AC/DC"}.
Итак, "json.object" начинает и заканчивает строку JSON для одной записи и вызывает блок кода "do". Для каждого столбца записывается имя "col" вместе со значением "rs.read". Смотрите же, как это делается в "json_encode_field", в строке (2):
__web_frameworks_and_shards/db_json/src/db_json.cr__
def json_encode_field(json, col, value) #
case value
when Bytes (1)
json.field col do
json.array do # writes begin and end of an array
value.each do |e|
json.scalar e # writes the value
end
end
end
else
json.field col do # write an object's field and value
value.to_json(json) (2)
end
В данном примере вызывается метод "to_json" для всех простых значениий БД в строке (2), но с типом "Bytes" в строке (1) мы должны обходиться осторожнее, рассматривая его как массив и прокручивая в цикле каждое значение. Такой подход не создает временный массив, и это наиболее эффективный способ обработки объектов, которые могут оказаться большими блобами (англ. - Binary Linked Object).
Исходный код целиком вы сможете найти в "db_json/src/db_json.cr" (т.е. в каталоге с загруженным кодом).
Здесь вы видите вывод первых десяти строчек из таблицы "artists":
\РИС.\
_______________________________________________
Иные аспекты разнообразных веб-приложений.
Опираясь на пример кода, который мы обсуждали в последнее время, вам должно быть по силам объединить эскизы ECR с данными БД, чтобы построить больше веб-приложений, ориентированных на перспективные базы данных. Но вы сможете сделать с Кемалем и Crystal гораздо больше. Вот ещё несколько примеров Kemal-проектов, которые могут вдохновить вас:
Демонстрация приложения ToDo (из обл. Тайм-менеджмента) [https://github.com/Angarsk8/realtime-todo-app], разработана с помощью Kemal, React и PostgreSQL, с использованием веб-сокетов.
Демонстрация приложения для чата [https://github.com/Angarsk8/chat-app-demo] здесь. [http://kemal-react-chat.herokuapp.com/ ]
Приложение [76], использующее аутентификацию JWT с "auth0", детально описано здесь.[77]
Приложение для блога. [78]
Блоговый сервер по имени Kamber. [https://github.com/f/kamber]
Kemal-watcher [80] плагин для надзора за клиентскими файлами, схожий с рассмотренным плагином "sentry".
Зацените, как легко вы можете отправлять SMS-сообщения [81] или совершать телефонные звонки [82] с помощью Twilio API.
Эти примеры позволяют лишь оценить преимущества Кемаля. Для подлинного вдохновения Вам ещё больше материала изучить придётся:
С параметрами запроса легко работать; если запрос содержит "/users/108", то ему соответствует обработчик get "/users/:id", и значение 108 извлекается посредством "env.params.url["id"]".
В цикл ответа можно вставить фильтры, позволяющие сотворить и инкапсулировать собственную логику, манипулируя запросом или ответом. Они объявляются как "before_x" или "after_x", где "x" может быть: "get", "post", "put", "patch", "delete" или "all".
Модульность Kemal достигается с помощью обработчиков (или промежуточных программ), подобных Rack: это классы, которые наследуют от "Kemal::Handler" и могут вызываться выборочно в зависимости от метода запроса. Встроенными программами обрабатываются исключительные ситуации, статичные файлы, также операции роутинга и ведение журнала. Есть и другие промежуточные программы для базовой аутентификации и защиты "csrf".
Кемаль может хранить информацию в сессиях, подключив шард "kemal-session"[83]. Данные могут сохраняться в памяти, файле, в СУБД Redis и даже в MySQL.
Kemal предлагает простую поддержку веб-сокетов (ознакомьтесь с приложением чата, например), которая работает гораздо быстрее, чем Node.js. (Впрочем, эта возможность обеспечена встроенным веб-сервером.)
Вы можете протестировать свое веб-приложение с помощью шарда "spec-kemal".
Kemal предоставляет доступ к меткам cookie, загрузкам файлов, встроенной поддержке SSL, а также разделение тестовой среды и производственной, пользовательскую обработку ошибок, отправку электронной почты через "smtp.cr"[84], и поддержку различных БД.
Вы можете развернуть своё приложение на облачной платформе Heroku или с помощью скрипта Capistrano.
Вот веб-сайт [85], который своей работой блестяще демонстрирует, как использовать разнообразные функции Кемаля. Raze [86] это фреймворк следующего поколения, который стремится конкурировать с Kemal.
AMBER ВЕБ-ФРЕЙМВОРК, похожий на RAILS.
Кемаль быстрый и полнофункциональный веб-сервер, но он изначально нацелен на минимализм и простоту. Если вам больше подходит фреймворк, способный удовлетворить запросы клиента наравне с Ruby on Rails, Crystal предлагает несколько Rails-подобных фреймворков. И неудивительно, что они в значительной степени полагаются на макросы.
Amber [https://amberframework.org/]
многое перенял у предшественников:
у Kemal, Ruby-on-Rails, Phoenix и других популярных программных каркасов; и он следует стилю MVC (Model-View-Controller). Вы можете найти его исходный код здесь [https://github.com/amberframework/amber], и нетрудно заметить, что развитие Amber проходит в бодром темпе. Проект объединил усилия с командой разработки Kemalyst [89], который тоже основан на Кемале.
Разработчики Amber стремятся построить аналог Rails для Crystal. Amber использует условные обозначения стандартного MVC-фреймворка, и такая практика обеспечивает генерирование кода и монтажную фрейм-структуру для быстрой разработки прототипов.
По умолчанию он использует легковесный ORM (Объектно-Реляционный Mapper), заимствованный у Гранита [https://github.com/amberframework/granite-orm] для работы с базами данных, используя минимальную конфигурацию. Amber предусматривает обработку простых запросов, таких как "find_by" и "all", и связывание один ко многим "belong_to" и "has_many". Но вы также можете выбрать ORM Crecto (похожий на "ecto" из Phoenix) или более функциональную ORM Jennifer.
Amber вооружен встроенным инструментом ИКС, названным (почему-то) "amber". Для вас он может сотворить каркас проекта, вместе с моделями, контроллерами и эскизами. Разводка маршрутов делается с помощью каналов, как в Phoenix.
По умолчанию приложения настроены на использование адаптера БД PostgreSQL, но при необходимости могут переключиться на MySQL, SQLite или любое другое хранилище данных или микросервис. По умолчанию применяется движок рендеринга Slang, но можно использовать и ECR. Amber полагается на Sentry для комфортной постепенной разработки. Помимо обработки HTTP-запросов, приложения Amber могут работать с веб-сокетами и могут выполнять рассылку по почте.
Если прочитанные сведения побуждают вас тут же начать работу с Amber, начните отсюда. [91]
В этой области ещё несколько фреймворков заслуживают упоминания: Аметист [92], carbon-crystal[93], и Lucky[94] от фирмы Thoughtbot. Стоит обратить внимание и на lattice-core (нетипичный фреймворк "websocket-first") [95], который базируется на Kemal и ориентирован на оптимизацию сеанса "клиент-сервер" (поддержание режима реал-тайм).
_Краткий экскурс по экосистеме шардов_
Экосистема Кристалла неуклонно растет: на момент написания оригинала eBook в свободном доступе насчитывалось более 3700 разномастных шард-пакетов различной специфики. Давайте сначала перечислим места, где вам следует поохотиться на шарды:
Ваша первая остановка Libhunt [96], его кураторский список включает более 50 отдельных категорий.
Еще один кураторский список Awesome Crystal.[97]
Crystal Shards [98] простой поисковый алгоритм, который позволяет отследить трендовые, наиболее популярные и недавно обновленные проекты.
Crystal ANN [99] это место, где публикуются новые проекты, версии, блоги или другие новшества для Crystal.
Почти невозможно не то что обсудить даже перечислить самые полезные шард-пакеты. Что касается баз данных и WEB, мы уже затронули немало актуальных шардов в этой и предыдущей главах.
Вот неcколько отборных шардов, которые стоит изучить, некоторые из них демонстрируют DSL-подобные
возможности Crystal.
ОБРАБОТКА ЗАДАНИЙ.
Sidekiq [100] это простой и эффективный фреймворк обработки фоновых задач, который в четыре раза быстрее, чем его API-совместимый предшественник из мира Ruby.[101]
Sidekiq использует Kemal для визуализации всех панелей мониторинга и пользовательского веб-интерфейса в приложении.
Schedule [102] и Cron_scheduler [103] помогут изготовить планировщик заданий.
РАЗВЕРТЫВАНИЕ.
Baked File System [104] позволяет хранить все ваши статичные файлы в одном двоичном файле, чтобы облегчить развертывание.
ГРАФИКА И ГРАФИЧЕСКИЙ ИНТЕРФЕЙС.
Наиболее перспективными являются "Qt5.cr" [105], который обеспечивает привязки к фреймворку "Qt5", и "libui" [106], который предоставляет привязки к нативной С-библиотеке GUI для Linux, OS X и Windows.
Пакет crystal-gl для Mac OS X и Linux [https://github.com/ggiraldez/crystal-gl] предлагает биндинг к OpenGL и библиотекам GLFW, SOIL, GLM и др.
ИГРЫ.
Для разработки видеоигр и мультимедийных приложений используйте CrSFML [108].
Cray [109] обеспечивает тесную интеграцию с чрезвычайно простой С-библиотекой RayLib с аудио, 2D и 3D-графикой, шейдерами; также доступны: клавиатура/мышь/геймпад/сенсорный ввод, манипулирование изображениями и многое другое.
Glove [110] другой игровой фреймворк, достигший пика своего развития.
ИСКУССТВЕННЫЙ ИНТЕЛЛЕКТ И МАШИННОЕ ОБУЧЕНИЕ.
Crystal-fann [111] интеграция с быстрой искусственной нейронной сетью для ЯП Crystal; FANN разработана компанией NeuraLegion для своих программных продуктов.
Ai4cr [112] является портом Ruby Playground, специально для исследователей ИИ.
НАБОР ИНСТРУМЕНТОВ.
crystal-futures [https://github.com/dhruvrajvanshi/crystal-futures] это реализация нестандартного типа "Future", подобная той, что появилась в JavaScript. Futures предлагает заманчивую возможность осуществлять многие операции одновременно, создавая эффективным и неблокируемым способом асинхронный параллельный код.
Crystal-clear [https://github.com/Groogy/crystal-clear] реализует концепцию "Дизайн по Контракту", используя макросы.
ПРИВЯЗКИ К СТОРОННИМ API.
Twitter-crystal [115] это библиотека для доступа к API Твиттера.
А ещё существуют привязки для "Salesforce REST API", а также для Spotify, Google Maps, Docker, GitHub, Slack, SoundCloud и многих других.
______________
В добрый путь...
В последней главе мы познавали могущество Crystal в царстве глобальных сетей: его штатный http-сервер, лёгкий и быстрый веб-фреймворк Kemal и, наконец, полновесный Янтарный веб-фреймворк. Вы также получили ключи от сундука с кристальными осколками.
Близится конец нашего Хрустального путешествия по страницам этой книги. Мы надеемся, что она вам понравилась так же, как нам понравилось писать её. И если в этом головокружительном вояже вам почудилось, что Crystal сверкнул для вас как восходящая звезда в мире разработки ПО, возможно, вы захотите использовать этот язык в ваших проектах. Присоединяйтесь к нашему сообществу и проявите свои таланты в программировании. Быть может, мы встретимся снова во Вселенной Кристалла.