С извинения за закъснението ви поднасяме деветото предизвикателство. Срокът е до неделя по обяд, тоест до неделя сутринта, понеже ще идвате с нас на Витоша. :-)
Девето предизвикателство
Можем ли безопасно да предположим, че пак няма да се тества с методи на
BasicObject
?@Никола, няма да тестваме с тези методи:
:==
,:equal?
,:!
,:!=
,:instance_eval
,:instance_exec
,:__send__
,:__id__
Ъм..
require './spec_helper.rb'
? Нещо аз ли пропускам? :)Трябва ли да работи и за деструктивни методи? Тоест:
s = Memoizer.new 'Foo'
s.chop! # 'Fo'
s.chop! # 'F'
@Георги Гърдев, мерси, че ни обърна внимание. Премахната е тази
require
клауза, беше забравена.@Георги Шопов, да. Може и да не е семантично правилно принципно, но това е умишлено, за да е проста спецификацията. Следвайте я.
Нека имаме следния сценарий:
- създаваме инстанцията
s = Memoizer.new 'foo'
- кешираме това извикване
s.capitalize # 'Foo'
- променяме обекта
s.chop! # 'Fo'
- връщаме го в изходно положение
s[0] = 'f'
s += 'o'
- сега правим
s.capitalize
- взимаме стойността от кеша или не?
- създаваме инстанцията
@Георги:
Взимате кешираната стойност. Не се занимаваме с инвалидация на кеш.
@Пламен това означава ли, че
s = Memoizer.new 'Foo'
s.chop! # 'Fo'
s.chop! # 'Fo'
, или аз не разбирам какво е инвалидация на кеш? Тоест като променя самия обект с деструктивен метод да не чистя кеша?@Георги Шопов, да, означава точно това. Пак казвам, гледайте спецификацията – там пише, че като се вика един и същ метод с едни и същи аргументи, то той се вика само веднъж и последващи извиквания са кеширани. Никъде не се говори за деструктивни методи или чистене на кеш, значи не трябва да правите такива неща.
И още едно допълнение:
s += 'o'
След изпълнението на този код,
s
вече не е инстанция наMemoizer
. Пимисли защо и експериментирай малко в конзолата, за да се убедиш, че е така.@Георги, не би трябвало да чистите кеша в никакъв случай. Да припомня от една от първите лекции (не мога да възпроизведа точен цитат, но беше нещо от сорта):
Двата най-трудни проблема в програмирането са именоването на променливи, инвалидацията на кеш и off-by-one грешките.
:-)
Няма ли да има половин точка ако един тест само не е минал?
Бъдещи предизвикателства дали е възможно да се обявяват със срок, който да включва поне един цял делничен ден. В последните две лекции се твърдеше, че предстои трета задача в сряда (13.11.2013), а вместо това в петък в почти 6 вечерта изниква предизвикателство със срок до неделя на обед. При този ден и час аз нито съм разбрал, че има предизвикателство, нито имах шанс да се включа. За мен делниците приключват в петък в пет часа, при някои колеги дори в четири часа. От този час не проверявам какъвто и да е канал за информация, който би ме ангажирал и за уийкенда.
В първия пост пише, че имало забавяне, което по моему можеше да се удължи с още 2-3 дни и да се обяви в неделя след обед или в понеделник през деня.
Забелязах следното - в някои решение като ключ за хеша се ползва само името на метода без аргументите му. Така при викане на един и същи метод с различни аргументи се връща един и същи резултат. Въпреки това при някои от тези решения всички тестове минават. Има ли пропуск в тестовете? Ако не - откъде се получава това разминаване?
P.S. Моля, ако е ок, публикувайте тестовете за това, а и за останалите минали предизвикателства и задачи. Благодаря.
Имам предложение към всички - искате ли като мине дадено предизвикателство / задача, заедно да го обсъждаме и да отбелязваме:
- най-често срещаните грешки;
- основните моменти в даденото предизвикателство (т.е. нещата, за които е трябвало да се сетим тип "трябва да наследява от BasicObject", "понеже BasicObject не включва Kernel, трябва да му викаме методите с
::Kernel.raise
" и всякакви особености от този род); - особено оригинални решения;
Според мен така ще извиличаме по-голяма полза от домашните си. Също така въпрос към екипа - има ли евнтуално възможност за включване на коментари под решенията на различните участници, т.е. система за peer-review?
И така, след като разгледах решенията, ето някои неща, които забелязах:
Най-често срещани грешки:
- създава се клас Error, който наследява от NoMethodError, без това да се изисква в условието;
- дава се достъп до четене на кешта (
attr_reader :calls
), без това да се изисква в условито; -
не се наследява от класа BasicObject, което води до грешка в този тест:
1) Memoizer returns the actual class of the target object Failure/Error: Memoizer.new(Object.new).class.should eq Object expected: Object got: Memoizer
методи, които връщат
nil
илиfalse
, не се кешират / викат се повторно.
Особено ми хареса решението на Георги Кръстев:
class Memoizer < BasicObject def initialize(target) @target = target @cache = ::Hash.new do |cache, message| cache[message] = @target.public_send(*message) end end private def method_missing(name, *args, &block) block ? @target.public_send(name, *args, &block) : @cache[[name, *args]] end end
по две причини:
- Използва това свойство на метода
Hash#new
(от Ruby doc):
If this hash is subsequently accessed by a key that doesn’t correspond to a hash entry, the value returned depends on the style of new used to create the hash.(...) If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block’s responsibility to store the value in the hash if required.
- не raise-ва експлицитно NoMethodError, защото ако обектът не отговаря на даден метод, той така и така ще raise-не тази грешка.
По време на разходката в планината стана въпрос за това, че ако ключа на хеша е стринг и някой от аргументите е някакъв по-особен обект не се знае дали ще работи както трябва. Това ме накара да се замисля.
Е ще работи, тъй като ще се използва to_s на Object и в ключа ще имаме нещо такова - "#<Foo:0x0000000211e200>".
@Валентин:
Няма ли да има половин точка ако един тест само не е минал?
Не, всичко или нищо, това е положението с предизвикателствата.
@Димитър:
Бъдещи предизвикателства дали е възможно да се обявяват със срок, който да включва поне един цял делничен ден.
По принцип искахме да поддържаме темпо, при което предизвикателства да се дават в понеделник вечер и четвъртък вечер. Ще си вземем бележка и ще се опитаме ако до четвъртък вечер нямаме готовност за предизвикателство, да го оставяме за понеделник, например. Бездруго бързането да се състави задачка и тестове водят до куп пропуски, например...
@Александър:
Има ли пропуск в тестовете?
Така като гледам, май има... При първа възможност това ще бъде коригирано. Колкото до коментарите под предизвикателствата, евентуално е възможно, но на практика трябва някой да седне да го имплементира, т.е. за момента няма как. А публикуването на пълните тестове по всички предизвикателства ми е в TODO-списъка от седмица насам. Имай вяра и търпение. :-)
@Александър, съгласен съм с идентификацията на най-честите грешки. Като цяло решенията на предизвикателствата се свеждат до правилно разчитане на спецификациите и писането на адекватни тестове. Следователно би било полезно да обсъждаме
писанетоизмислянето на тестове. @Росен, добавих нещичко към моите тестове с цел да счупя hash-венето презto_s
, но нямам такава имплементация за да мета-тествам.describe "Memoizer" do class MyString < String attr_reader :calls_to def init_logging @calls_to = [] end def * other @calls_to << :* super other end def getbyte other @calls_to << :getbyte super other end def to_s "" end private def violated @calls_to << :violated "privacy has been violated" end end it "calls the methods of the target class" do memoizer = Memoizer.new "Foo" memoizer.*(3).should eq "FooFooFoo" end it "uses BasicObject" do memoizer = Memoizer.new "Foo" memoizer.class.should eq String end it "returns results from cache instead of calls" do mystring = MyString.new "Foo" mystring.init_logging memoizer = Memoizer.new mystring a = memoizer.*(3) b = memoizer.*(3) a.should eq b mystring.calls_to.should eq mystring.calls_to.uniq end it "throws NoMethodError for missing methods, and nothing is cached" do mystring = MyString.new "Foo" mystring.init_logging memoizer = Memoizer.new mystring expect { memoizer.no_such_method }.to raise_error NoMethodError mystring.calls_to.size.should eq 0 end it "Memoizer doesn't call private methods, and nothing is cached" do mystring = MyString.new "Foo" mystring.init_logging memoizer = Memoizer.new mystring expect { memoizer.violated }.to raise_error NoMethodError mystring.calls_to.size.should eq 0 end it "calls to the same method called with different arguments are cached separately" do mystring = MyString.new "Foo" mystring.init_logging memoizer = Memoizer.new mystring memoizer.*(2) memoizer.*(3) mystring.calls_to.size.should eq 2 memoizer.*(2) memoizer.*(3) mystring.calls_to.size.should eq 2 end it "doesn't mix up different methods called with the same arguments" do mystring = MyString.new "Foo" mystring.init_logging memoizer = Memoizer.new mystring memoizer.*(3) memoizer.getbyte(3) mystring.calls_to.size.should eq 2 end end
Осъзнавам, че това което съм написал е глупост, защото join не използва to_s.
@Димитър Бонев, съжалявам, че си пропуснал предизвикателството, понеже сме го дали в петък вечер и политиката ти е да не си проверяваш нищо след 17 ч. :) Хубаво правиш, че се откъсваш от екрана от време на време, надявам се да си изкарал хубав уикенд :)
За други нещастници като мен, делниците не са (не биха били) опция за решаване на домашни и само вечери, нощи и уикенди стават. Така че -- различни хора, различни схеми. Дефиницията на предизвикателствата е сходна до блицкриг -- даваме ги изневиделица (макар че сме всъщност по-меки и винаги предупреждаваме), даваме срок 24 часа (макар че винаги е повече) и, Вальо, понеже са предизвикателства, ако имате дори един грешен тест, не взимате точка.
Както Пламен отбеляза, бележка си взимаме (винаги), но това не означава, че при определени обстоятелства, везните няма да натежат така, че пак да дадем предизивикателство по същия начин, със същия срок. Допълнителна причина е и че не е фатално, ако пропуснете някое друго, ако сте възпрепятствани по някаква причина (било то и доброволен избор да не гледате нищо онлайн). Та тъй.
@Сашо, аз съм качил пълния тест и нашето решение на това предизвикателство, а Пламен се е погрижил да качи тези неща за всички по-стари предизвикателства. Разгледай ги.
А идеята за обсъждане на решениятае чудесна. Темата на дадена задача/предизвикателство е много подходяща за случая. А по-интересните неща ще ги споменаваме и на лекции.
Ще обсъдим темата за ключовете-низове, например :) Ето два примера, съставени от другия Сашо, които го чупят това:
@Димитър Димитров, както се изразихте "различни хора, различни схеми", но с комбинацията от този срок и начина на обявяване на предизвикателствата вие фаворизирате една конкретна група от хора. Въпросното предизвикателство е с най-нисък брой предадени решения. Имайки предвид, че решението му има сходни точки с предшестващото го предизвикателство, аз отдавам ниското участие именно на спомената "комбинация на смъртта", която е погубила няколко участника. :) Това дали е така, може би ще се потвърди, ако следващото предизвикателство има повече или равен брой предадени решения, защото иначе възможно е да е резултат от тенденция на намаляване с всяко изминало предизвикателство.
В тази връзка, предлага ли се автоматизирана форма на нотификация на курсистите относно обявяването на старт на предизвикателство, например по имейл?
@Димитър Бонев, идеята за известия по имейл е готина, но трябва някой да я имплементира :) Ако имаш желание, даваме бонус точки и помагаме – научава се много... :)
@Димитър Димитров, желание имам. Остава да се уточнят детайлите, попълних си скайп полето на профила, обикновено вечер след 7 съм на линия, също опция е чрез e-mail или друга форма на комуникация по твое предпочитание.
Пингни ме след лекцията идната сряда, за да поговорим малко на живо за това. После ще продължим дискусията онлайн.
След дългото забавяне е време да поизтупаме темата от праха и да добавим още няколко коментара. Този път анонимността на авторите е (почти) гарантирана :-)
Ето ги и моите бележки:
По-тривиалните неща:
- Все още има решения с гадна идентация и лош whitespace.
- Не махате от решенията си закоментиран код (известно още като scar tissue
code), което е ненужно в един свят, в който има системи за контрол на
версиите като
git
. - Имаше и забравен
puts
, за което бяхме направили забележка, (все пак получихме извинение от автора по e-mail :-)). - Не си пускате примерните тестове. За това предизвикателство имаше само един
тест, но пускането му гарантира, че няма да си кръстите класа
Memorizer
. Да се правят typo-та е нормално и приемливо, но все пак има много прости „лекарства“ срещу оставянето на грешни имена в кода. Пускането на тестовете е един от тях.
Това са все дреболии, но именно защото са дреболии трябва да ви е лесно да се отучите от тях.
Дребни стилови забележки:
-
Hash#has_key?
е deprecated. Вижте това. Както пише в публикацията, ползвайтеHash#key?
. - В едно от решенията имаше
unless
с нетривиално (да се чете „има поне единand
илиor
“) условие, което е непрепоръчително.
Това не са непременно грешки, но все пак трябва да следите и стила си.
По самата задача:
- Както @Александър вече се включи, не се наследява от
BasicObject
, което трябваше да се направи, особено след предното предизвикателство. - Също така, не е удачно да си „замърсявате“ публичният интерфейс на
Memoizer
, било като направитеattr_reader
към кеша, било като си направите помощни методи и ги оставитеpublic
. - Имаше решение с дефиниран
Cache
клас, което в този случай може да си приеме за overkill, още повече, че дефиницията му беше оставена отвън и заемаше иметоCache
. Това е по-скоро въпрос на вкус, но все пак смятам, че си струва отбелязването. - По интересно е друго, помощен метод, наречен
call
, който обаче освен това пипа по кеша. Проблемно е, понеже страничният ефект не си личи по името и човек може да остане заблуден. Ако методътcall
правеше само едно нещо, като това нещо е просто извикване на подаден метод щеше да е по-добре. Сами можете да се сетите дали преименуването на метода отcall
наeither_call_method_and_cache_result_or_get_result_from_cache
поправя нещата. :-) Разбира се тук има поле за спор, например дали трябва да има и друг помощен метод (тоест един метод за манипулация на кеша и един за извикването на методи), дали можем да набутаме всичко вmethod_missing
, или нещо съвсем друго.
Трябва да сте влезли в системата, за да може да отговаряте на теми.