09. Call stack. Изключения, част 2. throw и catch. Fibers

09. Call stack. Изключения, част 2. throw и catch. Fibers

09. Call stack. Изключения, част 2. throw и catch. Fibers

9 ноември 2015

Днес

План за следващите лекции

Планина

Новини за работни позиции с Ruby

Въпрос 1

Каква е разликата между require './foo' и require_relative 'foo'?

require './foo'        # Релативно спрямо Dir.pwd
require_relative 'foo' # Релативно спрямо __FILE__

"Текущата директория" на процеса може да се променя както при стартиране, така и по време на изпълнение на програмата (с Dir.chdir) и е различна от директорията, в която се намира Ruby файла, изпълняващ require метода:

cd /home
ruby /foo/bar/baz.rb # Dir.pwd ще е: /home

Въпрос 2

Каква е разликата между require и load?

require изпълнява зареждания файл само веднъж, без значение колко пъти сме извикали require, докато load го изпълнява при всяко изикване на load.

Въпрос 3

Кой е родителският клас на класа StandardError?

Exception.

Въпрос 4

Кой клас стои най-горе в йерархията на всички изключения (като не броим Object)?

Exception.

Въпрос 5

От какъв тип са изключенията, хвърлени така: raise 'foo'?

RuntimeError.

Въпрос 6

Как ще се оцени следният израз?

begin
  raise 'oh noes!'
rescue Exception
  'A general exception has occurred.'
rescue RuntimeError
  'A standard error has occurred.'
end

На низа "A general exception has occurred.". Изключението се хваща от първия rescue, който споменава клас exception_class, за който е вярно, че exception.is_a?(exception_class).

puts с много аргументи

лирическо отклонение

Call stack

Достъп до call стека

Пример за call stack

# inception.rb:
def roll_the_ball()         go_deep               end
def go_deep()               we_need_to_go_deeper  end
def we_need_to_go_deeper()  even_deeper_than_that end
def even_deeper_than_that() puts(caller)          end

roll_the_ball

Изпълняваме го с ruby inception.rb.

Пример за call stack - резултат

Примерът от предния слайд ще продуцира:

inception.rb:3:in `we_need_to_go_deeper'
inception.rb:2:in `go_deep'
inception.rb:1:in `roll_the_ball'
inception.rb:6:in `<main>' 

Call стекът обикновено е по-дълбок

(съкратен) пример от irb

> puts caller
.../irb/workspace.rb:86:in `eval'
.../irb/workspace.rb:86:in `evaluate'
.../irb/context.rb:380:in `evaluate'
.../irb.rb:492:in `block (2 levels) in eval_input'
.../irb.rb:624:in `signal_status'
.../irb.rb:489:in `block in eval_input'
.../irb/ruby-lex.rb:247:in `block (2 levels) in each_top_level_statement'
.../irb/ruby-lex.rb:233:in `loop'
.../irb/ruby-lex.rb:233:in `block in each_top_level_statement'
.../irb/ruby-lex.rb:232:in `catch'
.../irb/ruby-lex.rb:232:in `each_top_level_statement'
.../irb.rb:488:in `eval_input'
.../irb.rb:397:in `block in start'
.../irb.rb:396:in `catch'
.../irb.rb:396:in `start'
.../bin/irb:11:in `
' => nil

Call stacks и backtraces

Изключения като цяло

Основни атрибути

Изключенията в Ruby са обекти като всичко останало – инстанции на клас Exception или негов наследник.

Имат три основни атрибута:

Основни атрибути (2)

Нека имаме инстанция на изключение в променлива error. Тогава:

Някои вградени изключения

foo               # NameError: undefined local variable or method `foo' for main:Object
1 / 0             # ZeroDivisionError: divided by 1
File.open         # ArgumentError: wrong number of arguments (0 for 1..3)
File.open('/Ah?') # Errno::ENOENT: No such file or directory @ rb_sysopen - /Ah?

Между другото, Errno::ENOENT си е нормално изключение:

Errno::ENOENT.ancestors.take_while { |kind| kind != Object }
# => [Errno::ENOENT, SystemCallError, StandardError, Exception]

Наши собствени изключения

За да си направим клас-изключение, обикновено наследяваме от RuntimeError или StandardError:

class NoFriendsError < StandardError
end

Как да ползваме изключения

Може да разделим изключенията на два основни вида.

  1. Непредвидими грешки, причинени от "околната среда".
  2. Програмистки грешки, причинени от неправилна употреба на парче код.

Непредвидими грешки

Програмистки грешки

Какво да хващаме?

Изключения в библиотеки

It is recommended that a library should have one subclass of StandardError or RuntimeError and have specific exception types inherit from it. This allows the user to rescue a generic exception type to catch all exceptions the library may raise even if future versions of the library add new exception subclasses.

Изключения в библиотеки (2)

Въпроси?

Въпроси дотук?

catch и throw

catch и throw - общ вид

def go_deep
  go_deeper
end

def go_deeper
  throw :some_label, 'the value'
  raise 'This line WILL NOT execute.'
end

return_value = catch :some_label do
  go_deep
end

return_value == 'the value' # => true

catch и throw

накратко

catch и throw

edge cases

catch(:baba) do
  throw :baba, 'Penka'
  'Ginka'
end                       # => "Penka"

catch(:baba) { 'Ginka' }  # => "Ginka"

throw :baba, 'Penka'      # error: UncaughtThrowError

catch и throw

реален пример

Взет от Vagrant:

found = catch(:done) do
  [modified, added, removed].each do |changed|
    changed.each do |listenpath|
      throw :done, true if listenpath.start_with?(hostpath)
    end
  end

  # Make sure to return false if all else fails so that we
  # don't sync to this machine.
  false
end

catch и throw

реален пример - дали?

Предния пример аз бих го направил така:

found =
  [modified, added, removed].any? do |changed_paths|
    changed_paths.any? { |path| path.start_with? host_path }
  end

Или дори така:

changed_paths = [modified, added, removed].flatten

found = changed_paths.any? { |path| path.start_with? host_path }

catch и throw

(по-)реален пример

Взет от Discourse (и леко модифициран):

def get_excerpt(html)
  parser = Nokogiri::HTML::SAX::Parser.new(self)

  catch(:found_excerpt) do
    parser.parse(html)
  end
end

def some_parser_hooks
  # Make checks and gather info...
  # Found our thing!
  excerpt = 'something'
  throw :found_excerpt, excerpt
end

Въпроси по catch и throw?

Следва кратка вметка за методи и след това Fibers.

Всички възможни аргументи на метод

лирическо отклонение с елементи на преговор

def an_example_of_great_method_design(
  a,
  _,
  _,
  b,
  c = :something,
  *splat,
  d,
  (e, f),
  keyword:,
  with_default: 'value',
  **other_keyword_args,
  &some_block
)
end

Всички възможни аргументи на метод (2)

Sandi Metz

Като споменахме Санди...

I'm the author of Practical Object-Oriented Design in Ruby (POODR). I believe in simple code and straightforward explanations. I want to help you transform your code and make the pain go away.

Генератор на Fibonacci

Кодът по-долу няма да приключи никога:

class FibonacciNumbers
  def each
    current, previous = 1, 0

    while true
      yield current
      current, previous = current + previous, current
    end
  end
end

FibonacciNumbers.new.each { |number| puts number }

Генератор на Fibonacci с енумератори

Кодът по-долу ще работи:

class FibonacciNumbers
  def each
    current, previous = 1, 0

    while true
      yield current
      current, previous = current + previous, current
    end
  end
end

FibonacciNumbers.new.to_enum.take(5) # => [1, 1, 2, 3, 5]

Fibers

Fibers

Най-простият възможен пример:

fiber = Fiber.new { :larodi }

fiber.resume # => :larodi
fiber.resume # => error: FiberError

Генератор на Fibonacci с Fiber

Обратно на нашия пример от преди малко:

class FibonacciNumbers
  def each
    current, previous = 1, 0

    while true
      yield current
      current, previous = current + previous, current
    end
  end
end

Fibonacci с fibers

Заменяйки yield с Fiber.yield, правим безкраен поток:

class FibonacciNumbers
  def each
    current, previous = 1, 0

    while true
      Fiber.yield current
      current, previous = current + previous, current
    end
  end
end

fibonacci_stream = Fiber.new { FibonacciNumbers.new.each }

fibonacci_stream.resume # => 1
fibonacci_stream.resume # => 1
fibonacci_stream.resume # => 2
fibonacci_stream.resume # => 3

Приложение на fibers

Enumerator-и и fibers

Enumerator класът в Ruby се възползва от Fiber.

Това се случва, когато направите (1..100_000).each.

Въпроси