Iggy
Iggy

Reputation: 5251

Ruby's variable equals if variable equals method?

I am reading through createyourlang book and saw a code snippet that looks like this:

  tokenizer = if identifier = chunk[IDENTIFIER, 1]
                IdentifierTokenizer.new(identifier, tokenizer).tokenize
              elsif constant = chunk[CONSTANT, 1]
                ConstantTokenizer.new(constant, tokenizer).tokenize
              elsif number = chunk[NUMBER, 1]
                ...

I find it confusing having two equal signs on the same line. What does it mean to have A = if B = C?

If you're wondering what chunk is, assume chunk is string "hello" and chunk[IDENTIFIER,1] equals to "hello" and the other chunk[OTHER_CONST,1] equals to nil. The function works. You can find the source code repo here. I am mainly curious how to read this function/ if there is a better way to rewrite this code to make it more readable?

Upvotes: 0

Views: 1079

Answers (1)

Cary Swoveland
Cary Swoveland

Reputation: 110675

Yes, it can be confusing, in part because one's first reaction to seeing if identifier = chunk[IDENTIFIER, 1] may be that it's probably a bug, that the author meant if identifier == chunk[IDENTIFIER, 1].

The code block given in the question is probably equivalent to:

tokenizer =
  if chunk[IDENTIFIER, 1]
    IdentifierTokenizer.new(chunk[IDENTIFIER, 1], tokenizer).tokenize
  elsif chunk[CONSTANT, 1]
    ConstantTokenizer.new(chunk[CONSTANT, 1], tokenizer).tokenize
  elsif chunk[NUMBER, 1]
    NumberTokenizer.new(chunk[NUMBER, 1], tokenizer).tokenize
  ..

Rather than computing chunk[IDENTIFIER, 1], chunk[CONSTANT, 1] and chunk[NUMBER, 1] twice, those values are assigned to variables (identifier, constant, number) the first time they are calculated. (Those assignments are evaluated before if and elsif are applied.) This is commonly done when such calculations are costly or have an undesired side effect when performed more than once. The use of temporary variables in this way also DRYs the code, but other approaches that achieve the same objective may be preferable. I personally avoid such inline variable assignments, in part because I regard them as ugly.

One alternative that may be more clear, and which DRYs the code even more, is the following.

tokenizer =
  tokenit(IDENTIFIER, IdentifierTokenize, tokenizer) ||
  tokenit(CONSTANT,   ConstantTokenize,   tokenizer) ||
  tokenit(NUMBER,     NumberTokenize,     tokenizer) ||
  ...

def tokenit(type, class, tokenizer)
  ch = chunk[type, 1]
  if ch
    class.public_send(:new, ch, tokenizer).tokenize
  else
    false
  end
end

This of course assumes that when ch is truthy class.public_send(:new, ch, tokenizer).tokenize is also truthy.

Upvotes: 6

Related Questions