Иерархическое использование уточнений

Уточнения были экспериментальным дополнением к версии 2.0, затем изменен и сделан постоянным в v2.1. Он дает возможность избежать «обезьяньих исправлений», предоставляя «способ локального расширения класса».

Я попытался применить Refinements к этому недавнему вопросу, который я упрощу таким образом:

a = [[1, "a"],
     [2, "b"],
     [3, "c"],
     [4, "d"]]

b = [[1, "AA"],
     [2, "B"],
     [3, "C"],
     [5, "D"]]

Элемент со смещением i в a соответствует элементу со смещением i в b, если:

a[i].first == b[i].first

а также

a[i].last.downcase == b[i].last.downcase

Другими словами, соответствие строк не зависит от регистра.

Проблема состоит в том, чтобы определить количество элементов a, которые соответствуют соответствующему элементу b. Мы видим, что ответов два, элементы со смещениями 1 и 2.

Один из способов сделать это — выполнить обезьянью исправление Строка#==:

class String
  alias :dbl_eql :==
  def ==(other)
    downcase.dbl_eql(other.downcase)
  end
end

a.zip(b).count { |ae,be| ae.zip(be).all? { |aee,bee| aee==bee } }
  #=> 2

или вместо этого используйте Refinements:

module M
  refine String do
    alias :dbl_eql :==
    def ==(other)
      downcase.dbl_eql(other.downcase)
    end
  end
end

'a' == 'A'
  #=> false (as expected)
a.zip(b).count { |ae,be| ae.zip(be).all? { |aee,bee| aee==bee } }
  #=> 0 (as expected)

using M
'a' == 'A'
  #=> true
a.zip(b).count { |ae,be| ae.zip(be).all? { |aee,bee| aee==bee } }
  #=> 2

Однако я хотел бы использовать Refinements вот так:

using M
a.zip(b).count { |ae,be| ae == be }
  #=> 0

но, как видите, это дает неверный ответ. Это потому, что я вызываю Array#== и уточнение не применяется в пределах Array.

Я мог бы сделать это:

module N
  refine Array do
    def ==(other)
      zip(other).all? do |ae,be|
        case ae
        when String
          ae.downcase==be.downcase
        else
          ae==be
        end
      end  
    end
  end
end

using N
a.zip(b).count { |ae,be| ae == be }
  #=> 2

но это не то, что я хочу. Я хочу сделать что-то вроде этого:

module N
  refine Array do
    using M
  end   
end

using N
a.zip(b).count { |ae,be| ae == be }
  #=> 0

но ясно, что это не работает.

Мой вопрос: есть ли способ уточнить String для использования в Array, а затем уточнить Array для использования в моем методе?


person Cary Swoveland    schedule 26.03.2015    source источник


Ответы (1)


Вау, с этим было действительно интересно поиграться! Спасибо, что задали этот вопрос! Я нашел способ, который работает!

module M
  refine String do
    alias :dbl_eql :==
      def ==(other)
        downcase.dbl_eql(other.downcase)
      end
  end

  refine Array do
    def ==(other)
      zip(other).all? {|x, y| x == y}
    end
  end
end

a = [[1, "a"],
     [2, "b"],
     [3, "c"],
     [4, "d"]]

b = [[1, "AA"],
     [2, "B"],
     [3, "C"],
     [5, "D"]]

using M

a.zip(b).count { |ae,be| ae == be } # 2

Без переопределения == в Array уточнение не будет применяться. Интересно, что это также не работает, если вы делаете это в двух отдельных модулях; это не работает, например:

module M
  refine String do
    alias :dbl_eql :==
      def ==(other)
        downcase.dbl_eql(other.downcase)
      end
  end
end

using M

module N
  refine Array do
    def ==(other)
      zip(other).all? {|x, y| x == y}
    end
  end
end

a = [[1, "a"],
     [2, "b"],
     [3, "c"],
     [4, "d"]]

b = [[1, "AA"],
     [2, "B"],
     [3, "C"],
     [5, "D"]]

using N

a.zip(b).count { |ae,be| ae == be } # 0

Я недостаточно знаком с деталями реализации refine, чтобы быть полностью уверенным в том, почему происходит такое поведение. Я предполагаю, что внутренняя часть блока уточнения рассматривается как вход в другую область верхнего уровня, аналогично тому, как уточнения, определенные за пределами текущего файла, применяются только в том случае, если файл, в котором они определены, анализируется с require в текущем файле. . Это также объясняет, почему вложенные уточнения не работают; внутреннее уточнение выходит за рамки в тот момент, когда оно выходит. Это также объясняет, почему исправление обезьяны Array работает следующим образом:

class Array
  using M

  def ==(other)
    zip(other).all? {|x, y| x == y}
  end
end

Это не становится жертвой проблем с областью действия, которые создает refine, поэтому refine на String остается в области действия.

person Zoë Sparks    schedule 27.03.2015
comment
Замечательно! Одна деталь: вы можете подумать о замене !self.zip(other).map {|x, y| x == y}.include? false на zip(other).all? {|x, y| x == y}. (Напомним, что self является получателем по умолчанию.) - person Cary Swoveland; 27.03.2015
comment
Ах, да, спасибо — у меня появилась дурная привычка использовать self везде, где это применимо. Это поможет мне не забыть подумать о том, имеет ли смысл его использовать. Здесь он выглядит намного лучше/читабельнее без self и с использованием all?. - person Zoë Sparks; 27.03.2015
comment
Многие рубисты используют self, когда это не нужно, потому что считают, что его отсутствие может ввести читателя в заблуждение. Я не в том лагере, но я не могу сказать, что они не правы. - person Cary Swoveland; 27.03.2015
comment
Мне кажется, это действительно зависит от ситуации. Бывают случаи, когда я бы намеренно использовал self там, где его можно было бы опустить, но в этом случае кажется, что строка кода настолько проста, что ее легче читать, если опустить self. Если бы это была более сложная/необычная строка кода, было бы легче сразу понять, используется ли self. - person Zoë Sparks; 27.03.2015