Семинар 3: Основы Ruby

Язык программирования Ruby - достаточно популярный в Европе и Северной Америке динамический язык программирования общего назначения.

Этот язык весьма своеобразен. В некоторой степени на его своеобразие повлиял язык Perl - именно благодаря нему в Ruby есть переменные, начинающиеся со знака $. В некоторой же степени своеобразие Ruby и высокий порог входа в него обусловлены философией этого языка. Согласно философии Ruby программа - это текст на естественном языке, именно поэтому можно не писать скобки при вызове методов, поэтому в названиях методов могут присуствовать знаки препинания и т.п. В классических языках программирования так делать не принято, поэтому с непривычки Ruby может показаться странным. Однако несмотря ни на что стоит изучить этот язык, хотя бы для того, чтобы расширить свой кругозор.

Немного воды

Как было сказано ранее, Ruby - это динамический язык программирования. Это зачит, что переменные в Ruby являются указателями на объекты, которые создает интерпретатор. Также программа на Ruby исполняется до первой ошибки и сразу падает. В общем-то ничего нового, все очень похоже на JS (спойлер - это распространяется на все динамические языки программирования).

Более того, в Ruby также как и в JS существует глобальный объект (но в Ruby он ведет себя гораздо более предсказуемо).

Но есть и некоторые отличия от того, что мы видели раньше. Во-первых, Ruby - это в первую очередь объектно-ориентированный язык программирования. Таким образом, в нем есть и классы, и объекты и ведут себя они ровно так, как мы привыкли видеть в C++, Delphi, Java. Во-вторых, Ruby - это концептуально чистый (то есть такой, в котором почти (sic!) нет исключений из какого-то правила) язык так называемого сверхвысокого уровня. Это значит, что почти все конструкции языка подчинены какой-то абстрации высокого уровня. В Ruby такая абстракция - это класс. Все в Ruby является объектом какого-либо класса, начиная с самой программы и заканчивая обычным литералом. Почти все вызовы функций (или даже операторов) - это вызовы методов каких-либо объектов, как явные, так и неявные (в смысле того, что на первый взгляд неочевидно, что это вызов метода, например, 2 + 2 - это с точки зрения Ruby 2.+(2)). Если никакого объекта нет, а функция все же вызывается, не спешите думать, что абстракция Ruby дырява - в таком случае функции являются методами того самого глобального объекта.

Даже класс в Ruby - это объект. В самом деле, если людям пришло в голову функция - это объект, то почему бы им не сделать объектом класс? Вообще такой подход (делать все объектами) характерен для динамических языков программирования и позволяет писать очень простые и гибкие программы.

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

function inner(argument) {
    console.log(argument);
}

function outer(a, f) {
    f(a);
}

outer(10, inner);

В Ruby можно было бы сделать так, если бы не одно но - при вызове методов можно не писать скобки, поэтому ни интерпретатор, ни даже человек не сможет точно сказать, что это

function

вызов функции function() или ссылка на функцию. По этой причине в Ruby ввели блоки кода.

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

Пока стоит усвоить, что блок может быть создан одним из жвух способов:

# f - это вызов какой-то функции, к нему мы добавляем свой блок.
f do |# при необходимости можно указать переменные, которые принимает блок |
    # код блока.
end

# в || указываются переменные, дальше идет тело (как правило однострочное).
f {|x| }

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

Требования к структуре программ

В задании к лабораторным работам есть требование - каждая программа должна быть разбита на три файла:

  • user.rb - файл, в котором производится работа с пользователем (ввод-вывод, сообщения об ошибках и т.п.);
  • src.rb - файл, содержащий функцию или функции, которые выполняют задание. Тут ничего не вводится и не выводится, только расчеты;
  • test.rb - файл с тестами.

Стоит пояснить, откуда вообще взялось такое разделение. Естественно, в рамках чисто учебных, к тому же простых задач оно кажется избыточным, но в целом в разработке что-то похожее используется повсеместно. Оказывается очень удобным отделить логику программы от логики обработки пользовательского ввода, потому что по своей сути это разные задачи, не пересекающиеся друг с другом. Разделение на разные модули сильно упрощает логику программы и делает ее более читаемой. В нашем случае в качестве модулей выступают файлы user.rb и src.rb.

Окей, с разделением логики и пользовательского ввода мы разобрались, а как насчет тестов? Для начала пару слов о том, что такое тесты. С первого курса мы привыкли тестировать программу руками и это вполне оправдано - написал, отладил, сдал, забыл. Другое же дело, если эта программа должна поддерживаться на протяжении длительного времени. Если тестировать ее руками, то рано или поздно (особенно если в разработке будет перерыв на пару месяцев и вы забудете, что программировали) при тестировании руками вы начнете делать это все хуже и хуже. Во-первых, будет лень и это понятно - можно простестировать 1 раз, 2, 3, 10, но делать 20-й раз одно и то же кому угодно надоест, особенно если тестировать надо много чего - это и времени занимает кучу. Во-вторых, вы будете ошибаться вне зависимости от лени - просто что-то забудете проверить после перерыва. Таким образом, получается, что тестировать руками большие сложные проекты неэффективно.

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

Для всего этого придумали тесты или автоматические тесты. По сути это программа, которая тестирует другую программу. Она вызывает, к примеру, функцию из вашей программы, что-то подает на вход и сравнивает результат с ожидаемым значением (которое задает тестирующий).

Сейчас, мы конечно не работаем над сложными проектами, поэтому объективно тесты можно было бы и не писать. Но вы должны найчиться делать это, поэтому в требованиях к выполнению лабораторных работ есть файл test.rb.

Названия файлов

Пожалуйста, называйте файлы именно user.rb, src.rb и test.rb - так гораздо легче проверять, преподаватели будут вам очень признательны.

Организация кода для выполнения лабораторных работ

Допустим, мы получили задание "Реализовать программу, которая на ввод пользователя отвечает числом 42". Разобьем программу на файлы:

  • в файл src.rb поместим функцию, которая будет возвращать число 42;
  • в файл user.rb поместим всю логику, отвечающую за ввод данных и вывод результата;
  • в файл test.rb поместим тесты функции из src.rb

Итак, рассмотрим файлы по порядку:

src.rb

def return_42
  42
end

user.rb

require_relative 'src'

puts 'Введите любое число и получите в ответ 42:'
any_number = gets.to_i
puts "Вы ввели #{any_number}, получили в ответ #{return_42}"

test.rb

require 'minitest/autorun'
require_relative 'src'

class Return42Test < MiniTest::Unit::TestCase
  def test_returns_42
    assert_equal 42, return_42
  end
end

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

Функциональный стиль Ruby

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

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

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

Заключение

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