Реализация Ruby <=> Combinator - PullRequest
6 голосов
/ 19 мая 2009

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

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

, которая производит функцию, которая может быть применена к двум D с, чтобы создать 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'.

Ответы [ 4 ]

8 голосов
/ 19 мая 2009

Вот что я делаю, чтобы сделать пользовательские правила сортировки более управляемыми: на всех моих классах, которые мне когда-либо нужно сортировать, я определяю методы "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 и такое же переопределение <=>) просто направляется на внутреннюю сортировку массива массивов.

7 голосов
/ 20 мая 2009

Вот рифф вашей идеи. Он не определяет никаких дополнительных констант, позволяет использовать любую комбинацию переменных экземпляра и методов для сравнения двух объектов, имеет ранний выход при неравном и включает все методы, определенные 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? '<=>') и пытаются дать более четкие сообщения об ошибках, если они этого не делают.

2 голосов
/ 09 марта 2012

Я использовал аналогичный подход как rampion, но хотел обработать случай, когда атрибуты могут быть 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
0 голосов
/ 19 мая 2009

Хорошо, вот быстрый взлом на расширение 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. Но тогда, как Руби, выгода, это, наверное, хорошо, не так ли?

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

...