Tuesday, February 1, 2011

Syntax Issues in Ruby

Still working through Chapter 3 of Beginning Ruby. It's slow going because every time I get to an example (and there are lots of examples) I have to stop and do it, then run it to see what it does. Then I try to change the code around using what I already know. Sometimes this works. I'm actually surprised at how often I can predict what will work in Ruby.

Sometimes it doesn't work. Take this bit of code for example. All it does is check a string for vowels and returns a result.

This code is valid:

x = "The quick brown fox jumped over the lazy dogs."
puts "This string has vowels" if x =~ /[aeiou]/

This code is not valid:

x = "The quick brown fox jumped over the lazy dogs."
if x =~ /[aeiou]/ puts "This string has vowels"

I don't get why this doesn't work. All I've done is rearranged the bits of code. It's possible I need a bit more code to make this work this way and it's possible it just doesn't work this way. It's also possible I'm just getting ahead of myself (again) but I've never been able to learn something, even the basics of something, without wanting to mess around with it and see what else it could do.

Either way, the example from the book didn't use a variable. In place of x the book had "Test string" which still returns the same result, "This string has vowels" but is less useful because a variable is more useful. If this was a bit of code I was actually going to use for something, I could pull that variable from anywhere - a file, user input, etc. So I don't have a problem understanding variables, at least.

7 comments:

  1. Great to hear you are learning Ruby. You'll love it.
    There are a number of ways to accomplish this in the order you are attempting (not sure what is going to happen to spacing in this code)...

    Using explicit newlines - This ain't sexy, but it's pretty darn clear.

    if x =~ /[aeiou]/
    puts "This string has vowels"
    end

    ----

    Using a Colon - A colon can be used in an if statement to separate the if from the statement. I never use this method and I can't think of many times I see others use it...though I'm probably not looking at enough code.

    if x =~ /[aeiou]/: puts "This string has vowels" end

    ----

    Using a Semicolon - A semicolon allows you to separate statement in Ruby. Why not just use a newline though? Note that the second semicolon is not actually necessary.

    if x =~ /[aeiou]/; puts "This string has vowels"; end

    ----

    Ternary Operator - I ternary evaluates the expression. If true, it returns the first value. If false, the second. I would only use a ternary on occasions in which I feel it makes the code cleaner. Some would argue that occasion never happens. This particular example is not a good time to use it.

    x =~ /[aeiou]/ ? "This string has vowels" : nil

    ----

    Anyway, the first example you noted that does work is a method you will see very often. Which one you choose will depend on the context and whatever makes the code easier to read. Good luck!

    ReplyDelete
  2. Thanks for the reply! Syntax is hard for me to wrap my head around some times, especially since I don't want to learn just one way of doing things.

    I appreciate the examples you posted, too. I'll work with those until that neural pathway is forged somewhere in my brain and it starts to make sense.

    ReplyDelete
  3. The reason this syntax error exists, is because Ruby lets you put any statement that returns a value directly after an if. For example, you could do this:

    if puts "hi"
      puts "puts statement was true"
    else
      puts "puts statement was false"
    end

    In Ruby almost every statement returns a value. puts is an interesting example as it always returns nil, so that code above has a block of code that will never be called.

    The point remains though, Ruby is expecting that this line, shown now without an if statement, is executable:

    x =~ /[aeiou]/ puts "This string has vowels"

    Run this, and you'll see you get the exact same syntax error. What's happening here is that Ruby sees multiple spaces between arguments, without any idea of what to do with them. Ruby expects methods to look like this

    method argument
    method argument, argument, argument

    Which it sees as identical to the following form, which more clearly shows the problem:

    method(argument)
    method(argument, argument, argument)

    But instead, you've given it something that could be interpreted multiple ways:

    object.method argument argument argument
    # or
    x =~ /[aeiou]/ puts "This string has vowels"

    # x =~ is a bit of a special case, let's assume it means object.method

    Let's pretend Ruby could understand this bit of code, what might it see?

    object.method; method(argument)
    object.method(method(argument))
    object.method(method, argument)
    # or
    x.=~(/[aeiou]/); puts("This string has vowels")
    x.=~(/[aeiou]/, puts("This string has vowels"))
    x.=~(/[aeiou]/, puts, "This string has vowels")

    Don't mind what's happening to x on the left side there. What is important is happening at the puts statement. Is the string an argument to puts, or is it an argument to the regex matcher? Should the return value of puts be passed to the regex matching function as the second argument?

    At first glance, the last two interpretations above might seem like really strange things to do. In the case we're using a puts statement, they truly are, but this style of taking the value of one thing, and passing it as an option to another is very often repeated just like this. So you can see why Ruby barfs on this input, it does not have enough clues to understand your intent and it wants to say so.

    Important lessons:
    * Almost every statement returns a value in Ruby
    * Any value can be passed as an argument to a method
    * Any value can be used as a condition to an if statement

    ReplyDelete
  4. It's funny that I read your comment this morning and didn't reply because I had a hard time understanding it. I don't know if it's because I've worked a bit further in Beginning Ruby, but I understand what you're saying more easily now.

    I was feeling a bit overwhelmed earlier this week, but it's getting easier. Parts of it, anyway, and the rest will come with practice and repetition.

    ReplyDelete
  5. Another way to put the condition first is with && or 'and' (which are different operators, and I assume the book will get to them).

    So, instead of

    puts "This string has vowels" if x =~ /[aeiou]/

    you could do

    x =~ /aeiou/ && puts "This string has vowels"

    or

    x =~ /aeiou/ and puts "This string has vowels"

    Usually, if you're just trying to get the condition in front, 'and' is OK. If you're doing assignment, && is better.

    For example:

    obj = do_something_that_might_return_nil()
    x = obj && obj.value
    y = obj and obj.value

    x will be nil or obj.value. y will always be obj.

    ReplyDelete
  6. @Matt

    I tried to post this comment twice already, but firefox wasn't having it. So here's my question again:

    Does using and / && perform a check in the same way that this bit of code does?

    puts "This string has vowels" if x =~ /[aeiou]/

    Here we have a conditional with if x =~ /[aeiou]/

    Does && work in the same way that ==, in that it will perform a check and return "This string has vowels" ?

    ReplyDelete
  7. Don't use and, it is has some confusing edge cases that are not worth troubling yourself with yet(or ever). Just use &&

    && (and other Rubyisms like it) perform short-circuit checking that very similar to an if statement. In this example:

    false && some_method

    some_method will never be called, because the logical ANDing of false always produces false, so the value of some_method is irrelevant.

    Now:

    true && some_method

    some_method will always be called, and it will be the return value, because in this case the true is irrelevant, the truthiness of that statement is entirely up to the return value of some_method.

    ReplyDelete