A PR for applying ameba linter rules in the shards repo sparked a discussion about the semantics of
!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
true if at least one of the collection’s members is truthy whereas
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
Pointer - those are the only types that can have falsey values - and means that
!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
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
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.
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
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. ↩︎