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 работи за конкретни обекти, не за целия клас.
Друго
- Тази задача е по-трудоемка. Ще дава малко повече точки.
- Чувствайте се свободни да ползвате решенията си на една от точките в другите. Може да преизползвате код между десетте хака.
- Тема на форумите за въпроси
- Примерен тест
- Бланка – може да ползвате този код като основа на вашето решение. Не е задължително.