Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Simon Free and Dan Wilson and Jason Dean
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Simon Free ( @simonfree ) Dan Wilson ( @DanWilson ) Jason Dean ( @JasonPDean )

Seven Languages In Seven Weeks: Ruby - Day 2

By on
Tags:

I just finished Ruby - Day 2 in my Seven Languages in Seven Weeks book. Day 2 definitely kicked it up a notch from day 1. I love how much the author - Bruce Tate - is really pushing us to look things up. He gives us problems that aren't too difficult, but they definitely require a lot of independent study in order to solve. And, while this is frustrating at times, having to open up a dozen browser tabs really gives a sense of just how robust a language can be. Ruby in particular, seems to provide a half-dozen ways to solve every problem, each of which uses fewer lines of code and less syntax than the approach before it.

HW1: Print the contents of an array of sixteen numbers, four numbers at a time, using just each. Now, do the same with each_slice in Enumerable.

# Print the contents of an array of sixteen numbers, four numbers
# at a time, using just each. Now, do the same with each_slice in
# Enumerable.


# First, let's build up our array. We can use a Range's Enumerable
# properties to build our array of values.

values = (1..16).to_a();

# Now, we're going to loop over the values using each. However, in
# order to output 4 values at a time, we're gonna only do so on
# certain indexes.
#
# Since we don't have any insight into which value we are looking
# at (index-wise), we're going to keep an internal stack of
# values which we will output only when it reaches a length of 4.
# However, I can't figure out how to do this with *just* each. As
# such, I am creating an external stack.

valueStack = [];

# Now, loop over each element in the array.

values.each{ |value|

	# Push the current iteration value onto the stack.

	valueStack.push( value );

	# Check to see if the value stack has reached a length of 4. If
	# so, we are going to output it.

	if (valueStack.length == 4)

		# Output the stack of 4 values as a list.

		puts( valueStack.join( ", " ) );

		# Reset the value stack to prepare it to collect the next
		# four values in the array.

		valueStack = [];

	end

};

I don't think I fully knew how to solve this problem. I tried a few attempts that only used the each() method; but, they all failed do to the local scoping of variables within the code block. Finally, I had to compromise and define a local variable outside of the each() method which I could then use as a stack within the iteration. I'd love to see how this can be sovled with only each().

When we run the above code, we get the following console output:

1, 2, 3, 4
5, 6, 7, 8
9, 10, 11, 12
13, 14, 15, 16

Once I had that done, I then tried to solve the problem with each_slice(), which was extremely easy:

# Print the contents of an array of sixteen numbers, four numbers
# at a time, using just each. Now, do the same with each_slice in
# Enumerable.


# First, let's build up our array. We can use a Range's Enumerable
# properties to build our array of values.

values = (1..16).to_a();


# This time, we are going to iterate over the array, four elements
# at a time, using the each_slice() method. This allows us to look
# at sections of an array as sub-arrays.

values.each_slice( 4 ){ |valueStack|

	# Output the sub-section of the array as a list.

	puts( valueStack.join( ", " ) );

};

This was much much easier. As you can see, when you use each_slice(), the sub-elements of the array are packaged up and passed to the code block - no grouping logic is required on our end. And, when we run the above code, we get the same exact console output, so I won't bother showing it.

HW2: The Tree class was interesting, but it did not allow you to specify a new tree with a clean interface. Let the initializer accept a nested structure with hashes and arrays. You should be able to specify a tree like this: { "grandpa" => { "dad" => { "child1" => {}, "child2" => {} }, "uncle" => { "child3" => {}, "child4" => {} } } }.

# The Tree class was interesting, but it did not allow you to
# specify a new tree with a clean user interface. Let the initailizer
# accept a nested structure with hashes and arrays. You should be
# able to specific a tree like this: .....


# The Tree Class

class Tree

	attr_accessor :childNodes;
	attr_accessor :name;


	# I return an initialized Tree instance.

	def initialize( treeData )

		# Initialize the node variables. By default, we are going
		# to treat this node as the root node (unless the incoming
		# tree data only has one top-level key, in which case that
		# will be the root node).

		@name = "root";
		@childNodes = [];

		# Check to see how many top-level keys the tree data has.
		# If it only has one, we can use it to define this tree
		# node; if it has more than one, will have to treat it as
		# the child data of a the "root" node.

		if (treeData.size() == 1)

			# We only have one top-level key. We can use that to
			# define this tree node.

			@name = treeData.keys()[ 0 ];

			# Convert the sub-level tree data into the chile nodes
			# of this tree node.

			treeData[ @name ].each{ |key, value|

				@childNodes.push(
					Tree.new( { key => value } )
				);

			};

		else

			# We have more than one top-level key. We need to
			# create a root node to house multiple nodes.

			treeData.each{ |key, value|

				@childNodes.push(
					Tree.new( { key => value } )
				);

			};

		end

	end


	# I visit all decendant nodes in a depth-first traversal.

	def visitAll( &block )

		# Visit this node.

		visit( &block );

		# Visit all of this node's children.

		childNodes.each{ |c|

			c.visitAll( &block );

		}

	end


	# I visit just this node.

	def visit( &block )

		block.call( self );

	end

end



# ------------------------------------------------------------ #
# ------------------------------------------------------------ #
# ------------------------------------------------------------ #
# ------------------------------------------------------------ #


# Create our new Tree.

tree = Tree.new(
	{
		"grandpa" => {
			"dad" => {
				"child1" => {},
				"child2" => {}
			},
			"uncle" => {
				"child3" => {},
				"child4" => {}
			}
		}
	}
);


# Visit all nodes in the tree, starting with the root.

tree.visitAll{ |node|

	puts( "Visiting #{node.name}" );

};

This problem was particularly hard because we had to build a tree based on a flexible data structure. The biggest problem that this presented was the fact that there's nothing about a hash that says it has to have one key. Normally, this isn't a problem; but, when you're dealing with a tree that necessarily has to start with one node, fitting one into the other can require some trickery. To deal with this, I check for the number of top-level keys that are in the hash - if there is only one, I treat it as the root node. If there are multiple, I create a new root node to house all of the top-level keys.

In my Tree class methods, you'll notice that the code block arguments are preceded by an ampersand. This apparently turns the code block into a Proc object which can then be treated like a variable. I don't fully understand what that means, but it appears to be necessary.

When we run the above code, we get the following console output:

Visiting grandpa
Visiting uncle
Visiting child3
Visiting child4
Visiting dad
Visiting child1
Visiting child2

HW3: Write a simple grep that will print the lines of a file having any occurrences of a phrase anywhere in that line. You will need to do a simple regular expression match and read lines from a file. (This is surprisingly simple in Ruby.) If you want, include line numbers.

# The first thing we are going to do is build up the file content
# we are going to be searching. In this case, we are going to be
# building up the content using Ruby's "Here Document" notation.

content = <<END_CONTENT_BUFFER.strip().gsub( /^\t*/m, "" )

	I definitely think that Helena Bonham Carter is hot. I
	know people will disagree with me here... and maybe she's
	not hot in the most mundane sense; but, there's something just
	absolutely stunning about her. Plus, she's a wonderful actress
	which makes her seem all that much hotter. If you haven't seen
	"Conversations With Other Women," I'd recommend it. She plays
	qutie well opposite Aaron Eckhart. Plus, it was the first
	movie I've ever seen her in knickers - it's refreshing to see a
	woman who can be hot without feeling like she has to be a twig.

END_CONTENT_BUFFER

# Create a connection to the relative-path file (relative to the
# current directory of execution).

contentFile = File.new( "./data.txt", "w" );

# Write the content to the file.

contentFile.puts( content );

# Since we opened this file as "writing", we need to close it -
# we can't use it for reading.

contentFile.close();


# ------------------------------------------------------------ #
# ------------------------------------------------------------ #
# ------------------------------------------------------------ #
# ------------------------------------------------------------ #


# Set the phrase that we're going to be searching for within the
# file. This could also be gotten from the standard IO (command
# line), but we'll hard-code it for now.

targetPhrase = "hot";


# Open the data file for searching. Notice that we are opening
# the file with a code block; in doing it this way, we don't
# have to explicitly close the file when we're done - Ruby will
# take care of that for us.

File.open( "./data.txt", "r" ){ |fileStream|

	# Read the file in, one line at a time, until we reach the
	# end of the file. Gets() will return the line or return nil
	# when it hits the end of the file.

	while ( line = fileStream.gets())

		# When Ruby uses a file, it appears to create meta data
		# about the line that's been read in and the line number.
		# I'm defining both here for reference, but am not using
		# all of it (since we assinged line above).

		lineContent = $_;
		lineNumber = $.;

		# Check the current line to see if it contains the given
		# phrase. To make the search a bit more flexible, we are
		# going to use a case-insensitive regular expression. Notice
		# that a regular expression literal can use variable
		# subistitution via the #{var} notation. The "o" pattern
		# modifier indicates that this subsitution only takes place
		# the first time the pattern is evaluated (I don't fully
		# understand what that means).
		#
		# NOTE: This assumes no reg-ex special charactrers are within
		# our pattern.

		if (line =~ /#{targetPhrase}/io)

			# Output the current line with the line number. When
			# you open a file,

			puts( "Line #{lineNumber}: #{line}" );

		end

	end

}

In this problem, I am first writing content to a file and then reading the file back in, line by line, looking for a given phrase. The content is defined by a Ruby "Here Document," or "heredoc." This is somewhat like the CFSaveContent tag in ColdFusion. The cool part about it, however, is that you can perform actions on the buffered content by attaching methods to the heredoc delimiter (in this case, I am stripping white space before saving the value).

When we run the above code, we get the following console output:

Line 1: I definitely think that Helena Bonham Carter is hot. I
Line 3: not hot in the most mundane sense; but, there's something just
Line 5: which makes her seem all that much hotter. If you haven't seen
Line 9: woman who can be hot without feeling like she has to be a twig.

This homework took a good amount of time to figure out. Like I said, the book is really pushing us to learn independently. I still have some issues with the extreme amount of syntactic sugar that Ruby provides; but, I suppose that once you get used to it, it does make programming faster.

Want to use code from this post? Check out the license.

Reader Comments

68 Comments

Another Perlism: Heredocs. Nice.

Another resource for you, if you find yourself really enjoying Ruby:

http://railsforzombies.org/

(One of our graduates worked on that project. That's the second language I've seen with an interactive console where you can test out language features -- the first I saw was MongoDB. It's pretty slick, and would be awesome for CFML/CFScript.)

20 Comments

@Rick

Most modern languages (ruby, python, php, tcl, ...) have interactive consoles. In fact Lisp had an interactive console back in the 60s. If anything, it's weird to see a language (like CF) that doesn't have one.

@Ben

To do it without a variable outside the block just invert your thinking and use each on a range:

a = (1..16).to_a
(0...4).each {|i| puts a[4*i...4*(i+1)].join(',') }

We can then generalize this to print any length array in groups of four:

def print_quads(a)
(0...a.size/4+1).each {|i| puts a[4*i...4*(i+1)].join(',') }
end

print_quads((1..7).to_a)
print_quads((1..16).to_a)

And we can generalize again and implement each_slice

def each_slice(a, n)
return if a.empty?
(0...a.size/n+1).each {|i| yield a[n*i...n*(i+1)] }
end

# print groups of 3
each_slice((1..7).to_a, 3) { |slice| puts slice.join(',') }

Your other examples could also be simplified if written more ruby like:

http://elliottsprehn.com/examples/bennadel-2062/file.rb
http://elliottsprehn.com/examples/bennadel-2062/tree.rb

You may want to check out a ruby style guide. It'll explain when to use certain features, naming conventions, and tips for writing simpler code.

Cool to see you checking out ruby. The only downside is that once you really get the hang of a language like ruby or python, CF code looks so bloated. It does change your perspective about how to write simple code though! :)

15,640 Comments

@Rick,

Thanks for the link. The book hasn't touched on Rails at all (beyond mentioning that it is very popular). It is definitely something I'd be interested in exploring.

@Elliott,

Very cool solutions! I think the key to being able to write such concise code is, in part, changing the mindset, and in part, simply knowing the vast amount of methods that are available!

I know my style is way off :) Ruby really loves the "_" approach to naming and *no* semi-colons (oh the humanity) which just kind of feels weird to me. I would assume that as I got more comfortable with the language, I would probably adapt the conventions more readily.

I have to say, though, Ranges are simply badass! As is the ability to bring back a portion of an array using a range selection.

1 Comments

Here's another possible solution for the first example (just using each):

>> a = (1..16).to_a

>> a.each {|i| print "#{i} "; puts if i % 4 == 0}

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16

15,640 Comments

@Stephen,

Pretty good, my friend. I see this leverages the fact that some outputs don't append a newLine character. Slick.

6 Comments

Just going through the book myself, and new to all seven languages as well (though my background is much more C/C++).

My attempt at grep used foreach:

def grep(expr, fname)
	File.foreach(fname) { |line|
	puts "line #{$.}: #{line}" if line =~ /#{expr}/
	}
end
15,640 Comments

@Jody,

I remember seeing something about foreach() and I could have sworn that there was a reason that I didn't use it. Although, I have so many languages floating around in my head that I can't remember what language I remember doing what in.

Your example looks way more concise than mine, though.

But, what I can say for fact is that I am IN LOVE with any kind of regular expression operator :) When I saw that languages like Groovy and Ruby can use "=~" for regular expression actions, my heart just melted :)

1 Comments

Another solution to #2 which I think is a little cleaner/shorter and handles nils, plus embedded hashes/arrays as mentioned in the book. I think it reflects my prior experience with Erlang, may not be Ruby-ish enough -- I'd be curious for someone to Ruby-ify the sample below.

I like the one provided by Elliott Sprehn but looks to me like it only handles hashes. That's a question really as perhaps I'm misunderstanding the code, I'm definitely still getting the hang of Ruby.

class Tree
 
	attr_accessor :children, :name
 
	def initialize(name, children=[])
		@name = name
		def trees_from_any(node)
			case node
				when NilClass then []
				when Tree     then [node]
				when String   then [Tree.new node]
				when Hash     then node.map { |key, value| Tree.new(key.to_s, value) }
				when Array    then node.map { |a| trees_from_any a }.flatten
				else [Tree.new "Unknown[#{node}] of type: #{node.class}"]
			end
		end
		@children = trees_from_any(children).flatten
	end
 
	def visit_all(&block)
		visit &block
		@children.each { |child| child.visit_all &block }
	end
 
	def visit(&block)
		block.call self
	end
 
end
1 Comments

I just got this book for Christmas, and I had a lot of fun/trouble with #2 as well. I'm still terrible at Ruby, so I'm not sure how Rubythonic (or whatever people say) this is, but here's my solution:

 
class Tree
	attr_accessor :children, :node_name
 
	def initialize(spec)
	@node_name = spec.keys[0]
	@children = []
	children_specs = spec.values[0]
	children_specs.each { |k, v| children.push(Tree.new({k => v})) }
	end
 
	def visit_all(&block)
	visit &block
	children.each {|c| c.visit_all &block}
	end
 
	def visit(&block)
	block.call self
	end
end
15,640 Comments

@Nat,

Looks pretty cool to me. I especially like your use of pattern matching (in the non-regex sense). As I have gotten through the first 5 languages, I am definitely getting a bit of a better handle on the concept of data-type pattern matching. At first it was super confusing. But now, I am starting to see some of its power.

@Boatzart,

Congrats on getting the book. I've been having a blast with it. It is, at the same time, thrilling, frustrating, demeaning, and inspiring. It's very interesting to go from something like ColdFusion, where I know so much about the language, to these others where I know pretty much nothing.

2 Comments

I don't know if you'd seen this yet, but you can print the array using each_slice with only one line of code like so:

(1..16).each_slice(4) {|a| p a}

It's short and sexy and amazing that this is built into Ruby.

2 Comments

@Matt,

I don't know what type of machine you're on but if it's Mac or Linux, you can use RVM to have different Ruby versions installed. If you're on Windows, you can use Pik to do the same type of thing.

@Boatzart,

One thing in Ruby that you probably wouldn't want to do is using the word 'spec' for naming, especially if you end up using Rspec to write tests. Rubyists tend to opt for more descriptive_naming_conventions_using_underscores.

1 Comments

I'm relieved to see that the second tree exercise was somewhat of a challenge to others as well. Below is what i came up with after staring at the screen for 30 minutes. Ruby is definitely fun.

#day2_tree2.rb

class Node

attr_accessor :name, :children

def initialize(tree_data)
	
	if 1 == tree_data.size
	@name = tree_data.keys[0]
	child_nodes = tree_data[@name]
	else
	@name = 'Root'
	child_nodes = tree_data
	end
	
	@children = []
	
	child_nodes.each do |key, value|
	children.push( Node.new( {key => value} ) )
	end
	
end


def visit(&block)
	block.call self
end


def visit_all(&block)
	visit &block
	@children.each {|c| c.visit_all &block}
end

end

tree_data = {'grandpa' =>
				{ 'dad' =>
							{'child 1' => {}, 'child 2' => {} },
					'uncle' =>
							{'child 3' => {}, 'child 4' => {} } },
			'grandma' =>
				{ 'aunt 1' =>
							{'x child 1' => {}, 'x child 2' => {} },
					'aunt 2' =>
							{'x child 3' => {}, 'x child 4' => {} } }
			}

Node.new(tree_data).visit_all{|node| puts node.name}
1 Comments

Hi guys,
thanks for all the info here, my solution for do#1 is similar to Elliott's; but I wanted to point out that Stephen's rely on the values of the array, which can be misleading. For instance it doesn't work in this case:

a = (11..26).to_a
a.each {|i| print "#{i} "; puts if i % 4 == 0}

which outputs:

11 12
13 14 15 16
17 18 19 20
21 22 23 24
25 26

the mental switch, imho, is to forget about the array, and start to think about the range instead. Nice switch btw.

2 Comments

Way dated but since I'm just now getting into this book I'd like to contribute :)

My answer was close to Stephen's original...

(1..16).each {|i| i % 4 == 0 ? (puts i) : (print i, ", ")}

2 Comments

My first solution for the problem #3. I'd be curious to better implementations though....

# readFile.rb
filename = ARGV[0]
word = ARGV[1]

f = File.open(filename, 'r')
f.each_line { |line| /#{word}/.match(line) ? (puts "#{f.lineno}: #{line}") : next}

# From Command Line
ruby readFile.rb lorem.txt lorem

1 Comments

Why Not?

def initialize(dane={})
@dzieci=[]
dane.each do |k,v|
	@nazwa_wezla = k
	v.each do |kk,vv|
	@dzieci.push(Drzewo.new(kk => vv))
	end
	end
end
I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel