Девето предизвикателство

  1. @Георги Гърдев, мерси, че ни обърна внимание. Премахната е тази require клауза, беше забравена.

    @Георги Шопов, да. Може и да не е семантично правилно принципно, но това е умишлено, за да е проста спецификацията. Следвайте я.

  2. Нека имаме следния сценарий:

    • създаваме инстанцията s = Memoizer.new 'foo'
    • кешираме това извикване s.capitalize # 'Foo'
    • променяме обекта s.chop! # 'Fo'
    • връщаме го в изходно положение s[0] = 'f' s += 'o'
    • сега правим s.capitalize
    • взимаме стойността от кеша или не?
  3. @Георги Шопов, да, означава точно това. Пак казвам, гледайте спецификацията – там пише, че като се вика един и същ метод с едни и същи аргументи, то той се вика само веднъж и последващи извиквания са кеширани. Никъде не се говори за деструктивни методи или чистене на кеш, значи не трябва да правите такива неща.

    И още едно допълнение:

    s += 'o'
    

    След изпълнението на този код, s вече не е инстанция на Memoizer. Пимисли защо и експериментирай малко в конзолата, за да се убедиш, че е така.

  4. @Георги, не би трябвало да чистите кеша в никакъв случай. Да припомня от една от първите лекции (не мога да възпроизведа точен цитат, но беше нещо от сорта):

    Двата най-трудни проблема в програмирането са именоването на променливи, инвалидацията на кеш и off-by-one грешките.

    :-)

  5. Бъдещи предизвикателства дали е възможно да се обявяват със срок, който да включва поне един цял делничен ден. В последните две лекции се твърдеше, че предстои трета задача в сряда (13.11.2013), а вместо това в петък в почти 6 вечерта изниква предизвикателство със срок до неделя на обед. При този ден и час аз нито съм разбрал, че има предизвикателство, нито имах шанс да се включа. За мен делниците приключват в петък в пет часа, при някои колеги дори в четири часа. От този час не проверявам какъвто и да е канал за информация, който би ме ангажирал и за уийкенда.

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

  6. Забелязах следното - в някои решение като ключ за хеша се ползва само името на метода без аргументите му. Така при викане на един и същи метод с различни аргументи се връща един и същи резултат. Въпреки това при някои от тези решения всички тестове минават. Има ли пропуск в тестовете? Ако не - откъде се получава това разминаване?

    P.S. Моля, ако е ок, публикувайте тестовете за това, а и за останалите минали предизвикателства и задачи. Благодаря.

  7. Имам предложение към всички - искате ли като мине дадено предизвикателство / задача, заедно да го обсъждаме и да отбелязваме:

    • най-често срещаните грешки;
    • основните моменти в даденото предизвикателство (т.е. нещата, за които е трябвало да се сетим тип "трябва да наследява от 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-не тази грешка.
  8. По време на разходката в планината стана въпрос за това, че ако ключа на хеша е стринг и някой от аргументите е някакъв по-особен обект не се знае дали ще работи както трябва. Това ме накара да се замисля.

    Е ще работи, тъй като ще се използва to_s на Object и в ключа ще имаме нещо такова - "#<Foo:0x0000000211e200>".

  9. @Валентин:

    Няма ли да има половин точка ако един тест само не е минал?

    Не, всичко или нищо, това е положението с предизвикателствата.

    @Димитър:

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

    По принцип искахме да поддържаме темпо, при което предизвикателства да се дават в понеделник вечер и четвъртък вечер. Ще си вземем бележка и ще се опитаме ако до четвъртък вечер нямаме готовност за предизвикателство, да го оставяме за понеделник, например. Бездруго бързането да се състави задачка и тестове водят до куп пропуски, например...

    @Александър:

    Има ли пропуск в тестовете?

    Така като гледам, май има... При първа възможност това ще бъде коригирано. Колкото до коментарите под предизвикателствата, евентуално е възможно, но на практика трябва някой да седне да го имплементира, т.е. за момента няма как. А публикуването на пълните тестове по всички предизвикателства ми е в TODO-списъка от седмица насам. Имай вяра и търпение. :-)

  10. @Александър, съгласен съм с идентификацията на най-честите грешки. Като цяло решенията на предизвикателствата се свеждат до правилно разчитане на спецификациите и писането на адекватни тестове. Следователно би било полезно да обсъждаме писането измислянето на тестове. @Росен, добавих нещичко към моите тестове с цел да счупя 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
    
  11. @Димитър Бонев, съжалявам, че си пропуснал предизвикателството, понеже сме го дали в петък вечер и политиката ти е да не си проверяваш нищо след 17 ч. :) Хубаво правиш, че се откъсваш от екрана от време на време, надявам се да си изкарал хубав уикенд :)

    За други нещастници като мен, делниците не са (не биха били) опция за решаване на домашни и само вечери, нощи и уикенди стават. Така че -- различни хора, различни схеми. Дефиницията на предизвикателствата е сходна до блицкриг -- даваме ги изневиделица (макар че сме всъщност по-меки и винаги предупреждаваме), даваме срок 24 часа (макар че винаги е повече) и, Вальо, понеже са предизвикателства, ако имате дори един грешен тест, не взимате точка.

    Както Пламен отбеляза, бележка си взимаме (винаги), но това не означава, че при определени обстоятелства, везните няма да натежат така, че пак да дадем предизивикателство по същия начин, със същия срок. Допълнителна причина е и че не е фатално, ако пропуснете някое друго, ако сте възпрепятствани по някаква причина (било то и доброволен избор да не гледате нищо онлайн). Та тъй.

    @Сашо, аз съм качил пълния тест и нашето решение на това предизвикателство, а Пламен се е погрижил да качи тези неща за всички по-стари предизвикателства. Разгледай ги.

    А идеята за обсъждане на решениятае чудесна. Темата на дадена задача/предизвикателство е много подходяща за случая. А по-интересните неща ще ги споменаваме и на лекции.

    Ще обсъдим темата за ключовете-низове, например :) Ето два примера, съставени от другия Сашо, които го чупят това:

  12. @Димитър Димитров, както се изразихте "различни хора, различни схеми", но с комбинацията от този срок и начина на обявяване на предизвикателствата вие фаворизирате една конкретна група от хора. Въпросното предизвикателство е с най-нисък брой предадени решения. Имайки предвид, че решението му има сходни точки с предшестващото го предизвикателство, аз отдавам ниското участие именно на спомената "комбинация на смъртта", която е погубила няколко участника. :) Това дали е така, може би ще се потвърди, ако следващото предизвикателство има повече или равен брой предадени решения, защото иначе възможно е да е резултат от тенденция на намаляване с всяко изминало предизвикателство.

    В тази връзка, предлага ли се автоматизирана форма на нотификация на курсистите относно обявяването на старт на предизвикателство, например по имейл?

  13. @Димитър Димитров, желание имам. Остава да се уточнят детайлите, попълних си скайп полето на профила, обикновено вечер след 7 съм на линия, също опция е чрез e-mail или друга форма на комуникация по твое предпочитание.

  14. След дългото забавяне е време да поизтупаме темата от праха и да добавим още няколко коментара. Този път анонимността на авторите е (почти) гарантирана :-)

    Ето ги и моите бележки:

    По-тривиалните неща:

    • Все още има решения с гадна идентация и лош 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, или нещо съвсем друго.

Трябва да сте влезли в системата, за да може да отговаряте на теми.