In Progress
Unit 1, Lesson 1
In Progress

Not Implemented

Video transcript & code

In the last episode we came up with a LogicalCondition class. In order to use it, we have to subclass it and provide a condition_holds? predicate.

require "thread"
module Tapas
  class Condition
    def initialize(lock)
      @lock = lock
      @cv   = ConditionVariable.new
    end

    def wait(timeout=nil)
      @cv.wait(@lock.mutex, timeout)
    end

    def signal
      @cv.signal
    end
  end

  class Lock
    attr_reader :mutex

    def initialize
      @mutex = Mutex.new
    end

    def synchronize(&block)
      @mutex.synchronize(&block)
    end
  end

  class LogicalCondition
    def initialize(client=nil, lock=Lock.new, condition=Condition.new(lock))
      @client    = client
      @lock      = lock
      @condition = condition
    end

    def signal
      condition.signal
    end

    def wait(timeout=:never, timeout_policy=->{nil})
      deadline = timeout == :never ? :never : Time.now + timeout
      @lock.synchronize do
        loop do
          cv_timeout = timeout == :never ? nil : deadline - Time.now
          if !condition_holds? && cv_timeout.to_f >= 0
            condition.wait(cv_timeout)
          end
          if condition_holds?
            return yield
          elsif deadline == :never || deadline > Time.now
            next
          else
            return timeout_policy.call
          end
        end
      end
    end

    private

    attr_reader :client, :condition
  end
end

Here are the two subclasses we derived in that episode. Each overrides condition_holds? to check for a different state of a Queue object.

class SpaceAvailableCondition < LogicalCondition
  private
  def condition_holds?
    !client.full?
  end
end

class ItemAvailableCondition < LogicalCondition
  private
  def condition_holds?
    !client.empty?
  end
end

LogicalCondition is an abstract class: that is, a class that must be subclassed in order to be used. In some languages, such classes are explicitly denoted by an explicit keyword. In Ruby there is no such annotation. The class is implicitly abstract by dint of the fact that if we use it without subclassing, it won't work.

For instance, if we simply instantiate an instance and try to wait on it, we get a NoMethodError complaining about the missing predicate.

require "./logical_condition"
cond = Tapas::LogicalCondition.new
cond.wait
# ~> /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:47:in `block (2 levels) in wait': undefined method `condition_holds?' for #<Tapas::LogicalCondition:0x00000001083a58> (NoMethodError)
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:45:in `loop'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:45:in `block in wait'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:27:in `synchronize'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:27:in `synchronize'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:44:in `wait'
# ~>    from -:3:in `<main>'

So how do other programmers know how to use this abstract class? Well, ideally, we'd provide some documentation explaining it. But there's a way we can make their lives a little easier in the code itself.

We can provide a stub implementation of the missing method. Inside this stub, we raise an exception. But not just any exception. We raise NotImplementedError, which is specifically intended for this scenario. NotImplementedError inherits from ScriptError, which in turn inherits directly from Exception. Since it does not inherit from StandardError, ordinary rescue clauses will not capture a NotImplementedError. This is good, because NotImplementedError indicates a coding defect in the program, and so it shouldn't be handled like a runtime error.

NotImplementedError.ancestors
# => [NotImplementedError,
#     ScriptError,
#     Exception,
#     Object,
#     PP::ObjectMixin,
#     Kernel,
#     BasicObject]
begin
  raise NotImplementedError
rescue
  puts "Rescued error"
end
# ~> -:2:in `<main>': NotImplementedError (NotImplementedError)

We include an informative error message with the exception.

def condition_holds?
  raise NotImplementedError, 
        "You must implement a #condition_holds? predicate that "
        "tests whether the condition is currently true"
end

Now if we try to use a LogicalCondition without subclassing and overriding #predicate_holds?, we get a helpful message telling us what we did wrong. But using this technique, users of the class can also read through the class source code and easily see the methods which are required but not provided.

require "./logical_condition2"
cond = Tapas::LogicalCondition.new
cond.wait
# ~> /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:62:in `condition_holds?': You must implement a #condition_holds? predicate that  (NotImplementedError)
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:47:in `block (2 levels) in wait'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:45:in `loop'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:45:in `block in wait'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:27:in `synchronize'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:27:in `synchronize'
# ~>    from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:44:in `wait'
# ~>    from -:3:in `<main>'

There are a number of OO patterns where an abstract class or module depends on methods that client code provides. One way to be kind to users of your code is to include "executable documentation" of these abstract methods using stubs that raise NotImplementedError.

And that's it for today. Happy hacking!

Responses