2017-01-14 7 views
2

Я изучал, как возиться с специальными механизмами сортировки в Ruby. Я в конечном итоге переписывания this neat JavaScript solution в Ruby:Возможно ли переопределить встроенный метод Ruby вне класса или модуля?

class SpecialStr 
    include Comparable 
    attr_accessor :str 
    def initialize (str) 
    @str = str 
    end 

    def <=> (other) 
    self_num, self_string = @str.split(' ') 
    other_num, other_string = other.str.split(' ') 
    self_num > other_num ? 1 : other_num > self_num ? -1 : 
     self_string > other_string ? -1 : 1 
    end 
end 

arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z'] 
arr_object = [] 
arr.each { |str| arr_object << SpecialStr.new(str) } 
arr_object.sort! { |x, y| y <=> x } 
output_arr = [] 
arr_object.each { |obj| output_arr << obj.str} 
puts output_arr 

Это желаемый выход (номер по убыванию, то строки по возрастанию):

8540 xxxxxx 
38 xxxx 
20 axxx 
20 bx 
2 m 
2 xxx 
2 z 

Но код казалось излишне сложным. (Руби должен быть более кратким, чем JS!) Поэтому я спросил себя (и теперь я спрашиваю вас), почему я не могу это сделать?

def <=> (other) 
    self_num, self_string = self.split(' ') 
    other_num, other_string = other.split(' ') 
    self_num > other_num ? 1 : other_num > self_num ? -1 : 
    self_string > other_string ? -1 : 1 
end 
arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z'] 
arr.sort! { |x, y| y <=> x } 
puts arr 

Это выводит неправильно, основываясь на sort, как если бы я не переопределяется <=>:

8540 xxxxxx 
38 xxxx 
20 bx 
20 axxx 
2 z 
2 xxx 
2 m 

код здесь короче, но не работает. Он использует версию <=>, встроенную в модуль Ruby's Comparable, а не мою попытку переопределить его. Почему я не смог его переопределить? Могут ли методы быть переопределены только внутри классов или модулей? Есть ли там более короткий способ написать этот первый скрипт в Ruby? (Извините, если это нуб вопрос, я новичок.)

ответ

3

Самый простой способ - разбить строку на число и слово и отсортировать по Массиву минус-номера (чтобы получить уменьшающиеся числа) и слово:

arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z'] 

arr.sort_by! do |number_word| 
    number, word = number_word.split 
    [ -number.to_i, word ] 
end 

puts arr 
# => 
# 8540 xxxxxx 
# 38 xxxx 
# 20 axxx 
# 20 bx 
# 2 m 
# 2 xxx 
# 2 z 

При сортировке массивов первый элемент (-number) имеет приоритет. Если оба первых элемента одинаковы, сортировка использует второй элемент (word).

+0

Святое дерьмо! Это намного проще, чем что-либо, что я или другие люди на этой более ранней теме! Отлично сработано. Не отвечает на вопрос, но полностью решает исходную проблему! – globewalldesk

1

Когда вы пишете

arr.sort! { |x, y| y <=> x } 

, что эквивалентно

arr.sort! { |x, y| y.<=>(x) } 

т.е. называет версию y «х Оператор <=> (космический корабль). Поскольку y - это всего лишь String, это выполняет сравнение по умолчанию для строк.

Чтобы написать код более сжато вы можете просто написать пользовательскую логику сравнения в блоке передается в sort!:

arr.sort! do |x, y| 
    x_num, x_string = x.split(' ') 
    y_num, y_string = y.split(' ') 
    y_num > x_num ? 1 : x_num > y_num ? -1 : 
    y_string > x_string ? -1 : 1 
end 

или альтернативно, записать его в качестве самостоятельного метода:

def my_compare(x, y) 
    x_num, x_string = x.split(' ') 
    y_num, y_string = y.split(' ') 
    y_num > x_num ? 1 : x_num > y_num ? -1 : 
    y_string > x_string ? -1 : 1 
end 

и называть это от sort!:

arr.sort! { |x, y| my_compare(x, y) } 

Несколько вещей, которые могли бы помочь прояснить:

В Ruby, нет свободных плавающих методов (т.е. методы, не привязанные к классу или модулю). Когда вы пишете def ... вне любого класса или модуля, метод добавляется к Object как метод экземпляра. Строго, есть unbound methods, но даже они нуждаются в ассоциировании с объектом, прежде чем они могут быть вызваны.

Следующая вещь, чтобы иметь в виду, где реализация по умолчанию <=> исходит из: это на Kernel модуль, который включен по классу Object.

Итак, когда вы пишете def <=>(other) ...вне класса переопределение метода для Object:

[1] pry(main)> method(:<=>).owner 
=> Kernel 
[2] pry(main)> def <=>(other) 
[2] pry(main)* puts "overridden!" 
[2] pry(main)* end 
=> :<=> 
[3] pry(main)> method(:<=>).owner 
=> Object 

Однако String класса переопределяет <=> сами. Для сравнения строки с другим объектом реализация String будет использоваться вместо реализации в Object, , даже если вы переопределили метод в Object.

Однако, если у вас есть класс, который не имеет своего собственного <=> (или преобладающего осуществление между ним и Object в иерархии классов), то действительно будет использоваться ваш переопределяется метод на Object:

[6] pry(main)> class Something; end 
=> nil 
[7] pry(main)> s1 = Something.new 
=> #<Something:0x007fddb4431ba8> 
[8] pry(main)> s2 = Something.new 
=> #<Something:0x007fddb4469760> 
[9] pry(main)> s1 <=> s2 
overridden! 
=> nil 

Объяснение того, что было продемонстрировано в настоящее время pry

Первый фрагмент кода с помощью метода method схватить метод, а затем с помощью owner, чтобы узнать, где в иерархии классов определяется этот метод.

Так еще один пример:

class Animal 
    def eat 
    puts "Munch!" 
    end 
end 

class Dog < Animal 
    def bark 
    puts "yap!" 
    end 
end 

Так что, если у нас есть собака:

buddy = Dog.new 

мы можем узнать, где его методы приходят из:

[10] pry(main)> buddy.method(:eat).owner 
=> Animal 
[11] pry(main)> buddy.method(:bark).owner 
=> Dog 

так в в первоначальном примере мы увидели, что <=> начал со ссылкой на метод из Kernel, но когда мы сделали def <=> ... это добавило метод непосредственно к Object, который теперь переопределял включенный метод.

Во втором примере показано, что происходит, когда существует минимальный класс без собственной реализации <=>. instance_methods(false) может показать нам методы экземпляра, которые непосредственно реализуются в классе. Пустой Something класс не имеет какой-либо :)

[14] pry(main)> Something.instance_methods(false) 
=> [] 

поэтому он будет использовать унаследованную <=> метод.

+1

Или перейдите прямо к 'arr.sort! (& Method (: my_compare))' и пропустите дополнительный шум явного блока. –

+0

Очень приятно. Да, конечно, мне не нужно использовать Comparable. Я мог бы просто написать автономный метод. Имеет смысл. Решает проблему, но все же не делает (не так?) Ответ на вопрос, можно ли переопределить определение '<=>' вне класса или модуля. – globewalldesk

+0

@globewalldesk В конце ответа я добавил еще немного фона, которое должно помочь в ответе на этот оригинальный вопрос. Если это все еще не ясно, тогда дайте мне знать, и я возьму еще одну трещину. – mikej

1

Ваша проблема заключается в том, что это:

y <=> x 

просто фантазии (и человек дружественный) способ написания:

y.<=>(x) 

поэтому оператор <=> не вызов функции, то это метод , вызывающий левую сторону оператора. Этот вызов метода не будет использовать ваш def <=>, потому что ваш метод компаратора не определен для объектов в сортировке массива, вы создали свой метод <=> в каком-то другом классе.

В JavaScript, вы говорите вещи, как это: больше

a.sort(function(a, b) { ... }) 

или в наше время:

a.sort((a, b) => ...) 

так что вы раздающие sort функцию, чтобы использовать компаратор, вы не определяя оператор компаратора где угодно, просто функцию, которая принимает два аргумента и возвращает желаемое значение.

В Ruby, вы обычно используете блоки, как «обратные вызовы»:

arr.sort! do |a, b| 
    a_num, a_string = a.split(' ') 
    b_num, b_string = b.split(' ') 
    a_num > b_num ? 1 : b_num > a_num ? -1 : a_string > b_string ? -1 : 1 
end 

Прежде чем мы продолжим, у вас есть проблемы с компаратором логикой, потому что блок для Enumerable#sort предполагается

возвращения -1, 0 или +1 в зависимости от сравнения между a и b.

и ваш блок не обрабатывает случай 0 (равенство). Кроме того, ваши _num s все еще строки, поэтому они не будут сравнивать числа. Первая проблема может быть решена с помощью Array#<=> (который сравнивает массивы поэлементно), а затем второй может быть установлена ​​с помощью простого to_i вызова:

arr.sort! do |a, b| 
    a_num, a_string = a.split(' ') 
    b_num, b_string = b.split(' ') 
    [a_num.to_i, a_string] <=> [b_num.to_i, b_string] 
end 

Вы можете пойти на шаг дальше за счет перехода к sort_by!:

arr.sort_by! do |e| 
    i, s = e.split(' ') 
    [i.to_i, s] 
end 

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

cmp = ->(a, b) do 
    a_num, a_string = a.split(' ') 
    b_num, b_string = b.split(' ') 
    [a_num.to_i, a_string] <=> [b_num.to_i, b_string] 
end 
arr1.sort!(&cmp) 
arr2.sort!(&cmp) 

natural = ->(e) do 
    i, s = e.split(' ') 
    [i.to_i, s] 
end 
arr1.sort_by!(&natural) 
arr2.sort_by!(&natural) 

или отдельный метод:

def cmp(a, b) 
    a_num, a_string = a.split(' ') 
    b_num, b_string = b.split(' ') 
    [a_num.to_i, a_string] <=> [b_num.to_i, b_string] 
end 

def some_other_method 
    arr1.sort!(&method(:cmp)) 
    arr2.sort!(&method(:cmp)) 
end 

def natural(e) 
    i, s = e.split(' ') 
    [i.to_i, s] 
end 

def some_other_other_method 
    arr1.sort_by!(&method(:natural)) 
    arr2.sort_by!(&method(:natural)) 
end 

Если вы на самом деле означает для сравнения значений self_number и other_number как строки, а затем оставить вне to_i вызовы и дальнейшее упрощение блоков/лямбды:

arr.sort! { |a, b| a.split(' ') <=> b.split(' ') } 
arr.sort_by! { |e| e.split(' ') } 
+0

Это часть, которую мне не хватало: «потому что ваш метод компаратора не определен на объектах в массиве, который вы сортируете, вы создали свой метод <=> в каком-то другом классе». Благодарю. – globewalldesk