Вопрос

Нередко возникает желание реализовать <=> (сравнение, или "космический корабль") оператор для типа данных продукта, то есть класса с несколькими полями (все из которых (мы надеемся!) уже есть <=> реализовано), сравнивая поля в определенном порядке.

def <=>(o)
    f1 < o.f1 && (return -1)
    f1 > o.f1 && (return  1)
    f2 < o.f2 && (return -1)
    f2 > o.f2 && (return  1)
    return 0
end

Это одновременно утомительно и чревато ошибками, особенно при большом количестве полей.Это настолько подвержено ошибкам, что я часто чувствую, что должен выполнить модульное тестирование этой функции, что только добавляет утомительности и многословия.

Haskell предлагает особенно приятный способ сделать это:

import Data.Monoid (mappend)
import Data.Ord (comparing)

-- From the standard library:
-- data Ordering = LT | EQ | GT

data D = D { f3 :: Int, f2 :: Double, f1 :: Char } deriving Show

compareD :: D -> D -> Ordering
compareD = foldl1 mappend [comparing f1, comparing f2, comparing f3]

(Для тех, кто не знаком с fold, вышесказанное расширяется до

comparing f1 `mappend` comparing f2 `mappend` comparing f3

который создает функцию, которая может быть применена к двум Ds, чтобы произвести Ordering.)

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

На самом деле, вопрос может быть даже немного интереснее этого, поскольку я, возможно, не захочу использовать только стандартный <=> оператор, но сортируется по-разному в разное время, например:

sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a]
sortByOrderings = sortBy . foldl1 mappend

sortByF3F1 = sortByOrderings [comparing f3, comparing f1]
sortByF2F3 = sortByOrderings [comparing f2, comparing f3]

Итак, вопросы:

  1. Каков типичный способ реализации подобных вещей в Ruby?
  2. Какой самый приятный способ сделать это, используя только то, что определено в стандартных библиотеках?
  3. Насколько близко можно подойти к приведенному выше коду Haskell и насколько он надежен по сравнению с ним?При необходимости, как можно гарантировать, что поля имеют правильно реализованный <=> или < и > операторы?

Кстати, хотя это вопрос Ruby, я рад рассмотреть обсуждение методов Haskell по теме, если старейшины этого сайта с этим согласны.Пожалуйста, не стесняйтесь комментировать, уместно это или нет, и, если да, также пометьте этот пост "haskell".

Это было полезно?

Решение

Вот рифф к вашей идее.Он не определяет никаких дополнительных констант, позволяет вам использовать любую комбинацию переменных экземпляра и методов для сравнения двух объектов, имеет ранний выход из not-equal и включает в себя все методы, определенные Comparable.

class Object
    def self.compare_by(*symbols)
        include Comparable
        dispatchers = symbols.map do |symbol|
          if symbol.to_s =~ /^@/
            lambda { |o| o.instance_variable_get(symbol) }
          else
            lambda { |o| o.__send__(symbol) }
          end
        end
        define_method('<=>') do |other|
          dispatchers.inject(0) do |_,dispatcher|
            comp = dispatcher[self] <=> dispatcher[other]
            break comp if comp != 0
            comp
          end
        end
    end
end

class T
    def initialize(name,f1,f2,f3)
      @name,@f1, @f2, @f3 = name,f1, f2, f3;
    end

    def f1
      puts "checking #@name's f1"
      @f1
    end
    def f3
      puts "checking #@name's f3"
      @f3
    end

    compare_by :f1, :@f2, :f3
end

w = T.new('x',1,1,2)
x = T.new('x',1,2,3)
y = T.new('y',2,3,4)
z = T.new('z',2,3,5)

p w < x   #=> checking x's f1
          #   checking x's f1
          #   true
p x == y  #=> checking x's f1
          #   checking y's f1
          #   false
p y <= z  #=> checking y's f1
          #   checking z's f1
          #   checking y's f3
          #   checking z's f3
          #   true

Если вы хотите, вы могли бы вставить туда дополнительную проверку ошибок, чтобы убедиться, что значения, используемые для сравнения, действительно соответствуют <=> (используя respond_to? '<=>'), и попытайтесь выдавать более четкие сообщения об ошибках в случае, когда они этого не делают.

Другие советы

Вот что я делаю, чтобы сделать пользовательские правила сортировки более управляемыми:во всех моих классах, которые мне когда-либо нужно сортировать, я определяю методы "to_sort", которые возвращают массивы, а затем переопределяю <=> чтобы использовать to_sort:

class Whatever
  def to_sort
    [@mainkey,@subkey,@subsubkey]
  end

  def <=>(o)
    self.to_sort <=> o.to_sort
  end
end

Таким образом, сортировка любого массива Whatevers (включая разнородные массивы Whatevers и Whateverothers и Whathaveyours, все из которых реализуют функции to_sort, зависящие от типа, и этот же <=> переопределить) просто переходит к внутренней сортировке массива массивов.

Я придерживался того же подхода, что и рэмпион, но хотел обработать случай, когда атрибуты могли бы быть nil.

module ComparableBy
  def comparable_by(*attributes)
    include Comparable

    define_method(:<=>) do |other|
      return if other.nil?
      attributes.each do |attribute|
        left  = self.__send__(attribute)
        right = other.__send__(attribute)
        return -1 if left.nil?
        return 1 if right.nil?
        comparison = left <=> right
        return comparison unless comparison == 0
      end
      return 0
    end
  end
end

Пример использования:

SomeObject = Struct.new(:a, :b, :c) do
  extend ComparableBy
  comparable_by :a, :b, :c
end

Что ж, вот быстрый взлом расширения к Object чтобы это произошло тем, что кажется достаточно приятным способом.

class Object

    def self.spaceship_uses(*methods)
        self.const_set(:SPACESHIP_USES, methods)
    end

    def <=>(o)
        raise(NoMethodError, "undefined method `<=>' for #{self.inspect}") \
            unless self.class.const_defined?(:SPACESHIP_USES)
        self.class.const_get(:SPACESHIP_USES).each { |sym|
            self.send(sym) < o.send(sym) && (return -1)
            self.send(sym) > o.send(sym) && (return  1)
        }
        return 0
    end

end

class T

    def initialize(f1, f2) @f1, @f2 = f1, f2; end

    attr_reader    :f1, :f2
    spaceship_uses :f1, :f2

end

Это, конечно, не касается каких-либо проблем с набором текста, чтобы убедиться, что < и > должным образом реализованы для объектов, возвращаемых методами в SPACESHIP_USES.Но тогда gain, будучи Ruby, это, наверное, прекрасно, не так ли?

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top