Orion Edwards
Orion Edwards

Reputation: 123662

Ruby assign a variable or raise error if nil

In kotlin and C#, you can assign a variable, or else if the value is nil, you can throw an exception using the ?: and ?? operators.

For example, in C#:

var targetUrl = GetA() ?? throw new Exception("Missing A");
// alt
var targetUrl = GetA() ?? GetB() ?? throw new Exception("Missing A or B");

Is this possible in ruby? If so, how?

Basically, what I want to do is this

target_url = @maybe_a || @maybe_b || raise "either a or b must be assigned"

I'm aware I can do this

target_url = @maybe_a || @maybe_b
raise "either a or b must be assigned" unless target_url

but I'd like to do it in a single line if possible

Upvotes: 3

Views: 2635

Answers (4)

Stefan
Stefan

Reputation: 114248

Basically, what I want to do is this

target_url = @maybe_a || @maybe_b || raise "either a or b must be assigned"

You have to add parentheses to raise to make your code work:

x = a || b || raise("either a or b must be assigned")

It would be "more correct" to use the control-flow operator or instead of ||: (which makes the parentheses optional)

x = a || b or raise "either a or b must be assigned"

This is Perl's "do this or die" idiom which I think is clean and neat. It emphases the fact that raise doesn't provide a result for x – it's called solely for its side effect.

However, some argue that or / and are confusing and shouldn't be used at all. (see rubystyle.guide/#no-and-or-or)

A pattern heavily used by Rails is to have two methods, one without ! which doesn't provide error handling:

def a_or_b
  @maybe_a || @maybe_b
end

and one with ! which does:

def a_or_b!
  a_or_b || raise("either a or b must be assigned")
end

Then call it via:

target_url = a_or_b!

Upvotes: 3

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84443

Use Operator Precedence

Another way to do what you want with an extremely minimal code change is to use the lower-precedence or operator, which has a lower precedence than both || and =. For example:

# This is closest to what you want, but violates many style guides.
target_url = @maybe_a || @maybe_b or raise "either a or b must be assigned"

You can also wrap the logical line without changing how it works, e.g.:

# Same code as above, but wrapped for line length
# and to clarify & separate its expressions.
target_url = @maybe_a || @maybe_b or
  raise "either a or b must be assigned"

Either way, the code raises a RuntimeError exception as expected. Because of the precedence rules, no parentheses are necessary.

Please note that a number of style guides like this one will tell you to avoid the or operator altogether, or to use it only for flow control, because it is often the cause of subtle precedence bugs that can be hard to spot at a glance. With that said, the wrapped version is really just an inverted variation of my other answer, and is pretty easy to visually distinguish without the use of parentheses, especially with syntax highlighting enabled. Your mileage and style guide strictness will most certainly vary.

Upvotes: 2

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84443

Use Postfix Condition with Assignment Expression

Because most everything in Ruby evaluates to an expression, you can do this as a single logical line by using unless as a postfix conditional followed by an assignment expression. I've chosen to wrap the line to fit a reasonable line length, but feel free to make it a single line if you really want a "one-liner." For example:

raise "either a or b must be assigned" unless
  target_url = @maybe_a || @maybe_b

This will correctly raise RuntimeError, as you are expecting.

Auto-Vivification

Please note that this particular approach will auto-vivify @maybe_a and assign nil to it. It will also do the same for @maybe_b if @maybe_a evaluates as falsey. While handy, auto-vivification may trip you up later if you're relying on defined? to identify undefined variables elsewhere in your code. The pros and cons of this idiom will therefore depend on your broader intent, but it will certainly get the job done within the scope of the original question.

Upvotes: 0

eux
eux

Reputation: 3280

You could work around it with brackets:

(target_url = @maybe_a || @maybe_b) || raise("either a or b must be assigned")

Upvotes: 1

Related Questions