You might recall a few weeks back I started doing the Ruby courses at Code School, and I was documenting my findings / experiences as I worked my way through the lessons. By the end of the second lesson my brain was fried and I realised a) I wasn't really taking it all in any more; b) my writing was just rubbish. So I broke off the last article halfway through, intending to revisit it the following day to complete it. As is often the case, I got distracted, and am only revisiting this now.
Just to recap, in the first part of this lesson I covered expressions, methods and classes, exceptions, and touched on ActiveSupport. All fascinating stuff. The balance of the lesson covers modules and blocks. These are pretty comprehensive topics, so there's a lot still to write-up.
Modules
A module - it seems - is basically an include file with a library of functions withing which functions are defined. Although as the video explained - I actually had to watch this one twice, because I found myself asking "WTF are you on about, mate?" half way through the first viewing - there's a lot more to using them than one might get with a simple <cfinclude> in CFML.Namespacing
The video starts with the notion that to group like-minded functions together, one can just put them in a file and require the file. Then one can simply call the functions in ones code. Much like a CFM file with a bunch of UDFs in it. As the dude points out, this is less than ideal as it pollutes the global namespace with all these UDFs, which is a bit messy. One can deal with this in Ruby using module and give it a name, thus namespacing the functions:module MaoriWordsUtils
def self.getNumbers
["tahi", "rua", "toru", "wha"]
end
end
puts MaoriWordsUtils.getNumbers
Note how we've now got some namespacing on the library functions.
This sort of thing would be great for libraries of stuff like on CFLib.
Mixins
The same as including a file in CFML, one can include a module within a class in Ruby to avail those methods as instance methods any objects created from that class, as a mixin:module MaoriWordsUtils
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
class MaoriWords
include MaoriWordsUtils
end
words = MaoriWords.new
puts words.getNumbers
Here instead of just using the module directly, we're using include to add methods (see: append_features) into the class. The tutorial explained the whys and wherefores of doing all this mixin vs modules vs inheritance, etc, and it's kinda general design pattern stuff, so if you want to know about that... google. It's not a language specific thing, so outwith the remit of this article.
Extend
Ruby's support for mixins is a bit more thorough than ColdFusion's. Well: Ruby's support for methods in general is more fully-realised than CFML's, in that Ruby has the concept of class methods (static methods in Java parlance) and object methods. This concept is followed-through with how modules can be mixed-in to a class, and one can use extend to mix-in the module's methods as class methods (not instance methods):module MaoriWordsUtils
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
class MaoriWords
extend MaoriWordsUtils
end
puts MaoriWords.getNumbers
Note that I'm not creating an instance of MaoriWords there, I'm calling the method directly on the class. Here's a good explanation of class methods vs instance methods.
So to recap: to use a module to mix-in instance methods: use include. To mix-in the module's methods as class methods: use extend. There's more to it than that, but I'll get to that in a bit.
Object Extend
On the other hand, we can extend a single object with methods from a module too:module MaoriWordsUtils
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
class MaoriWords
end
maoriWords1 = MaoriWords.new
maoriWords2 = MaoriWords.new
maoriWords1.extend MaoriWordsUtils
puts maoriWords1.getNumbers
puts maoriWords2.getNumbers
The call to getNumbers only works for maoriWords1, not maoriWords2. That in itself is not so surprising, but the technique to extend an object is nice! As the video points out, it's slightly confusing that from within a class extend is used to add the module's methods as class methods, but when used on an object it adds them as instance methods. Not ideal IMO, but I guess it's just a quirk one needs to remember.
Hook Methods
This is quite cool. Ruby modules have hooks that listen out for the module being included (etc), so [stuff] can be done when they are (used). You'll've noted that include adds a module's functions as instance methods, and extend adds them as class methods. What above if you want both (and this will be a common requirement)? One way to do it is to simply organise the module into instance methods and class methods (the class methods being defined in an inner module), and then use both include and extend:# encoding: utf-8
module MaoriWordsUtils
module ClassMethods
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
def getColour index
["whero", "karaka", "kowhai", "kakariki", "kikorangi", "tuauri", "tawatawa"][index]
end
end
class MaoriWords
include MaoriWordsUtils
extend MaoriWordsUtils::ClassMethods
end
puts MaoriWords.getNumbers
puts "\n"
maoriWords = MaoriWords.new
puts maoriWords.getColour 2
Notice how we've got a module within a module, and use the :: operator to reference the inner module within the outer one. The label "ClassMethods" is not significant, it is just a Ruby convention. It could be anything.
However this requires the class to have knowledge of which methods are intended for which usage, and for it to be replicated within every class using the module. A more consolidated approach is to use one of these hook methods within the module which allow it to detect how it's being used, then dictate to the class using it how the module should be used. Kinda. Basically one can do this:
# encoding: utf-8
module MaoriWordsUtils
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
def getColour index
["whero", "karaka", "kowhai", "kakariki", "kikorangi", "tuauri", "tawatawa"][index]
end
end
class MaoriWords
include MaoriWordsUtils
end
puts MaoriWords.getNumbers
puts "\n"
maoriWords = MaoriWords.new
puts maoriWords.getColour 2
Here the module dictates both instance and class methods. A key thing here is that the base reference above is a reference to the class doing the extending/including. So the in the base.extend expression, base is the MaoriWords class, so the MaoriWords class gets extended with MaoriWordsUtils' ClassMethods.
All the include / extends / hooks and stuff are well explained in another article by the bod whose blog I linked to before. He explains it better than I can (I am only just grasping some of this stuff, so I'm not the best person to explain it. I'm only on day 2 of Ruby, don't forget!).
ActiveSupport::Concern
I have to admit that even after the second viewing of the video, I had no idea what the fella was on about with this. I am hoping looking at some code and googling will help.[research... another few viewings of the video... working through the code... head-scratching...]
OK, basically the Concern module just streamlines some stuff around module integration. In the first example in the tutorial, the code to handle the class / instance method stuff can be streamlined, thus:
# encoding: utf-8
require "active_support/concern.rb"
module MaoriWordsUtils
extend ActiveSupport::Concernincluded
module ClassMethods
def getNumbers
["tahi", "rua", "toru", "wha"]
end
end
def getColour index
["whero", "karaka", "kowhai", "kakariki", "kikorangi", "tuauri", "tawatawa"][index]
end
end
That's easier. Also note that when looking at the docs for Concern I linked to above, I was able to just track down the bare-minimum requirement for it (instead of requiring active_support/all). I guess the conceit here is that when doing the include/extends stuff one will generally want to deal with both the instance and class methods, so ActiveSupport:concern removes the need to write the same repetitive code each time one wants to do it.
Another thing ActiveSupport:concern allows us to do is to execute a code block when the module is included:
require "active_support/concern.rb"
module Utils
extend ActiveSupport::Concern
included do
puts "Kia ora"
end
end
class SomeClass
include Utils
end
Interestingly, even though there's no code calling anything here: I'm just defining a module and a class, I still get "Kia ora" on the screen. This is a foolishly contrived example, but one could use this to run some code to fire up things the module needs to work properly.
Lastly - for this section - Concern can handle dependency management for us too. This basically seems to get around circular dependencies, by - and I'm getting this wrong - peppering extend ActiveSupport::Concern about the place within modules which share dependencies, and then Ruby (and ActiveSupport) sorts out how to load stuff to resolve the dependencies. I'm not going to put my own code example here as I don't want to suggest it's something one ought to use, but there's an impractical example (it's all" foo" and "bar", rather than practical real-world stuff) which demonstrates it adequately in the docs for ActiveSupport::Concern.
Fortunately that's the end of that section of the course. I gotta say that I dissed the videos before, but the code exercises were even worse. Often I simply couldn't understand what they were asking, until I clicked the "hint" option which showed me the solution, and I was left thinking "oh for goodness sake... if that's what you meant, why didn't you just say?!" I understood the theory/technology just fine (except how the dependency-resolution stuff worked, unless "basically by magic" is the actual mechanism), but the instructions were very unclear. And in defence of the videos, now that I've watched the one for this section of the course again, I do actually "get" what he's on about now.
Next up... blocks...
Blocks
I've already mentioned blocks a bit in the first tutorial, but this one goes further.Yield
Blocks can be called, and have code passed to them. Within the block, the yield statement means "and call the code that was passed in", so we could have this for example:def greet
yield
yield
end
greet do
puts "G'day"
end
Which, fairly uselessly, results in:
G'day
G'day
Slightly more usefully, one can pass arguments into the code, thus:
def greet
yield "Kia ora!"
yield "Hallo!"
end
greet do |greeting|
puts greeting
end
This outputs:
Kia Ora!
Hallo!
Lastly, as with any other contruct in Ruby: blocks return values too. The value of the last expression in the block is returned:
def binaryOperation(x,y)
yieldResult = yield x,y
puts "yieldResult: " + yieldResult.to_s
return yieldResult
end
binaryOperationResult = binaryOperation(2,3) {|a,b| a+b}
puts "binaryOperationResult: " + binaryOperationResult.to_s
binaryOperationResult = binaryOperation(4,5) {|a,b| a*b}
puts "binaryOperationResult: " + binaryOperationResult.to_s
That example is somewhat lacking in imagination, but it demonstrates the point:
yieldResult: 5
binaryOperationResult: 5
yieldResult: 20
binaryOperationResult: 20
binaryOperationResult: 5
yieldResult: 20
binaryOperationResult: 20
I can't find the official API docs for yield, but here's the tutorial covering blocks and including yield from the docs site.
Conventions
Another thing they covered is the conventions behind whether to use curly braces or DO/END. There are two conventions.The first convention is that if the block is a single line: use curly braces. If it's more than one line: DO/END.
The second convention is that if the result of the block is meaningful, or is used in later processing: use curly braces. If the important bit is what goes on in the block, and the resultant value - remember all expressions in Ruby return values, and a block is no different here - is not important: use DO/END.
I don't know enough about Ruby nor have written (or looked at, even) enough Ruby code to form an opinion as to which fits better with me. However I suspect - given I tend towards being a bit nadelesque with my vertical whitespace (but not as bad as Ben himself) - I would seldom have a single-line block anyhow, so I'll probably err towards the latter: if it has a result: curly braces; if not: DO/END.
Enumerable
Lastly the tutorial has an impenetrable exercise introducing the Enumerable module, which one can include in a collection class to add in a bunch of methods for handling collections. The conceit to make the Enumerable stuff work is that one's class needs to implement an each() method which Enumerable then leverages to provide the rest of the functionality it offers. There's a whole raft of methods in there, and there's better ways to learn them than to have me explain them, so I'll leave this bit at simply pointing out the facility exists.Conclusion
I was pretty unsatisified with this "course"... it wasn't until after I came back to it having done my own research that a lot of it made sense, and even then, the instructions on the code exercise were so terse it was difficult to understand what it was they were asking. This is a shame. However it did encourage me to go out and do the research, and I have learned a fair chunk as a result. Cool.Oh... I re-did all the exercises just now (and wrote & tested the code examples for this article too) whilst at work, where I don't have Ruby installed. a quick google yielded repl.it which has an online Ruby interpretter. Cool!
Anyway, that's it. I've got a Skype call with my boy in 5min, so I better go plug me camera and headphones in, and get Skype cranked up...
--
Adam