Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Monkey patching и Duck typing в Ruby: а проблема ли это самого языка?

Много разговоров и плевков в адреc Ruby из-за механизмов monkey patching и duck typing. Всё дело в том, что язык Ruby настолько мощный и гибкий, что позволяет реализовывать подобные трюки в коде, но недостатки ли это самого языка или всё же тех писателей, которые прибегают к использованию данных механизмов в своём коде?

Monkey patching

Про monkey patching вообще уже можно забыть, если вы освоите и будете использовать механизм Refinements. Приведу очень простой пример:

class Test

  module TestString
    refine String do
      def blank? = (self.length == 0 || self =~ /\A\p{Space}+\z/u) ? true : false
    end
  end

  using TestString

  def self.str_blank?(str) = str.blank?

end

str = "abc"
strb = "   "

p str.blank?              # => undefined method 'blank?' for an instance of String (NoMethodError)
p Test::str_blank?(str)   # => false
p Test::str_blank?(strb)  # => true

И всё! Вопрос закрыт - дополнительный метод для класса String действует только внутри класса Test, никаких побочных эффектов ни на другие классы и модули, ни, тем более, в глобальном пространстве имён нет.

Duck typing

class A

  def hello = p "class A hello"

  def hello_any(who)
    who.hello
  end

  # принимаем только экземпляры класса А или его потомков
  def hello_only_descendant(who)

    # схоже на поведение метода с видимостью protected, но это лишь пример
    # вполне возможна проверка любых других условий, не только 
    # принадлежность к определённому классу
    raise 'not a A' unless who.is_a? A  # только А или потомки, все остальные - чужаки

    who.hello

  end

end

class B < A
  def hello = p "class B hello: descendant"
end

class A_other
  def hello = p "class A_other hello"
end

a = A::new
b = B::new
other = A_other::new

a.hello             # => "class A hello"
b.hello             # => "class B hello: descendant"
other.hello         # => "class A_other hello"

a.hello_any(a)      # => "class A hello"
a.hello_any(b)      # => "class B hello: descendant"
a.hello_any(other)  # => "class A_other hello"

a.hello_only_descendant(a)      # => "class A hello"
a.hello_only_descendant(b)      # => "class B hello: descendant"
a.hello_only_descendant(other)  # => in 'A#hello_only_descendant': not a A (RuntimeError)

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

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

a = 1
b = '123'
c = a + b  # => in 'Integer#+': String can't be coerced into Integer (TypeError)

Но, в то же время, и не запрещает безоговорочно - есть возможность приведения к ожидаемому (совместимому) типу (принудительного изменения типа) (coerce). Для продолживших быть несогласными могу вас адресовать, например, к языку программирования Java - там механизм приведения к совместимому типу используется очень широко. В данном случае необходимо реализовать обработку аргумента типа String для метода + экземпляра класса Integer.

Неплохая статья по теме Let’s talk about Type Coercion in Ruby, хотя обсуждений этого механизма Ruby в Интернет достаточно много. И так же, о методах для приведения к соответствующему (базовому) классу (Array, Hash, Integer, String) #to_s or #to_str? Explicitly casting vs. implicitly coercing types in Ruby.

Самый простой вариант приведения типов - использовать явное преобразование типа:
c = a + b.to_i

Возможно и более гибкое поведение, например, проводить какие-то свои преобразования типов при определённых операциях. В нижеследующем примере, наоборот (для наглядности), реализуем приведение к типу String других типов (Integer и Float) для метода + класса String.

class Test

  module TestString

    refine String do

      def +(other)

        case other
          when Integer
            self.class.new("#{self}+i#{other.to_s}")
          when Float
            self.class.new("#{self}+f#{other.to_s}")
          else
            super
        end

      end

    end

  end

  using TestString

  def self.add(num)
    'add: ' + num
  end

end

p Test::add('123')   # => "add: 123"
p Test::add(123)     # => "add: +i123"
p Test::add(123.45)  # => "add: +f123.45"

Возможно так же автоматическое (неявное) приведение к базовым типам (Array, Hash, Integer, String) (Implicit conversion) - для этого в классе должны быть определены методы экземляра класса, соответственно: to_ary, to_hash, to_int, to_str.
В данном примере научим Integer "превращаться" в строку:

# определяем метод для стандартного класса Integer, вызывая бурю
# эмоций от указывающих на monkey pathcing ;-) )))
class Integer
  def to_str = "Integer: #{self}"
end

a = String.new(123)

b = a + ", " + 456

p a  # => "Integer: 123"
p b  # => "Integer: 123, Integer: 456"

Не забываем и про видимость методов модулей и классов в парадигме ООП

Например, использование protected может сильно облегчить контроль "уток" от "не уток, но крякающих" на уровне механизмов самого языка. Ознакомиться с ограничениями видимости методов модулей и классов можно здесь.

А проблема ли это Ruby?

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