2. Десет малки хакчета
Крайният срок вече изтече. Не може да пращате решения.
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']
Алгоритъмът е следния:
- Сравнявате
a[0] <=> b[0]- При резултат
<0иматеa < b - При резултат
>0иматеa > b
- При резултат
- В противен случай продължавате към
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 работи за конкретни обекти, не за целия клас.
Друго
- Тази задача е по-трудоемка. Ще дава малко повече точки.
- Чувствайте се свободни да ползвате решенията си на една от точките в другите. Може да преизползвате код между десетте хака.
- Тема на форумите за въпроси
- Примерен тест
- Бланка – може да ползвате този код като основа на вашето решение. Не е задължително.