Seven Languages In Seven Weeks: Ruby - Day 1
As I blogged about earlier, I've just started the new Pragmatic Programmers' book, Seven Languages in Seven Weeks. Each of the languages comes with homework assignments which I shall be discussing on the blog. I just finished Ruby - Day 1.
HW1: Print the string "Hello, world."
# Print the string "Hello World." To start off, we're going to
# output the double-quoted value. This will evaluate the string
# for any embedded variables.
puts "Hello World"
# Demonstrate the embedded variable behavior.
message = 'Hello World'
puts "#{message}"
# Print the string 'Hello World.' When using single quotes, the
# string will be treated as a literal; that is, no evaluation of
# the string will take place.
puts 'Hello World'
# As a final approach, we're going to remove some of the "syntactic
# sugar" and fall back on a more tradional look and feel.
puts( "Hello World" );
# There is also a "print" method will will print to the standard
# output (like puts), but will not append any new line.
print "Hello World\n"
When dealing with strings in Ruby, those surrounded with double-quotes are evaluated for variable replacement; those surrounded with single-quotes are treated as literals. Regardless of these rules, you'll see me using double-quotes most of the time. I know it entails more overhead but, until I am more comfortable with the syntax, I'm going to do what I can to make the code look more familiar.
When we run the above code, we get the following console output:
Hello World
Hello World
Hello World
Hello World
Hello World
HW2: For the string "Hello, Ruby," find the index of the word "Ruby."
# For the string "Hello, Ruby" find the index of the word, "Ruby."
# First, let's set up a value to test with.
phrase = "Hello, Ruby"
# We can use the index method to locate the first index of a given
# value within the string.
puts( phrase.index( "Ruby" ) );
# The index() method also accepts a regular expression pattern which
# can be defined with Javascript-like RegExp literals.
puts( phrase.index( /Ruby/ ) );
# If we're going to use regular expressions, we can use the regular
# expression operator to test for the position of the first pattern
# match within a given value.
puts( "Hello, Ruby" =~ /Ruby/i );
I love the fact that Ruby has a built-in Regular Expression operator (=~). This operator returns the index of the first match of the given pattern with the given string. It's like ColdFusion's reFind() method (except for that Ruby doesn't treat zero as a falsey - only false and nil).
When we run the above code, we get the following console output:
7
7
7
HW3: Print your name ten times.
# Print your name 10 times.
# We can start off with a basic while loop.
i = 1
while (i <= 10)
puts( "Ben" );
i = (i + 1);
end
# Ruby uses a lot of Ranges. These are sets of values that go from
# a start value to an end value. We can use a FOR loop to loop over
# the ranges.
for i in (1..10)
puts( "Benjamin" );
end
# We can also use ranges with iteration.
(1..10).each{
puts( "B-Jamin" )
}
# I am not sure if this creates a range, or is doing something else
# more "special"; but, we can also use the Integer's upto() method
# to iterate from one number to another.
#
# In this version, our iteration body is using do/end. This would
# also work with the {..} approach; I'm just trying to get used to
# the enormous number of ways in which you can execute code.
1.upto( 10 ) do |i|
puts( "Big Ben" );
end
# We can also use the times method to iteration from zero to n-1
# where n is our base value (NOTE: This one is a Married With
# Children reference).
10.times{
puts( "Grand Master B" );
}
This is really the first block of code that gave me a goofy smile. Ruby definitely has some sweet collection-based functionality. Ranges and functions like .upto() and .times() just make looping a joy. Although, I have to say that I could not find a straight-forward for() loop that matched the more traditional style (ie. for(init,condition,step)). It seemed that this kind of basic for() looping had to be finagled with a while() loop or something to that effect.
When we run the above code, we get the following console output:
Ben
Ben
Ben
Ben
Ben
Ben
Ben
Ben
Ben
Ben
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
Benjamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
B-Jamin
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Big Ben
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
Grand Master B
HW4: Print the string "This is sentence number 1," where the number 1 changes from 1 to 10.
# Print the string "This is sentence number 1", where the number 1
# changes from 1 to 10.
# Since we've already done a bunch of iteration, I'm doing to bring
# back the Range-based iteration and just use string substitution.
(1..10).each{ |i|
puts( "This is sentence number #{i}" );
};
# We can also create implicit arrays to act as collections. And, we
# can then iterate over them with the each method.
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].each{ |i|
puts( "This is sentence number #{i}" );
};
# We can also take ranges, convert them to arrays, and then iterate
# over them in the same way as we did with the above array.
(1..10).to_a().each{ |i|
puts( "This is sentence number #{i}" );
};
# Ruby seems to celebrate putting stuff on one line, which I happen
# to think makes for horrendous readability. But, I figured I'd play
# around with it a bit. One thing you start to see is this kind of
# post-execution condition.
i = 0
puts( "This is sentence number #{i+=1}" ) while (i < 10)
# Here's the same kind of loop, except it's using the "until" clause
# rather than the while clause.
i = 0
puts( "This is sentence number #{i+=1}" ) until (i == 10)
Here, I was really just trying to explore some more looping paradigms. As you can see, Ruby has a ton of ways to do looping, including a number of single-line approaches. I don't much care for looping on a single line, though it appears to be a popular mechanism in the language.
When we run the above code, we get the following console output:
This is sentence number 1
This is sentence number 2
This is sentence number 3
This is sentence number 4
This is sentence number 5
This is sentence number 6
This is sentence number 7
This is sentence number 8
This is sentence number 9
This is sentence number 10
This is sentence number 1
This is sentence number 2
This is sentence number 3
This is sentence number 4
This is sentence number 5
This is sentence number 6
This is sentence number 7
This is sentence number 8
This is sentence number 9
This is sentence number 10
This is sentence number 1
This is sentence number 2
This is sentence number 3
This is sentence number 4
This is sentence number 5
This is sentence number 6
This is sentence number 7
This is sentence number 8
This is sentence number 9
This is sentence number 10
This is sentence number 1
This is sentence number 2
This is sentence number 3
This is sentence number 4
This is sentence number 5
This is sentence number 6
This is sentence number 7
This is sentence number 8
This is sentence number 9
This is sentence number 10
This is sentence number 1
This is sentence number 2
This is sentence number 3
This is sentence number 4
This is sentence number 5
This is sentence number 6
This is sentence number 7
This is sentence number 8
This is sentence number 9
This is sentence number 10
HW5: Run a Ruby program from a file.
This is actually how I was running all of these homework (HW) assignments. As long as "ruby" is in a folder in your system paths, you can use it to run an .rb file from the command line / console:
ben$> ruby hw1.rb
HW6: Bonus problem: If you're feeling the need for a little more, write a program that picks a random number. Let a player guess the number, telling the player whether the guess is too lower or too high.
# Bonus problem: Pick a random number and have the user guess
# the number. When the guess, if incorrect, tell them if they
# are too high or too low.
# Pick a random number between 1 and 10.
randomNumber = (rand( 10 ) + 1);
# Keep looping, getting a value from the user, untli they have
# selected the random number.
begin
# Print a message for the user:
puts( "Guess the number between 1 and 10" );
print( "Guess: " );
# Get the number from the user. This number will come back as a
# string. We can then convert it to an integer so that we can
# compare it to our random number. If the user did not enter a
# numeric string, to_i() returns zero (0).
userGuess = gets().to_i();
# Check to see where the number falls compared to the randomly
# selected number.
if (userGuess < randomNumber)
puts( "...Guess higher!" );
elsif (userGuess > randomNumber)
puts( "...Guess lower!" );
end
end while( userGuess != randomNumber )
# At this point, ther user had guess the random number
# which has caused the begin/end loop to end.
puts( "Heck yeah! You guessed #{randomNumber} correctly!" );
The one thing I really didn't like about this problem was coming across the "elsif" clause of an IF/ELSE statement. I mean really? Would it have killed you guys to include the "e" in elsEif? I know Ruby is geared towards making the programmer productive; but, should that ever be done at the expense of single-letter readability?
When we run the above code, we get the following console output:
Guess the number between 1 and 10
Guess: 1
...Guess higher!
Guess the number between 1 and 10
Guess: 2
...Guess higher!
Guess the number between 1 and 10
Guess: 3
...Guess higher!
Guess the number between 1 and 10
Guess: 4
...Guess higher!
Guess the number between 1 and 10
Guess: 5
...Guess higher!
Guess the number between 1 and 10
Guess: 6
...Guess higher!
Guess the number between 1 and 10
Guess: 7
Heck yeah! You guessed 7 correctly!
As I was doing research for this answer, I also came across the fact that you could use Range objects within switch statements (case statements). I know this is not really practical in our particular problem domain; however, I wanted to take this as an opportunity to play with using Ranges for our case conditions. I have reworked the above code to use a CASE block rather than an IF block.
# Bonus problem: Pick a random number and have the user guess
# the number. When the guess, if incorrect, tell them if they
# are too high or too low.
# Pick a random number between 1 and 10.
randomNumber = (rand( 10 ) + 1);
# Keep looping, getting a value from the user, untli they have
# selected the random number.
begin
# Print a message for the user:
puts( "Guess the number between 1 and 10" );
print( "Guess: " );
# Get the number from the user. This number will come back as a
# string. We can then convert it to an integer so that we can
# compare it to our random number. If the user did not enter a
# numeric string, to_i() returns zero (0).
userGuess = gets().to_i();
# This time, we are going to be using the CASE statement to
# find the position of the guess within the 1..10 range. We are
# going to construct a lower and upper range. The lower range
# uses ... which is exclusive. The upper range uses .. which is
# inclusive.
lowerRange = (1...randomNumber);
upperRange = ((randomNumber + 1)..10);
# Check to see where the number falls compared to the randomly
# selected number.
case( userGuess )
when lowerRange
puts( "...Guess higher!" );
when upperRange
puts( "...Guess lower!" );
end
end while( userGuess != randomNumber )
# At this point, ther user had guess the random number
# which has caused the begin/end loop to end.
puts( "Heck yeah! You guessed #{randomNumber} correctly!" );
As you can see, we are now using the randomly selected number to act as the slipping point between our upper and lower Range objects. What's cool about ranges is that a case block uses a tripple equals to evaluate matches; and, with ranges, the tripple equals operator will check to see if the given value is in the range.
When we run the above code, we get the following console output:
Guess the number between 1 and 10
Guess: 1
...Guess higher!
Guess the number between 1 and 10
Guess: 2
Heck yeah! You guessed 2 correctly!
There's definitely some features of Ruby that are awesome; but, there's also some things that make me unhappy. I suppose some of that is just getting used to the new syntax (or lack thereof). One thing that I don't care for is how much of the code is optional. I am sure that a seasoned Ruby developer could come in a cut most of my code in half; but at what cost? Once you get above Assembly language, is efficiency ever really more important than readability?
Want to use code from this post? Check out the license.
Reader Comments
Ben, you can do a for loop in Ruby:
for i in 0..5
puts "Value of local variable is #{i}"
end
It's just that most Rubyists prefer the (to my eye) nicer approaches such as range.
@Hal,
I think for single-step for-loops, the range approach is simply beautiful. My concern is that if you ever need to do some sort of "i+=2" step, it looks like you have to fall back on a WHILE loop, which just looks less attractive.
But, maybe I just couldn't find the for() loop that allowed for a step-wise expression.
Hi Ben,
If you're trying to specify an increment using a for loop in Ruby, you can do something like this:
for i in (1..10).step(2)
puts i
end
Michael
@Michael,
Ahhh, awesome, thanks!
@Ben
To use a step-change on your range you can do:
(0..10).step(2) do |i|
puts "Num: #{i}"
end
Which prints:
Num: 0
Num: 2
Num: 4
Num: 6
Num: 8
Num: 10
D'oh. Gotta remember to refresh the page to see if someone like @Michael beat me to the punch. :)
@Stephen, @Michael,
Thanks to both of you :) One thing I can't quite figure out, however, is when the {/} notation is supported in addition to a do/end or something like that.
For example, I can do this:
1.upto(10){ ... }
1.upto(10) do ... end
But, I can't seem to do the same for the for() loop.
for i in (1..10) do ... end
... works, but the same with {/} seems to fail. Are there just times when the two cannot be interchanged?
Here's what I remember of the rules (I'm just an amateur Ruby user still): {...} or do...end can be used for blocks, which are really an argument to a method. So they can be used on methods (upto for instance). You can't use {...} on language constructs like "for" "if", etc.
When it comes to blocks, the {...} differs from do...end only in terms of operator precedence. I typically use {...} for one-liners and do...end for multi-line blocks, but there are other approaches.
@Stephen,
Ah, ok cool. I think that makes a bit more sense.
Cool blog post Ben. Love to see how others learn new stuff. Keep them coming!
If someone were only interested in Ruby would this be a worth while book?
Hey Ben,
I'll be interested to see how you get on with the book, seems a lot to wrap your head around in 7 weeks.
Out of interest, do you comment production code the same way you comment code samples on your blog. I find it strangely hard to read!!
Phil
@Andrew,
Trying my best :) This book is actually very grueling. The author really challenges you to figure out the problems. I'm loving it!
@Drew,
It only spends 3 "days" talking about Ruby. But the whole point of the book is that learning a number of features of a number of languages will actually help you learn all of the languages better. As he says in the intro (I think), "To get better at math, try learning Latin." Whether or not that makes sense, the point is that perspective leads to better understanding.
That said, as far as a technical manual, if you just wanted to learn Ruby head to toe, you might want something that holds your hand a bit more??
But really, I'm not one to recommend a book on a language I *just* started learning :)
@Phil,
So far, I'm loving it. It's like being in school, but only taking the cool classes :)
As for the commenting, I'm sort of in a weird place. Since these aren't going to be languages I have done before, I have no preferred editor. As such, I'm just using my ColdFusion Builder which doesn't know how to highlight the Ruby files. I am a man who loves white space, but I'm add a lot of extra white space here simply to narrow my thoughts as I type.
If you questioning the amount of comments, I'm simply trying to drill the concepts into my head by trying to explain *every* single step I take!
@Ben,
TextMate is really nice to use with Ruby and my own personal preference. It's definitely worth checking out if you haven't used it before.
Michael
@Ben,
I was just interested in your commenting in general, I see you add a lot in on your CF posts too.
I use CBuilder at work but at home I run Aptana 3 (beta) which has all the support for Ruby and Rails, might want to check it out. Maybe install on CBuilder as a plugin.
Phil
@Michael,
I'll take a look. I'm working on a Mac at the office and a Windows 7 machine at home. Been using Builder sometimes at home, sometimes just NotePad++.
@Phil,
In general, I do put a lot of comments in my code. Helps me think about what I'm doing. Plus, when I come back to the code later, it helps me remember what I did without having to mentally parse the code. Plus, since comments are typically light-gray, they also act a nice buffer between my lines of code. It allows me to focus on every individual line of code without being distracted.
Interesting post, Ben, thanks!
At first glance, it looks like Ruby got a number of things from Perl. The =~ search operator, for example, and the "single-line loop with postfix conditional" for another. That's one of my favorite things about Perl, actually, and I miss it in other languages:
is so much prettier and more elegant than
I look forward to the rest of these posts. (Especially once you get to Erlang!)
Perhaps a little late...
I have been using Rubymine, from Jetbrains for my Ruby dev - http://www.jetbrains.com/ruby/
It costs, but you get 30 days to try it for free. I am a fan of their Java IDE - IDEA - so its an easy thing to pickup.
@Rick,
Until() is cool, but the one that really warps my brain is the "unless" concept. Ruby has this thing where you can use "unless" instead of using a NOT operator... I think. It quite literally hurts for me to think about it :)
This construct just really confuses me. I find it to be completely unreadable. In the book, they said it was suppose to more easily map to "English"? But, it doesn't compute in my head.
Even in its other context, it seems so odd:
Am I the only one who finds this very counter-intuitive?
@Chris,
Thanks for the link. About to move on to "Io" in the book; I'll check this out when I come back to Ruby.
I really like the unless and if operators used like that.
Here was some code I was doing last night:
@super_programmer = SuperProgrammer.find_by_last_name( "Nadel" )
return false if @super_programmer.nil?
Now, we know that given this *particular* search, it would never return false, but don't you find that pretty readable?
@Hal,
Ha ha :) I will agree that with in the context of return(), it does feel better; it's probably something that just needs some getting used to.
Ben,
One of the purposes of Ruby is to read as closely to English as possible. Your goal, in writing a Ruby program, is to be able to read it as easily and as quickly as you read English. That's why, for examples, Ruby does not force you to use various keyboard symbols in the syntax, and that's why it lets you things in multiple ways, including as one-liners. It's always up to you to choose the particular style that is easiest to read for any given part of your program.
As an example, Ruby does not require parentheses to call a method because, in Ruby, calling a method should read like an English sentence - and English sentences don't use parentheses to denote the direct object. While you might use
when writing in Computerese, English is written as
As another example, English permits us to qualify sentences with appended subordinate clauses such as if, unless, while, and until. Ruby permits the same -
so that you have the freedom to write your code somewhat like you would write a sentence in English. Now, it would be very awkward to append a subordinate clause to a sequence of clauses, in both English and in Ruby - so we typically use this convenience, in both English and in Ruby, only in simple cases.
A few minor points:
Strings with double-quotes are interpolated, not evaluated - there is no evaluation going on. Rather, the Ruby interpreter takes your nice clean code
and, under the hood, simply pretends it was originally written as
The square-brackets notation for creating arrays is known as the array literal notation, rather than the implicit array notation. Nothing is implicit in square-brackets notation - rather, because we are using syntax to create an array rather than using methods, it is called the literal notation of creating an array. Likewise,
is a numeric literal while
is not.
Regular-expression literal notation (regular expressions using the slashes, e.g.
) are far older than Ruby and JavaScript. I don't know the history, but the notation is used in Perl quite a lot.
The three-clause
is, like
, rather low-level: Computerese, rather than English. If you're writing in Ruby because you like writing in English, then you probably would not be using the three-clause
anyway. (Granted, Ruby does have its own share of low-level, and even supremely awkward, constructs. Don't use them: instead, just try to write in natural English.)
Cheers!
Hal,
If you want to go even more idiomatically Ruby, you don't even need to use "if abc.nil?" - you can use "unless abc" (so long as abc won't be a Boolean value).
Cheers!
Ben,
Looks like my comment to you got ... formatted.
Any chance you've got support for inline code formatting that will just format code, inline with the rest of the text, in a monospace font, rather than turning it into its own separate paragraph?
Cheers!
@Justice,
I think there's is definitely going to be an adjustment period going from a more structures language to a more lenient language. I love to use semi-colons and parenthesis and double-quotes. But, that doesn't necessarily mean that they are better - it probably just means that is what I am used to.
I can logically accept that it's nice to read something in English; but, when you're not used to it, I think you tend to read code with the computerese mentality.
Joseph actually just made some similar points about the Io language:
www.bennadel.com/blog/2064-Seven-Languages-In-Seven-Weeks-Io-Day-1.htm
Another example of me wanting to use semi-colons when it seems to be patently not what the language actually wanted.
Thanks for the terminology points; I definitely do play a little fast a loose with my terms. Please know that that is not out of laziness - it's simply that I don't necessarily understand the distinction. Take "interpolate" - I definitely use the term "evaluate" to just mean that something works a certain way at run time. I know, not the best definition :) I'm still working this all out, so in all sincerity, I really do appreciate the terminology help.
Sorry about the inline code formatting. I had just assumed that this would only be used with "blocks" of code. Any thoughts on how to delimit inline and block level code? What about something like an "icode" tag for an inline code tag?
As a final thought, since part of my exploration is trying to learn about language of which I know nothing, I do try to bend them to my pre-conceived notions of what a "good" language allows for. It is very possible that that is ultimately limiting in my ability to embrace the goodness that a language has to offer. I shall try to open my mind.
@Justice,
Quick follow up on "implicit". I definitely use that term in ColdFusion all the time. Looking at the LiveDocs, it looks like I have been using it wrong; I tend to think of "implicit arrays" - but, it looks like the LiveDocs refer to "implicit creation" of arrays... not "implicit arrays." So, again, thanks for the feedback.
Ben,
StackOverflow and GitHub use backticks to denote inline code. Example:
Or, you could just treat any regular code tag-pair where the beginning tag is immediately preceded by a newline and the ending tag is immediately followed by a newline as a block code tag; otherwise, an inline code tag.
Cheers!
@Justice,
I think I'll go with the CODE+NewLine approach. I have a deep-seated dislike of the ` character. Something about it just makes me aggravated. You have to use that character to escape keywords in MySQL. That's probably why I don't like it. You'll probably realize that I'm an emotional coder with syntax hang-ups :)