if¶
An if
evaluates the given branch if its condition is truthy. Otherwise, it
evaluates the else
branch if present.
a = 1
if a > 0
a = 10
end
a # => 10
b = 1
if b > 2
b = 10
else
b = 20
end
b # => 20
elsif¶
To write a chain of if-else-if you use elsif
:
if some_condition
do_something
elsif some_other_condition
do_something_else
else
do_that
end
Variable types¶
After an if
, a variable’s type depends on the type of the expressions used in both branches.
a = 1
if some_condition
a = "hello"
else
a = true
end
# a : String | Bool
b = 1
if some_condition
b = "hello"
end
# b : Int32 | String
if some_condition
c = 1
else
c = "hello"
end
# c : Int32 | String
if some_condition
d = 1
end
# d : Int32 | Nil
Note that if a variable is declared inside one of the branches but not in the other one, at the end of the if
it will also contain the Nil
type.
Inside an if
's branch the type of a variable is the one it got assigned in that branch, or the one that it had before the branch if it was not reassigned:
a = 1
if some_condition
a = "hello"
# a : String
a.size
end
# a : String | Int32
That is, a variable’s type is the type of the last expression(s) assigned to it.
If one of the branches never reaches past the end of an if
, like in the case of a return
, next
, break
or raise
, that type is not considered at the end of the if
:
if some_condition
e = 1
else
e = "hello"
# e : String
return
end
# e : Int32
As a suffix¶
An if
can be written as an expression’s suffix:
a = 2 if some_condition
# The above is the same as:
if some_condition
a = 2
end
This sometimes leads to code that is more natural to read.
As an expression¶
The value of an if
is the value of the last expression found in each of its branches:
a = if 2 > 1
3
else
4
end
a # => 3
If an if
branch is empty, or it’s missing, it’s considered as if it had nil
in it:
if 1 > 2
3
end
# The above is the same as:
if 1 > 2
3
else
nil
end
# Another example:
if 1 > 2
else
3
end
# The above is the same as:
if 1 > 2
nil
else
3
end
Ternary if¶
The ternary if
allows writing an if
in a shorter way:
a = 1 > 2 ? 3 : 4
# The above is the same as:
a = if 1 > 2
3
else
4
end
if var¶
If a variable is the condition of an if
, inside the then
branch the variable will be considered as not having the Nil
type:
a = some_condition ? nil : 3
# a is Int32 or Nil
if a
# Since the only way to get here is if a is truthy,
# a can't be nil. So here a is Int32.
a.abs
end
This also applies when a variable is assigned in an if
's condition:
if a = some_expression
# here a is not nil
end
This logic also applies if there are ands (&&
) in the condition:
if a && b
# here both a and b are guaranteed not to be Nil
end
Here, the right-hand side of the &&
expression is also guaranteed to have a
as not Nil
.
Of course, reassigning a variable inside the then
branch makes that variable have a new type based on the expression assigned.
Limitations¶
The above logic works only for local variables. It doesn’t work with instance variables, class variables, or variables bound in a closure. The value of these kinds of variables could potentially be affected by another fiber after the condition was checked, rendering it nil
. It also does not work with constants.
if @a
# here `@a` can be nil
end
if @@a
# here `@@a` can be nil
end
a = nil
closure = ->{ a = "foo" }
if a
# here `a` can be nil
end
This can be circumvented by assigning the value to a new local variable:
if a = @a
# here `a` can't be nil
end
Another option is to use Object#try
found in the standard library which only executes the block if the value is not nil
:
@a.try do |a|
# here `a` can't be nil
end
Method calls¶
That logic also doesn't work with proc and method calls, including getters and properties, because nilable (or, more generally, union-typed) procs and methods aren't guaranteed to return the same more-specific type on two successive calls.
if method # first call to a method that can return Int32 or Nil
# here we know that the first call did not return Nil
method # second call can still return Int32 or Nil
end
The techniques described above for instance variables will also work for proc and method calls.
if var.is_a?(...)¶
If an if
's condition is an is_a?
test, the type of a variable is guaranteed to be restricted by that type in the then
branch.
if a.is_a?(String)
# here a is a String
end
if b.is_a?(Number)
# here b is a Number
end
Additionally, in the else
branch the type of the variable is guaranteed to not be restricted by that type:
a = some_condition ? 1 : "hello"
# a : Int32 | String
if a.is_a?(Number)
# a : Int32
else
# a : String
end
Note that you can use any type as an is_a?
test, like abstract classes and modules.
The above also works if there are ands (&&
) in the condition:
if a.is_a?(String) && b.is_a?(Number)
# here a is a String and b is a Number
end
The above doesn’t work with instance variables or class variables. To work with these, first assign them to a variable:
if @a.is_a?(String)
# here @a is not guaranteed to be a String
end
a = @a
if a.is_a?(String)
# here a is guaranteed to be a String
end
# A bit shorter:
if (a = @a).is_a?(String)
# here a is guaranteed to be a String
end
if var.responds_to?(...)¶
If an if
's condition is a responds_to?
test, in the then
branch the type of a variable is guaranteed to be restricted to the types that respond to that method:
if a.responds_to?(:abs)
# here a's type will be reduced to those responding to the 'abs' method
end
Additionally, in the else
branch the type of the variable is guaranteed to be restricted to the types that don’t respond to that method:
a = some_condition ? 1 : "hello"
# a : Int32 | String
if a.responds_to?(:abs)
# here a will be Int32, since Int32#abs exists but String#abs doesn't
else
# here a will be String
end
The above doesn’t work with instance variables or class variables. To work with these, first assign them to a variable:
if @a.responds_to?(:abs)
# here @a is not guaranteed to respond to `abs`
end
a = @a
if a.responds_to?(:abs)
# here a is guaranteed to respond to `abs`
end
# A bit shorter:
if (a = @a).responds_to?(:abs)
# here a is guaranteed to respond to `abs`
end
if var.nil?¶
If an if
's condition is var.nil?
then the type of var
in the then
branch is known by the compiler to be Nil
, and to be known as non-Nil
in the else
branch:
a = some_condition ? nil : 3
if a.nil?
# here a is Nil
else
# here a is Int32
end
if !¶
The !
operator returns a Bool
that results from negating the truthiness of a value.
When used in an if
in conjunction with a variable, is_a?
, responds_to?
or nil?
the compiler will restrict the types accordingly:
a = some_condition ? nil : 3
if !a
# here a is Nil because a is falsey in this branch
else
# here a is Int32, because a is truthy in this branch
end
b = some_condition ? 1 : "x"
if !b.is_a?(Int32)
# here b is String because it's not an Int32
end
unless¶
The unless
clause works exactly like if
but with negated condition. It matches if the condition is falsey.
unless x
is equivalent to if !x
.
unless some_condition
expression_when_falsey
else
expression_when_truthy
end
# The above is the same as:
if !some_condition
expression_when_falsey
else
expression_when_truthy
end
unless
does not have an equivalent to elsif
, but otherwise supports
the same features as if
, including suffix notation:
close_door unless door_closed?