2. Десет малки хакчета

Краен срок: 13:00, 05 ноември 2008

Крайният срок вече изтече. Не може да пращате решения.

Ruby е динамичен език. Толкова, че ви позволява да променяте вградените к ласове. Понякога този подход се нарича monkey patching. Той е едно от огромните предимства на езика.

В тази задача ще трябва да направите десет различни промени във вградените класове на Ruby.

1. Symbol.to_proc

Една популярна идея е да предефинирате Symbol#to_proc така, че следните двойки редове да правят едно и също нещо:

array.map { |item| item.name }
array.map(&:name)

array.reject { |item| item.nil? }
array.reject(&:nil?)

array.inject { |a, b| a + b }
array.inject(&:+)

Когато руби види inject(&:name) той вика метода to_proc на :name и го предава като блок на inject.

2. 6 * 9

Ако сте чели пътеводителя, знаете големия въпрос и неговия отговор. За нещастие, Ruby не го знае. Още нещо важно, което липсва в езика (като истината че 0 == 1, нали).

Предефинирайте умножението на числа така, че 6 * 9 и 9 * 6 да връщат 42. Всички останали умножения да работят постаро му.

3. Object#&

Често се налага да се пишат изрази от рода на…

puts user.assignments.last.date

…където assignments, last и date могат да върнат nil. За да се избегне грешка по време на изпълнение се пише нещо от рода на:

puts user.assignments && user.assignments.last && user.assignments.last.date

Така накрая имаме nil ако някоя от функциите във веригата ни върне nil. Една идея е това да се постига със следния код:

puts user.&.assignments.&.last.&.date

Мислете за foo.&.bar като “върни nil ако foo е nil или foo няма метод bar; иначе върни foo.bar”. Имплементирайте Object#& така, че горният код да работи по посочения начин.

Обърнете внимание, че числа, булеви стойсти и nil имат поведение за &:

true & false == false
true & true == true
nil & true == false
0b0110 & 0b0011 == 0b0010

Предефинирайки &, направете го да работи за числа, булеви стойности и nil, но да запазва вече дефинираното от Ruby поведение.

Може да огледате кои други вградени класове дефинират & и там да направите същото.

4. Class#around_method

Xerox са измислили нещо, наречено аспектно-ориентирано програмиране. Една от идеите се нарича “around joint point” и ви позволява да обградите извикването на метод с някакъв код и дори да избирате дали методът да бъде извикан.

Ето как с подобен механизъм ще направим Array#<< да обръща аргументите си до низове и да ги добавя в масива само ако са по-дълги от 10 символа и ако масивът не е препълнен (има повече от 20 елемента).

Array.around_method(:<<) do |obj, invoke, *args|
  invoke[args[0].to_s] if args[0].to_s.length > 10 and obj.size <= 20
end

around_method е метод на Class и взема аргумент (името на метода, който да
декорира и блок. Блокът приема променлив брой аргументи, като

  • Първият (obj) е обекта, върху който е извикан метода.
  • Вторият (invoke) е Proc, lambda или нещо с оператор [], което да извиква оригиналния метод. Блокът на around_method не е длъжен да извика invoke. Може да го извика и повече от веднъж.
  • Останалите са аргументите, предадени на метода.

Например:

Array.around_method(:insert) do |obj, invoke, *args|
  invoke[*args]
end

array = []
array.insert(0, :larodi)

# obj ще бъде array
# invoke[] ще извика оригиналния array.insert
# *args ще бъде [0, :larodi]

5. Object#swap_methods

Добавете метод на Object, който взема имена на два метода и ги разменя.

text = "Coltrane"
text.swap_methods(:upcase, :downcase)
text.downcase == "COLTRANE" # true
text.upcase == "coltrane" # true

След извикване на swap_methods, всички други методи трябва да си останат както са. swap_methods не трябва да променя никакви други методи, освен подадените два. Също така, не трябва да добавя нови методи.

6. Enumerable#map_hash

Добавете метод Enumerable#map_hash, който да работи така:

["Coltrane", "Vedder", "Evans"].map_hash { |name| [name, name.length] }
# Връща {"Coltrane" => 8, "Vedder" => 6, "Evans" => 5}

["1;2", "3;4", "1;5"].map_hash { |str| str.split(';') }
# Връща {'1' => '5', '3' => '4'}

Блокът, предаден на map_hash, трябва да връща списък с два елемента, където първият е ключ, а вторият е стойност. Самият map_hash връща хеш от ключове и стойности, като ако блокът върне последователно два еднакви ключа, се пази само последната стойност.

7. Proxy.new

Създайте клас Proxy. Той трябва да работи така:

text = Proxy.new(" John Coltrane ")
text.length         # 15
text.strip          # "John Coltrane"
text.slice(6, 3)    # "Col"

Всеки обект прокси изпраща методите, които е получил, към обекта, с който е създаден и връща съответния резултат.

Наследници на Proxy могат да предефинират това поведение с метода around. Така ще направим прокси, което обръща всички аргументи до низове с inspect, преди да ги препрати и връща резултата като низ, отново получен с inspect върху оригиналния резултат:

class Inspector < Proxy
  def around(name, *args)
    puts "Just about to call #{name}"
    result = yield(*args.map(&:inspect))
    puts "#{name} just exited"
    result.inspect
  end
end

list = []
proxied = Inspector.new(list)
proxied << 1729   # Като list << "1729"
proxied << []     # Като list << "[]"
proxied << "foo"  # Като list << '"foo"'

proxied.reverse   # Връща '["\\"foo\\"", "[]", "1729"]'

Всяко прокси изпраща методите, които е получило, към обекта, с който е конструирано. Ако наследник на Proxy е предефинирал around, той трябва да може да работи така:

  • Получава името и аргументите на метода, който е извикан върху проксито
  • Може да извика оригиналния метод с yield (и да си избере какви аргументи да му подаде)
  • Връща резултат, който да бъде върнат от метода.

Така например:

list = [10, 11, 12, 13]
proxy = Proxy.new(list)

def proxy.around(name, *args)
  yield(args[0] + 1) * -1
end

proxy.index(11)
# 1. Извиква proxy.around(:index, 11)
# 2. yield-ът вътре извиква list.index(12), който връща 2
# 3. proxy.around връща -2
# 4. proxy.index(11) връща върната стойност от around, която е -2

8. Array#<, Array#>, Array#<=, Array#>=

Предефинирайте сравнението на масиви, така че да работи лексикографически по елементите на масива:

[1, 2, 3] < [1, 2, 4]
[1, 2, 3] > [1, 2, 2]
[1, 2] < [1, 2, 3]
[2, 1] > [1, 1, 1]
['a', 'b', 'c'] > ['a', 'b', 'b']

Алгоритъмът е следния:

  1. Сравнявате a[0] <=> b[0]
    • При резултат <0 имате a < b
    • При резултат >0 имате a > b
  2. В противен случай продължавате към a[1] и b[1] и т.н.
    • Ако двата масива са еднакво дълги и <=> върне 0 за всяка двойка стойности, то масивите са равни.
    • Ако по-дългият масив съдържа по-късия в началото си, то късият е по-малкия.

9. Hash#multi_invert

Добавете метод на Hash, който да “обръща” хеша. На всяка стойност от стария хеш съпоставя масив от всички ключове които са сочили към нея (или равна на нея). Така например:

{"Coltrane" => "John", "Evans" => "Bill", "Gates" => "Bill"}.multi_invert
# Връща { "John" => ["Coltrane"], "Bill" => ["Evans", "Gates"] }

{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 1}.multi_invert
# Връща {1 => ['a', 'd'], 2 => ['b'], 3 => ['c']}

Редът на старите ключове в масива няма значение.

10. Object#expose_all

Добавете метод expose_all, който да създава accessor-методи за всички instance-променливи на обекта, върху който е извикан.

class Thing
  def initialize
    @foo = 1
    @bar = 2
  end
end

thing = Thing.new
thing.foo       # Дава грешка - няма такъв метод.
thing.bar       # Отново грешка.

thing.expose_all
thing.foo       # Връща 1
thing.bar       # Връща 2

new_thing = Thing.new

new_thing.foo   # Грешка - expose_all работи за конкретни обекти, не за целия клас.

Друго

  • Тази задача е по-трудоемка. Ще дава малко повече точки.
  • Чувствайте се свободни да ползвате решенията си на една от точките в другите. Може да преизползвате код между десетте хака.
  • Тема на форумите за въпроси
  • Примерен тест
  • Бланка – може да ползвате този код като основа на вашето решение. Не е задължително.