Wednesday 4 September 2013

Ruby: third tutorial (part 1 of tbc)

G'day:
I first encountered Code School a few months back when Ray pointed out that they were having a free weekend. Having taken advantage of the freeness and being satisfied with the quality of their content (if not their delivery, in places), I signed up for a paid-for account. And - as is often the way with these things - did not touch it again. That was a waste of money! Anyway, I can't afford to be throwing money away, so I forced myself to sit at the computer last weekend and work through another course: this time "Ruby Bits Part 2". I'd enjoyed the first two Ruby courses I'd done, and intend to complete the entire Ruby track: then courses in all. So here are my CF-developer-centric thoughts on this third course.

Blocks, Procs and Lambdas and digressions

Ruby has a strange adjunct to how one defines and uses a function, in that any function call seems to be also able to take a code "block" as a special last argument, which can then be executed on demand from within the function's code. here's an example:

def number_lister numbers
    numbers.each do |number|
        puts number
        yield
    end
end

number_lister ["tahi", "rua", "toru", "wha"] {puts "====="}

And this outputs:

C:\src\ruby>ruby block_sample.rb
tahi
=====
rua
=====
toru
=====
wha
=====

The block is executed by the yield statement. One interesting thing here is that the above syntax works fine on my local install of Ruby which is 1.9.3 (puts RUBY_VERSION), but not on repl.it which is on 1.8.7. The current version of Ruby is 2.0.0, however I'm running 1.9.3 as that's what the railsinstaller package includes. I'm not sure why they're still languishing on 1.x instead of 2.x: compat issues perhaps. I think I read something to that effect somewhere, but cannot find a ref anywhere now.

Anyway, to get that code to work on repl.it, I need to use more verbose syntax for the block:

def number_lister numbers
    numbers.each do |number|
        puts number
        yield
    end
end

number_lister ["tahi", "rua", "toru", "wha"] do puts "=====" end

Note the do / end instead of the curly braces. Indeed I think this would normally be written like this:

number_lister ["tahi", "rua", "toru", "wha"] do
    puts "====="
end

One can pass values into the block thus:

def colour_lister colours
    colours.each_with_index do |value, index|
        yield value, index
    end
end

colour_lister ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"] {
    |value, index|
    puts "#{index+1} #{value}"
}

Also note here I'm using a different array iterating construct: each_with_index, which gives both... the value and the index to the loop.

Now this is all lovely, but I have to ask... "why?". Why do functions intrinsically have an optional block argument that can be passed to them? I have no problem with the notion of functions taking callback arguments, indeed it's a fantastic construct, but surely if one wants to write a function which takes a callback argument... then write it accordingly! I don't get why Ruby has this as an innate thing. It seems very clunky / poorly designed to me. I'm all ears if someone has a reasonable explanation. Although as I explore blocks / procs / lambdas further, it more and more seems like some sort of poorly-planned bolt-on, than a good idea.

One last thing to note before moving on slightly, one can check to see if the block is there before yielding to it:

def colour_lister colours
    if  block_given?
        puts "I've got a block"
    else
        puts "No block passed"
    end
    colours.each_with_index do |value, index|
        yield index if block_given?
        puts value
    end
end

colour_lister ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"] {
    |index|
    print "#{index+1} "
}
puts "======================"
colour_lister ["whero","karaka","kowhai","kakariki","kikorangi","tawatawa","mawhero"]

And this outputs:

1 whero
2 karaka
3 kowhai
4 kakariki
5 kikorangi
6 tawatawa
7 mawhero
======================
No block passed
whero
karaka
kowhai
kakariki
kikorangi
tawatawa
mawhero

The key bit here is block_given? (yeah, the question mark is part of the construct). This is a boolean method which returns true if there's a block to use. Pleasingly, in Ruby a convention is that boolean functions have a trailing question mark. I think this helps in the reading of the code. Note that the question mark has no special meaning to the Ruby language interpreter, it is simply a convention.

Also note the interesting (to a CFML developer, anyhow) syntax with how the conditionality works on the yield statement:

yield index if block_given?

The statement only executes if the following condition is met. This is an interesting reversal of condition-first, actions follow. I guess it's a readability thing. And it's quite nice, I guess.

Digressing with this construct further, one can do this:

puts "Zachary" if true

But one cannot - it seems - do this:

if true puts "Zachary" end

One needs to multi-line that one:

if true
    puts "Zachary"
end

There's probably some good reason why one cannot have that on a single line, but it escapes me...

[beat]

... or at least it did until I read the error message (something I berate people for not doing when faced with an error and being puzzled as to why their code doesn't work!):

singleLine.rb:1: syntax error, unexpected tIDENTIFIER, expecting keyword_then or ';' or '\n'
if true puts "Zachary" end
            ^

Right, so that's a dead give away. It explains the error, and tells me what to do about it. The revised - syntactically correct - version works:

if true then puts "Zachary" end

Looking at it now... Ruby needs the then in there, perhaps, so it can tell where the condition ends and the "if true" block starts. And it can infer that if there's a line break. Dunno.



Do you know what? That's tipping 1000 words there (incl. the code), and that's perhaps a reasonable place to pause.

As a kind of interim conclusion, I'm enjoying the learning process here, and I like the emphasis on passing callback code around, even if I think Ruby's approach to the "block" construct a bit misguided. I'll continue with this line of thinking later (today? Tomorrow? Dunno at this point)...

--
Adam