Ruby / Foundations: values, methods, text, and valid objects / Lesson 2
Write conditions using Ruby truthiness and expression results
Use explicit predicates, if and case results, bounded iteration, and clear Boolean precedence for archive rules.
On this page
Lesson goals
- Explain why zero and empty objects are truthy while nil and false are not.
- Choose branch order and range endpoints from the actual policy.
Only nil and false are falsey
Ruby conditions accept objects and interpret only nil and false as falsey. Zero, an empty string, and an empty array are truthy, so testing a count directly does not mean count is positive and testing a string does not mean it contains text. State the intended predicate with comparisons or emptiness checks. This differs from several familiar languages and can silently change a ported rule without a syntax error. Keep absence, false as a meaningful setting, zero quantity, and empty content distinct when the domain needs those distinctions.
Make the truthiness boundary visible
The script classifies fixed values without printing their potentially confusing representations. The ternary expression chooses one label from the condition’s truthiness. It does not convert an empty collection into false or validate its elements. A predicate method conventionally ends in ?, but the name alone does not force the implementation to return only true or false. Design callers around the documented result contract. No Ruby interpreter was installed in the authoring environment, so these labels are expected results rather than a transcript from a real run.
[nil, false, 0, '', []].each do |value|
puts(value ? 'truthy' : 'falsey')
end
falsey
falsey
truthy
truthy
truthy
Expected lines are falsey, falsey, truthy, truthy, truthy. Empty and zero values remain present objects. The source was not executed.
Branches return values as well as control execution
if, unless, and case are expressions. Assigning their result can replace several mutations of a temporary output variable. A branch’s last evaluated expression supplies its value, so an added logging call can accidentally change a method’s result. elsif conditions are tested in order. case uses each when condition’s === behavior, which can support ranges or classes rather than only simple equality. Avoid clever nested unless forms with multiple negatives; a direct positive predicate is often easier to review, especially when a policy has several distinct rejection reasons.
Put narrow cases before broader alternatives
The example handles negative and zero counts explicitly before using a range for small positive groups. The inclusive range 1..3 contains three, while 1...3 excludes it. A range is an object with methods and matching behavior, not a promise that every possible input supports meaningful iteration. Use each when only elements matter; use an index only when position is part of the algorithm. Reading an out-of-range array position can return nil, so an indexing mistake may become a later NoMethodError instead of an immediate boundary exception.
def count_label(count)
if count < 0
'invalid'
elsif count == 0
'empty'
else
case count
when 1..3 then 'small'
else 'large'
end
end
end
[-1, 0, 2, 4].each { |count| puts count_label(count) }
invalid
empty
small
large
Expected output is invalid, empty, small, large. Negative values never reach the range classification. This standalone script was not run.
Choose Boolean operators for clear grouping
&& and || have different precedence from the low-precedence and and or keywords. Combining assignment and these operators can change which value is assigned even when the expression looks similar. Prefer explicit parentheses or a separate if when grouping is not obvious. ||= also treats an existing false value as needing replacement, so it is not a universal initialize-only-if-absent operation. For a Boolean setting where false is meaningful, distinguish key absence or nil explicitly. Read short-circuit behavior before placing side effects on either side of a Boolean operator.
Common errors and fixes
Zero is accepted by a supposed positive-count check.
- Why it happens
- The code tested the object’s truthiness instead of count > 0.
- How to fix it
- Write the domain predicate explicitly.
- Prevent it next time
- Test negative, zero, and positive values separately.
A saved false setting is replaced by a default.
- Why it happens
- ||= conflated false with absence.
- How to fix it
- Check nil or key presence according to the setting contract.
- Prevent it next time
- Include false as a meaningful configured value in checks.
A method begins returning a log result after a small edit.
- Why it happens
- The last evaluated expression changed.
- How to fix it
- Make the intended return value explicit or restore its final position.
- Prevent it next time
- Review return behavior whenever adding side effects to expression branches.
Check your understanding
1. Why is if title insufficient to require nonempty title text?
Show worked solution
An empty string is truthy, and a non-string object can also pass. Validate the required type and content policy, including whether whitespace-only text is allowed. Keep a missing title distinct from a blank one when callers need different rejection reasons. Ruby’s permissive truthiness does not replace a domain boundary.
2. What boundary cases distinguish 1..3 from 1...3 in a classification?
Show worked solution
The value three is included only by the first range. Check one, two, three, and a neighboring outside value so the intended endpoint is documented. Do not infer the policy from a handful of values far from the boundary. A concise range expression is correct only when its inclusion rule matches the requirement.
Key takeaways
- Truthiness, emptiness, and domain validity are different questions.
- Branch results, matching semantics, and operator precedence affect observable behavior.