Advanced Ruby - Day 1

Let’s get started. I’m at the Advanced Ruby training. Here are the topics we gonna cover

  • Blocks, Procs, and Closures
  • Ruby 1.9
  • Your Own, Private Ruby
  • Design in a Dynamic Language
  • Messin’ with Types
  • …make it fast.
  • The Ruby Object Model
  • Metaprogramming
  • Making Domains-Specific Languages
  • Concurrency
  • Exotic Control Flow
  • Library Organization
  • Debugging and Profiling
  • JRuby
  • Ruby Extras
  • Distributed Programming
  • Onward and Upward!

In fact when I saw the topics that will be covered by Dave Thomas and Chad Fowler, and the fact that some of my collogues from Pinnacol where attending I had to join…Oh yea, it’s a 25 minute drive from home too.

So this morning I woke up and installed Ruby 1.9. I didn’t use multiruby as many do I just downloaded Ruby 1.9.1-p0 from http://www.ruby-lang.org/en/downloads. These are the steps to install:


cd ruby-1.9.1-p0
autoconf
./configure —prefix=/usr/local/ruby1.9
make
sudo make install

Then you can check the version by

 
$/usr/local/ruby1.9/bin/ruby -v 
ruby 1.9.1p0 (2009-01-30 revision 21907) [i386-darwin9.6.0]
$/usr/local/ruby1.9/bin/irb 
RUBY_VERSION 
=> "1.9.1"

I didn’t add /usr/local/ruby1.9/bin to my path, so the default ruby is still my mack default, 1.8.6 of my mac (ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0]).

Keep on reading to see some of the code we are walking through. This said it’s hard to convey how much value Chad and Dave are passing along with all their deep explanations of each of the examples…

Objectives

Deep
The “Why” not just the “How”
Explore Ruby’s Lesser-kown Corners
Pragmatic, not Dogmatic
Discussion for Highest Group Value

Blocks, Procs, and Closures

Blocks

one two {…} # binds to #two, and result of that is passed to one

one two do … end # block bind to #one, and two is passed to as argument.

Built-in support:

  • block_given?
  • loop
  • yield
  • break, redo, next, retry
def show_break
  puts "before yield"
  yield
  puts "after yield" # never make it here
end

puts "before call"
result = show_break do
  puts "before break"
  break 123
  puts "after break" # never make it here
end
puts "after call, result = #{result}"

The output of the above code is

before call
before yield
before break
after call, result = 123

Then Dave and Chad explain in details the next, redo, and retry keywords. Note retry is removed in 1.9.

Blocks Into Objects

def show_block_param(&block)
  puts block.inspect  # use the blokc
  [1,2,3].each &block # pass the block along
  puts block.call
  yield
end

show_block_param { puts "in block"}

Using lambda, proc, and Proc.new

  • 1.8: proc and lambda are synonyms
  • 1.9: proc and Proc.new are synonyms

So don’t use proc.
Lambda closer to method calling
Proc.new uses parallel assignment rules for parameter passing

Proc.new { |a,b| }.call(1,2,3) # OK
lambda { |a,b| }.call (1)  # ArgumentError: wrong number of args (1 for 2)

Dave goes onto the parameters and yield, followed by Return form blocks.

def meth(&block)
  block.call
end

b = Proc.new { return 99 }
puts meth(&b) # LocalJumpError: unexpected return

Bindings

Encapsulate the context at a point of execution

class A
  attr_writer :ivar
  def initialize
    @ivar = 123
  end
  def get_binding
    ivar = "ivar"
    binding
  end
end

a = A.new
bind = a.get_binding { "block value" }
eval "puts ivar", bind  # => ivar
eval "puts @ivar", bind # => 123

a.ivar = "new value"
eval "puts ivar", bind  # => ivar
eval "puts @ivar", bind # => new value

Closures

Blocks carry with them the binding in which they were created

def block_in_sandbox(param)
  ivar = "local variable"
  lambda do
    puts "param = #{param}"
    puts "ivar = #{ivar}"
  end
end

block = block_in_sandbox(99)
block.call

# param = 99
# ivar = local variable

Then we moved onto an exercise to create a counter with what we just learned:

require 'test/unit'
def counter(start=0, increment=1)
  start -= increment
  lambda do
    start += increment
  end
end

class CounterTest < Test::Unit::TestCase

    def test_counter
      result = counter(0,2)
      assert_equal 0, result.call
      assert_equal 2, result.call
      assert_equal 4, result.call
    end
    
    def test_another_one
      another_one = counter(10, 3)
      assert_equal 10, another_one.call
      assert_equal 13, another_one.call
      assert_equal 16, another_one.call
    end
end

# 2 tests, 6 assertions, 0 failures, 0 errors

Blocks, closures, and define_method

class Example
  def self.create_multiplier(name, factor)
    define_method(name) do |arg|
      arg * factor
    end
  end
end

Example.create_multiplier(:double, 2)
Example.create_multiplier(:triple, 3)

eg = Example.new
puts eg.double(33) # 66
puts eg.triple(33) # 99

Ruby Internals

Now we are going lower level than we want to…Looking at the source of Ruby:

  • Main interpreter in top-level source
  • Ruby libraries in lib/
  • C libraries in ext/
  • Architecture-sepcific stuff in bcc32/, win32/, wince/ and 68k/

Most important files parse.y and eval.c

If you install ruby like I listed at the start of this entry you have all the source files, I have mine under /Users/daniel/Downloads/ruby-1.9.1-p0. Opening in textmate it looks as follows

20090309_ruby_source.png

ParseTree shows the nodes

sudo gem install ParseTree
Successfully installed RubyInline-3.8.1
Successfully installed sexp_processor-3.0.1
Successfully installed ParseTree-3.0.3

require 'rubygems'
require 'parse_tree'

class Daniel
 def talk
    "salut"
 end
end

p = ParseTree.new
p.parse_tree(Daniel).to_yaml

--- 
- - :class
  - :Daniel
  - - :const
    - :Object
  - - :defn
    - :talk
    - - :scope
      - - :block
        - - :args
        - - :str
          - salut
=> nil
  1. Note, doesn’t work i 1.9
  2. In 1.9 can call the interpreter
  3. Also ripper library, gives the output of the parse.

parse.y

Chad now explain how the parser works. In the parser.y you’ll find exactly how the parsing works. It’s over 10000 lines for Ruby 1.9

if_tail		: opt_else
		| keyword_elsif expr_value then
		  compstmt
		  if_tail
		    {
		    /*%%%*/
			$$ = NEW_IF(cond($2), $4, $5);
			fixpos($$, $2);
		    /*%
			$$ = dispatch3(elsif, $2, $4, escape_Qundef($5));
		    %*/
		    }
		;

expr_value	: expr
		    {
		    /*%%%*/
			value_expr($1);
			$$ = $1;
		        if (!$$) $$ = NEW_NIL();
		    /*%
			$$ = $1;
		    %*/
		    }
		;

Naming Conventions

  • NODE_XX type of a node
  • rb_cXxxx class object for Ruby clas Xxx
  • rb_mXxxx module object for Ruby module xxxx
  • rb_xxx C-implementation of a Ruby method
  • Qxxx immediate value of Qnil
  • RXXXXXX

Object Representation

Look in ruby.h for definition

To find Classes and Modules you can search in the c files:

$ grep String *.c | grep rb_define_class
string.c:    rb_cString  = rb_define_class("String", rb_cObject);

If we look in string.c we see how the methods of the String Ruby class are defined in c.

    
void
Init_String(void)
{
#undef rb_intern
#define rb_intern(str) rb_intern_const(str)
    rb_cString  = rb_define_class("String", rb_cObject);
    rb_include_module(rb_cString, rb_mComparable);
    rb_define_alloc_func(rb_cString, str_alloc);
    rb_define_singleton_method(rb_cString, "try_convert", rb_str_s_try_convert, 1);
    rb_define_method(rb_cString, "initialize", rb_str_init, -1);
    rb_define_method(rb_cString, "initialize_copy", rb_str_replace, 1);
    rb_define_method(rb_cString, "<=>", rb_str_cmp_m, 1);
    rb_define_method(rb_cString, "==", rb_str_equal, 1);
    rb_define_method(rb_cString, "eql?", rb_str_eql, 1);
    rb_define_method(rb_cString, "hash", rb_str_hash_m, 0);
    rb_define_method(rb_cString, "casecmp", rb_str_casecmp, 1);
    rb_define_method(rb_cString, "+", rb_str_plus, 1);
...

Writing Extensions

Why?

  • Interface to external libraries
  • Performance
  • Accessing internal
  • Showing off

C-api is really nice.

Recipe is: create new directory, create source .c file. Create extconf.rb script and run it (requires ‘mkmf’). make. make install…It just all works.

Practice in your own user-local Ruby copy.

Example wrapping the Tidy library.

And onto lunch and exercise…Back at 1:15pm

  • How many build-in classes define a to_i method?
  • What’s the difference between Symbol#to_i and Symbol#to_int?

Ruby 1.9

  • Encoding support and multinationalization
  • Method Calling
  • Blocks, Procs, and Iterators
  • Regular Expressions
  • Fibers
  • Lots more…
  • Performance

The 1.9 rdoc can be found here

# default argument values not only at end of argument list.

def lunch(spam=false, how_much)
  spam ? puts("no thanks") : puts(how_much)
end

lunch(2)
lunch(true, 2)

#symbol to proc backed in

puts ['daffy', 'donald'].map(&:upcase)

# enumerator

e = [1,2,3].each
p e.next
p e.next
e.rewind
p e.next

# Enumerator halts loops

letters = ('a'..'e').to_enum
loop do 
    puts "STARTING: #{letters.next}"   # .next Stops the loop at the end by raising StopIteration. Can call it your self.
end
puts "DONE"

# Stabby Procs

l = lambda{|x,y| "woo ugly #{x+y}"}.call(1,2) # 1.8
puts l

l =->x,y{"woo ugly #{x+y}"}.(1,2)  # 1.9
puts l

# === is Margic

less_than_one_thousand =-> v{v<1000}
divisible_by_three =->v{v % 3 ==0}

case 909
 when less_than_one_thousand
     puts "#is Less than a thousand"
 when divisible_by_three
     puts "is Divisible by three" 
end   

puts  1005 ===  less_than_one_thousand

# Block paramters always local to block

n = 123
10.times do |n|
end
puts n  

# Can declare block-local varaibles

number_processded = 12
pizza_shop =->(orders; number_processded) do  # ; means use outside scope
  puts "inside:#{number_processded}"
end

pizza_shop[["pepperoni", "anchovies"]]
puts "number_processded:#{number_processded}"

# Curry

adder =->(x,y){x+y}.curry
add_three = adder.(3)
# add_three.(1,2)  ArgumentError: wrong number of arguments (3 for 2)
add_four = adder[4]

 
#  Reglar Expression - (?<name>pattern)

if /(?<key>\w+)=(?<value>.*)/ =~ "ruby=cool and fun" 
    puts key
    puts value
end

# Reglar Expression  
# see http://pragdave.blogs.pragprog.com/pragdave/2008/10/fun-with-ruby-19-regular-expressions.html

# Fibers

fibs = Fiber.new do
  n1 = n2 = 1
  loop do 
    Fiber.yield n1
    n1, n2 = n2, n1+n2
    puts "n1:#{n1} n2:#{n2}"
  end  
end  

10.times { print fibs.resume}

# Fibers - continuation

require 'continuation'  # basis of continuation
require 'fiber'         # symetrical coroutines

More Ruby 1.9 goodness

  • RubyGems now backed in
  • BasicObject
  • Ripper: access to the parser
  • Object#tap and Object#p
  • Enumerable#cycle
  • Hashes now retina their order
  • Hash#select
  • Minitest and minispec
# from http://pragdave.blogs.pragprog.com/pragdave/2008/04/babydoc.html

require 'ripper/filter'
require 'ripper'

# This class handles parser events, extracting
# comments and attaching them to class definitions
class BabyRDoc < Ripper::Filter
  def initialize(*)
    super
    reset_state
  end

  def on_default(event, token, output)
    reset_state
    output
  end

  def on_sp(token, output) output end
  alias on_nil on_sp

  def on_comment(comment, output)
    @comment << comment.sub(/^\s*#\s*/, "    ")
    output
  end

  def on_kw(name, output)
    @expecting_class_name = (name == 'class')
    output
  end

  def on_const(name, output)
    if @expecting_class_name
      output << "#{name}:\n"
      output <<  @comment
    end
    reset_state
    output
  end

  private

  def reset_state
    @comment = ""
    @expecting_class_name = false
  end
end

BabyRDoc.new(File.read(__FILE__)).parse(STDOUT)

Performance

It’s really faster. Chad is showing some benchmarks, but experiencing makes you believe.

How to use 1.9 without blowing up your production version?

  • Download or checkout source
  • autoconf (if checked out)
  • ./configure
  • make
  • make test
  • sudo make install

Install’s it in /usr/local/ruby.

Now you may want to prefer installing it in it’s own directory. Then do

  • ./configure —prefix=/usr/local/ruby19
  • export PATH=/usr/local/ruby1.9/bin/:$PATH

When you install gems it install them in the ruby version you use to run the gem install.

require 'rbconfig'
puts RbConfig::CONFIG

But wait…you can make it better. Dave installs it in it’s home folder.

  • ./configure —prefix=/Users/dave/ruby19

Design in a Dynamic Language

  • Can write in Java style, but it’s better to write using ruby.
  • Inheritance (is-a) .vs. Composition (has-a)
  • Inheritance is the work of the devil
  • Inheritance the exception, composition the norm

Composition

  require 'forwardable'
  class Queue
    extend Forwardable

    def initialize
      @q = [ ]    # prepare delegate object
    end

    # setup prefered interface, enq() and deq()...
    def_delegator :@q, :push, :enq
    def_delegator :@q, :shift, :deq

    # support some general Array methods that fit Queues well
    def_delegators :@q, :clear, :first, :push, :shift, :size
  end

Posted by Daniel Wanja Mon, 09 Mar 2009 22:30:27 GMT