Ruby

Дисклеймор - мопед не мой, собирал по частям как на просторах интернета, так и по бумажной литературе. Например использовал официальную доку и книжку “Путь Ruby” Хэла Фултона.

Первое, что пишут разработчики про свой язык и что просят запомнить - наш язык не про скорость, наш язык про удобство разработчику. Так что не стоит ждать от него каких-либо откровений.

Немного о языке

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

puts "Hello world!"

на самом деле является вызовом

Kernel.puts("Hello world!")

Выполнить данный пример можно, набрав в консоли2) заветное ruby, вписав туда код, перейдя на новую пустую строку и нажав Control+D. Выглядеть это должно примерно так

$ ruby
puts "Hello world!"
<Ctrl+D>

Поскольку всё в Ruby приводится к объекту - следовательно даже к простой строке или числу можно с ходу применять всякие методы, ей свойственные, например

"Простая строка".length
-1400.abs

Очень важный момент, который сразу не даётся в книгах, но который, например, мне, как интересующемуся, очень даже интересен. Можно посмотреть все методы и поля класса/объекта двумя простыми способами:

<Class/Object>.methods
<Class/Object>.inspect

Не забываем менять <Class/Object> на нужные нам либо имя класса, либо переменную, в которой находится экземпляр.

Немного о синтаксисе

Синтаксис… Если вспомним начало статьи - там было чётко указано - язык для программистов, а не для программ. Отсюда следует, что

  • Точка с запятой в конце строки не нужна, так же игнорируются все проблеы в коде больше одного 3)
  • Круглые скобки не обязательны 4)
puts("Hello world!") # валидно
puts "Hello world!" # валидно
  • Фигурные скобки не испольузются, в конце блоков ставится end
def someFunc
  puts "Some string"
end
 
if true == true then
  puts "True is true"
end
  • Методы по умолчанию возвращают значение последней выполненной операции
def printString(str)
  "Print: #{str}"
end
  • Существует постфиксная форма записи условий 5)
puts "True is true" if true == true
puts "False is not true" unless false == true

Области видимости

Очень сильно хотелось написать контекст 6), однако здесь всё же префиксы используются для других целей. Итак, с помощью особых знаков мы можем определить область видимости переменной:

Локальные Глобальные Экземпляр Класс Статические и имена классов
test$test@test@@testTest
variable$variable@variable@@variableVariable

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

Массивы/Хэши

Здесь всё просто, как и в других скриптовых ЯП. Синтаксис, кстати, тот же:

array = [ 'first', 'second', 'third' ]
hash = { first: 1, second: 2, third: 3 }

Доступ, соответственно

array[0] # выведет first
hash[:first] # выведет 1

Кстати, литералы внутри квадратных скобок8) можно экранировать предворив двоеточием, но можно и взять в кавычки.

Дополнительно есть ещё один модный способ объявления массивов9)

array = %w{ first second third }

Управляющие структуры

Как и в любом уважающем себя языке в Ruby есть

  • if/elsif/else
  • unless
  • while
  • for

Работают все примерно так же как и везде, разве что вместо фигурных скобок для if/unless используется связка then … end10)

if true == true then
  puts "True is true"
end

А для for и while вообще блок открывать не надо (пример11))

for i in 1..3
  puts i
end

Все структуры имеют и постфиксную форму12)

square = square * square while square < 1000

Регулярки

Повсюду и везде13). Можно использовать в условиях:

if var =~ /test/ then
  puts "Variable contains 'test' word"
end

Можно натравливать непосредственно на переменную

str.sub(/Perl/, "Ruby") # Заменит первое вхождение Perl в строке на Ruby
str.gsub(/Python/, "Shit") # Заменит все вхождения слова Python на Shit

Блоки и итераторы

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

{ puts "hello" }
 
do
  puts "Hello"
end

Можно, например, объявить метод, который при получении блока кода выполнит его или операцию над ним. Для этого в методе используется волшебное слово yield. Простой пример, пока без магии

def yielding
  yield
end
 
yielding { puts "Test" }

Что же здесь произошло? Мы создали метод yielding с волшебным словом внутри, а потом первым аргументом передали ему блок кода, который и был выполнен14). А теперь магия - внутрь блока, который передаётся как аргумент можно передавать свои аргументы. Они должны быть объявлены через запятую в начале блока между двумя вертикальными чертами '|'. Итак, магия:

class Test
  def initialize(array)
    @array = array
  end
  def each
    i = 0
    while i < @array.length
      yield @array[i]
      i += 1
    end
  end
end
 
t = Test.new(%w{one two three})
t.each {|elem| puts "Element is #{elem}" }

При выполнении выведет

Element is one
Element is two
Element is three

Ну а дальше, имея в арсенале такую весёлую штуку как yield сделать итераторы проще простого

5.times { print "*" } # Выведет 5 звёздочек
3.upto(6) { |i| print i } # Выведет 3456
('a'..'e').each { |letter| print letter } # Выведет abcde

Чтение и вывод

Для вывода в Ruby чаще всего используется два метода - puts и print(f). Первый выводит каждый свой аргумент с новой строки, а print как заказано (printf совместим с printf из других языков программирования и позволяет форматировать строку как заблагорассудится, используя знак процента и послеующую букву для обозначения типа вставляемого значения)

puts 1,2,3

выведет

1
2
3
printf "Hello %s at %d.%d", "World", Time.now.day, Time.now.month

выведет

Hello World at 6.11

Стоит обратить внимание, что используя print(f) самому придётся следить за символами переноса строк.

Для чтения же со стандартного ввода есть простой метод gets.

variable = gets
puts variable

Дополнительно у gets есть хитрый эффект. Когда получаемые данные из стандартного ввода не передаются в переменную метод сохраняет их в глобальной переменной $_ 15), что позволяет делать лёгкие, но совершенно не читаемые на первый взгляд конструкции

while gets
  if /Ruby/
    print
  end
end

Однако если идти “Путём Ruby” - следует использовать итератор

ARGF.each { |line| print line if line =~ /Ruby/ }

Классы, объекты

Итак, мы подошли к самой мякотке, а именно ООП в Ruby. Ну и как водится - самое главное в классе, хоть и не обязательное, это конструктор. Не мудрствуя лукаво сразу шматок кода

class Test
  def initialize(str)
    @str = str
  end
 
  def hello
    puts "Hello " + @str
  end
end

Собственно что же произошло в блоке выше - мы определили класс с именем Test (имя класса является константой, как было написано выше, это следует запомнить) и описали метод, который вызывается при создании нового экземпляра этого класса. В этом методе мы получаем один аргумент и кладём его в переменную экземпляра. Второй метод нам нужен для следующего примера, однако сразу же стоит оговориться про такой важный момент, как видимость методов. Как и в большинстве объектно-ориентированных языков программирования ruby умеет разделять методы на публичные (public), защищённые (protected) и приватные (private), что, соответственно даёт право обращаться к методу всем извне, даёт право метод наследовать и даёт право использовать метод только внутри экземпляра. Конструктор класса всегда является приватным. Теперь создадим свой первый экземпляр нового класса:

t = Test.new('world')
t.hello()

На выходе получаем строку 'Hello world'. Поздравляю, мы только что создали свой первый экземпляр и вызвали его метод. В дополнение к модификаторам видимости хочется сказать, что ruby имеет своё собственное представление об их использовании. При написании модификатора перед объявлением метода - первый распространяется только на второй, но если модификатор стоит отдельно в строке - он распространяется на все методы, пока не будет указан другой модификатор или закончится объявление класса.

class Test
  def initialize(str)
    @str = str
  end
 
  private def concat(*args)
    args.join(" ")
  end
 
  def hello
    puts concat("Hello", @str)
  end
end
 
t = Test.new("world")
t.hello

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

  • Простые аргументы через запятую
def method(string, int)
  printf("%s - %d\n", string, int)
end
 
method("One", 1)
  • Именованные аргументы (если задано значение по умолчанию - аргумент можно опускать
def method(string:, int: 1)
  printf("%s - %d\n", string, int)
end
 
method(string: "One")
  • Хэш (причём если хэш стоит в конце, можно опускать фигурные скобки, однако круглые обязательны в любом случае)
def method(str, options)
  puts str, options
end
 
method('test', arg1: 1, arg2: 2)
  • Произвольное количество аргументов (удобно, например, для форматирования)
def method(*args)
  args.each do |element| puts element end
end
 
method('test')
method(1,2,3)

По поводу деструкторов всё очень просто - их не существует. В ruby используется сборщик мусора, который работает по принципу счётчика ссылок. Как только количество ссылок на объект достигает ноля - объект уничтожается. (Не самый лучший, но и не самый худший)

Наследование

Следующим аспектом ООП является наследование - бишь расширение базового класса новым функционалом без повторного написания одного и того же кода. Упрощает сопровождение - усложняет логику, но обо всё по порядку.

Итак, пример

class Animal
  def legs
    @legs
  end
end
 
class Dog < Animal
  def initialize
    @legs = 4
  end
end
 
class Duck < Animal
  def initialize
    @legs = 2
  end
end
 
dog = Dog.new()
duck = Duck.new()
 
puts dog.legs, duck.legs

Разберём по частям. Первым мы объявили класс родитель - Животное. У всех животных есть ножки16), поэтому их можно посчитать, а следовательно и вернуть значение из экземпляра. Но ножек у всех разное количество, поэтому дальше мы создали два класса потомка - Собаку и Утку17) и уже непосредственно в дочерних классах мы уточняем у какого животного сколько ножек. В конце мы создаём по экземпляру животного и вызываем ранее созданный метод, унаследованный от родителя. Сложно, запутанно, не понятно, но то ли ещё будет. Например если мы в дочернем классе переопределяем метод родительского класса, но нам зачем-то надо вызвать внутри метод родительского класса - можно использовать слово super

class RobbedDuck < Duck
  def legs(stolen_legs)
    super() - stolen_legs
  end
end
 
rduck = RobbedDuck.new()
puts rduck.legs(1)

Итак. в примере выше в родительском классе мы создали, по сути, геттер, как любят его нынче называть, но разработчики языка пошли дальше и придумали как упростить написание геттеров и сеттеров для поклонников языка и ввели следующие, я даже и не знаю, неверное методы: attr_reader (создаёт геттер), attr_writer (создаёт сеттер), attr_accessor (создаёт и геттер, и сеттер) Перепишем наш класс более простым способом

class Animal
  attr_accessor :legs
end
 
class Dog < Animal
  def initialize
    @legs = 4
  end
end
 
class Duck < Animal
  def initialize
    @legs = 2
  end
end
 
class RobbedDuck < Duck
  def legs(stolen_legs)
    super() - stolen_legs
  end
end
 
dog = Dog.new()
duck = Duck.new()
rduck = RobbedDuck.new()
 
dog.legs = 5
 
puts dog.legs, duck.legs, rduck.legs(1)

Как видно из примера - теперь я могу прикрутить собаке пятую ногу не особо заботясь о реализации метода присваивания полю экземпляра.

Виртуальные аттрибуты

На самом деле это лишь очередной способ сделать удобнее, но если так можно, почему бы и нет. Делаем геттер и сеттер и получаем псевдоаттрибут

class Road
  attr_accessor :length
 
  def initialize(length)
    @length = length
  end
 
  def lengthInKilometers
    @length/1000.0
  end
 
  def lengthInKilometers=(length)
    @length = (length*1000.0).to_i
  end
end
 
r = Road.new(100)
printf("Length of road %d (in km %f)\n", r.length, r.lengthInKilometers)
r.lengthInKilometers = 2.0
printf("New length of road %d (in km %f)\n", r.length, r.lengthInKilometers)

Поле и метод класса (статическое поле/метод)

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

class Post
  @@visitors = 0
 
  def initialize(text)
    @text = text
  end
  def render
    @@visitors += 1
    printf("Text: %s\nView count: %d\n\n", @text, @@visitors)
  end
 
  def self.visitors
    @@visitors
  end
end
 
p1 = Post.new("It's a first post")
p2 = Post.new("It's a second post")
p3 = Post.new("It's a third post")
 
p1.render
p2.render
p3.render
p1.render
puts "Current count of visitors is #{Post::visitors}"

Статческое поле будет иметь одно значение во всех участках кода. Для объявления методов класса используется ключевое слово self. Как один из примеров использования методов и полей класса может быть парадигма одиночек (singletone). Пожалуй здесь я не буду изобретать велосипед и скопирую пример из документации

class Logger
  @@logger = nil
 
  def self.create
    @@logger = self.new unless @@logger
    @@logger
  end
end
1)
которые на самом деле так же являются методами
2)
да-да, мы используем Linux, как это не странно
3)
это не относится к строкам
4)
везде
5) , 6) , 9) , 11) , 13) , 15)
привет Perl
7)
для знающих и умеющих более каноничные языки - статические члены - Class::variable
8)
и не только
10)
хотя даже слово then можно опустить
12)
каким бы высосанным из пальца этот пример бы ни был
14)
очень похоже на eval, но только похоже
16)
ну или почти у всех, но в нашем идеальном мире пусть будут у всех
17)
да-да, утка птица, но что я могу поделать