A PR for applying ameba linter rules in the shards repo sparked a discussion about the semantics of any?
vs. !empty?
for collections.
The original discussion is in this comment thread:
I think this topic warrants its own discussion in the broader community.
The discussion was sparked by the ameba rule Performance/AnyInsteadOfEmpty
which claims that calls to any?
should be replaced by negating the result of empty?
.
Enuemrable#any?
returns true
if at least one of the collection’s members is truthy whereas Enumerable#empty?
returns false
if there is at least on member in the collection, regardless of whether it’s truthy.
The vast majority of use cases cares about the plain existence of any item in the collection and doesn’t care about truthiness. In fact, truthiness doesn’t matter at all when the item type cannot be falsey. That’s the case when it does not include Nil
, Bool
, or Pointer
- those are the only types that can have falsey values - and means that any?
and !empty?
are exactly equivalent.
(Note: I think the categorization of that rule is wrong. It’s primarily about semantics, performance is only an additional aspect and actually irrelevant because there’s no performance difference when the item type cannot be falsey - and if it can, there’s a difference in semantics and performance doesn’t matter.)
(Note 2: We’re only talking about the #any?
overload that doesn’t take any arguments or blocks. The other variants have specific semantics that are unrelated to this discussion.)
I care a lot about code readability.
!empty?
is a double negation. Resolving that to a positive predicate reduces cognitive load. When called with a receiver, the additional visual distance between the method name and the prefixed negation operator further increases cognitive load.
The diff from that original discussion is a good showcase:
- spec.dependencies.any? || (Shards.with_development? && spec.development_dependencies.any?)
+ !spec.dependencies.empty? || (Shards.with_development? && !spec.development_dependencies.empty?)
I find the original expression much easier to comprehend than the suggested alternative.
The semantics in this case are identical: the type of spec.depdendencies
is Array(Dependency)
, so it cannot have any falsey values.
So for this example, I see no good reason to change this code to a less readable version for no other gain.
This thoughtbot article was brought up to underline the preference of !empty?
over any?
I completely agree to the article’s sentiment for Ruby. But Crystal is different. Typed collections mean that in most cases any?
is already semantically exactly equivalent to !empty?
. And what’s important: the item type precisely attests that. So the type of the collection tells whether it could possibly contain any falsey item and determining that does not require to actually iterate the collection as it is in Ruby.
Collections including boolean values or pointers are relatively rare in the first place. The main application of falsey item values in a collection is nil
. That applies to both Crystal and Ruby (the latter doesn’t even have pointers) but in Crystal collections with nilable item types are much less common due to static typing: If a type is nilable, you have to explicitly handle that. Thus it’s common to get rid of nil
values as early as possible to prevent the compiler constantly bugging about it.
So we see far less nil
values appear in Crystal collections than in Ruby.[1]
And remember, in Crystal when the item type cannot be falsey, any? == !empty?
applies.
Maybe it would help to clear things up if any?
had exactly the same meaning as !empty?
. That would require to go the extra mile for the alternative and less commonly used meaning of “has any element that’s not falsey” with an explicit any?(&.itself)
or a new method like any_truthy?
.
A possible compromise could also be to concede the name of any?
to retain the current semantics, but introduce another method that’s exactly equivalent !empty?
. I can’t think of a good concise name for that, though.
-
Doing the same in Ruby would probably be a good idea, it’s just that nobody bugs you about it. ↩︎