Even and Odd Integers

Would be interesting to see how many of the x % 2 == 1 are behaving as expected if the use case is generalized to handle negative xs.

People need to learn about the common pitfall of using % without being aware of the (at least three) different variants and which one is implemented in the language (see eg Swift’s vs Python’s %, using negative lhs).

(I don’t have a strong opinion about whether isEven should be added or not, but I do think Swift’s std lib should provide the alternative of flooring division quotient and remainder, since Swift’s % (truncating) is practically useless for negative lhs.)

2 Likes

AFAICT 100% of the uses in the compat suite were on integers that would only ever be positive (like array indices, count from enumerated(), arc4random()).

1 Like

Oh, except for SwifterSwift's definition of isOdd itself, which was correct.

Thank you very much @Ben_Cohen for taking the time to research and write that. I really appreciate the feedback.

I’d like to go through the criteria and try to make a stronger case in a few key areas.

Commonality: One issue I’ve had with finding supporting examples is that % 2 == 0 isn’t very search engine friendly (if anyone has a trick, please let me know!). For example, Github searching the apple/swift repo yields zero results, but doing a local text search of the cloned repo yields 63 results. Most results are in test and benchmark but I would still consider this to be “real-world” code.

A few other examples in apple/swift include:

// Codable.swift.gyb
guard count % 2 == 0 else { throw DecodingError.dataCorrupted ... }

// Reverse.swift: documentation for public let base: Base.Index
// guard let i = reversedNumbers.firstIndex(where: { $0 % 2 == 0 })

// Deserialization.cpp
assert(e % 2 == 0 && "malformed default witness table");

There are also a few for % 2 == 1 which are potentially bugs for negative integers.

// RangeReplaceableCollection.swift: documentation for removeAll(where:)
// numbers.removeAll(where: { $0 % 2 == 1 })

// AnyHashable.swift.gyb. 
if (lhs % 2 == 1 || rhs % 2 == 1) && (lhs / 8 == rhs / 8) {

& 1 == 0 is also used in a few apple/swift tests and Bool.swift:

// Bool.swift: random<T: RandomNumberGenerator>
return (generator.next() >> 17) & 1 == 0

Other examples based on searches of my local repositories:

Some really real-world examples:

I’d also like to push back on discounting example/sample code usage. This type of code is often written by a more experienced developer for a much larger but less experienced audience. A single blog post, playground, book, or tweet-sized snippet can be read thousands of times, which I think legitimately qualifies as real-world usage, even if it doesn’t end up in a production software product.

Readability: Criteria met.

Not trivially composable: This functionality is trivial to implement, once you’ve dug around in the Swift integer protocol hierarchy to figure out that BinaryInteger is the appropriate protocol to extend.

There is one case where they aren’t trivially composable: the sample/education usage. In this context, it’s usually not appropriate for an author to introduce this functionality (unless they are teaching extensions!) in order to avoid distracting from the main task at hand (e.g. filter, map, etc). It may also be the same situation for authoring test code: it'd be used if it were there but it's not worth the overhead of defining it manually.

The “trivial inverse” issue isn’t something I have a great answer for, other than there is precedence for this in standard libraries of other languages (Ruby, Haskell, Clojure, and according to RosettaCode: Julia, Racket, Scheme, Smalltalk, Common Lisp).

Sufficiently general: isDivisible(by:) is certainly more general than isEven/isOdd, and % is more general than isDivisible(by:).

A very preliminary survey seems to indicate that % 2 == 0 is used significantly more often than other divisors.
Apple/swift:

  • % [0-9]+ == 0: 105
  • % 2 == 0 uses: 63 (60%)

My workspace of projects and cloned repos (not including apple/swift)

  • % [0-9]+ == 0 uses: 240
  • % 2 == 0 uses: 192 (80%)

Perhaps this somewhat justifies the specificity. Also, even and odd numbers have had shorthand labels for thousands of years.

Discoverability: .isEven/.isOdd are certainly more discoverable than % == 0 and % != 0. I completely agree that learning about % was and still may be a necessity, just as learning c-style for-loops probably was for many of us earlier on in our developer lives. The great thing about Swift is that it opens up new and better ways of expressing an idea, such as a method on an integer type, that weren't possible before.

Performance traps: N/A

Discourages/encourages misuse: Discourages potential misuse of % in the case of negative dividends (see next) when determining oddness.

Correctness traps: % 2 == 1 for negative dividends is a potential source of bugs and confusion. -3 % 2 evaluates to -1 in Swift; the same expression evaluates to 1 in Ruby and Python, for example, due to differences in the semantics of %. There is CERT C coding standard warning about this style of problem due to implementation-defined behaviour in earlier C versions. Modern polyglot developers still need to worry about the same problem though!

Can be implemented more efficiently in the std lib: Not really.

I know this is going to be a tough sell, especially to an audience that is likely quite experienced, but I think that uses for isEven/isOdd are frequent enough and offer a big enough improvement to justify the (small) weight they add to the standard library.

7 Likes

My point was that testing for oddness by writing x % 2 == 1 rather than x % 2 != 0 makes it look like the author has an incomplete understanding of the semantics of %, and should be considered bad practice (even in cases where x would only ever be positive).

If this bad practice is common enough, it's an indication that % is not a "very fairly basic feature" and isOdd is not "trivially composable".

4 Likes

x % 2 == 1 tests that x is both positive and odd all in one go, which is entirely legitimate when the only expected inputs are positive. In no way is such testing of one's assumptions (for free, performance-wise!) bad practice.

I don't agree with this specific statement. I am only speaking in defense of isDivisible(by:) but divisibility is a component of the clearly-not-quite-so-clear semantics of % isDivisible(by:) would be a great addition.

Another one: It's pretty common to do odd/even striping of tables. In fact, it's so common that CSS has three syntaxes for its :nth-child selector: :nth-child(odd), :nth-child(even), and :nth-child(2n+1).

7 Likes

I am very strongly in favor of adding isDivisible(by:). I am on the fence about isOdd or isEven, but I figure they would get enough use in UI code that they are probably worth being there for beginners/convenience.

1 Like

<bikeshed>
It should be isEvenlyDivisible(by:), because 10 is divisible by 3. Just not evenly divisible.
</bikeshed>

Edited to add:

I'd be in favor of adding isOdd and isEven because:

1️⃣ they increase readability and therefore expressivity

2️⃣ It makes mapping or ternary logic simpler which increases readability and therefore expressivity

view.backgroundColor = row.isEven ? .white : .lightGray

vs

view.backgroundColor = (row % 2 == 0) ? .white : .lightGray // extra parentheses yuck
2 Likes

That's not standard usage, at least not in Number Theory (see e.g. here).

2 Likes

And are we seriously expecting that users of Swift will be well-versed in number theory in order to understand this distinction? Or could we help them understand what the method is actually doing by giving it a nice, descriptive name?

4 Likes

I think "is divisible" is descriptive enough. I've never heard anyone say that 10 is divisible by 3 (in the context of integers). Your comment seemed to imply that "10 is not divisible by 3" was technically wrong somehow, which is not true.

11 Likes

If I came across a method on an integer that was isDivisible(by:), I would assume it would return true for everything except zero, because nothing is divisible by zero, because division by zero is undefined.

100 is divisible by 42. 1 is divisible by 2. π is divisible by e. etc. I learned this all the way back when I was six or seven years old.

Then along comes Swift and tells me that what I've known for decades is wrong?

I get that there's this motivation to be as pedantically correct as possible. But we also need to account for general human understanding, and the intricacies of number theory and "what division actually means" is not general knowledge. Let's make this language forgiving and name things in such a way that we are teaching people what goes on, instead of making them wonder "wtf why doesn't this work like I expect it to"

ETA: 10 is absolutely divisible by 3, in the context of integers. I get back 3.

1 Like

The problem with that is that you first have to figure out who that "general human" is and what they actually understand. It might not be what either one of us two understand.

That said, I don't think it's too much to ask of people that they should learn what a method they want to use does. It's not like we rename "map" because people might think it has something to do with Apple Maps.

(Also "the intricacies of number theory" != "literally the first thing about Number Theory". Should we replace the type "Int" with "WholeNumberWithLimitedRange", too?)

5 Likes

I understand where you're coming from but I think "divisible" is generally considered to mean "able to be divided without a remainder".

Oxford definition for divisible (mathematics): (of a number) containing another number a number of times without a remainder. ‘24 is divisible by 4’

Wolfram also has a divisible function. "Divisible[n,m] is effectively equivalent to Mod[n,m]==0."

16 Likes

Type “is divisible by” (with quotes) into your favorite search engine. When I did, I found many high-ranked hits on pages clearly intended for a general audience, even a grade-school audience, not mathematicians. All the pages I checked clearly define “is divisible by” as equivalent to “leaves zero remainder when divided by”. It is unfair to say that definition demands the reader be “well-versed in number theory in order to understand”.

I'm not saying “is evenly divisible by” isn't also common usage. It is. But I personally don't like that phrasing, because it's got “even” embedded in it, which also means “divisible by two”, so why muddy the waters? If you want to be pedantic, “is exactly divisible by” avoids overloaded terms.

But I think “is divisible by” is generally understood to mean “leaves zero remainder when divided by”, and will be understood that way by most Swift users, regardless of mathematical background.

15 Likes

Iff we really wanted a different name to avoid possible ambiguity that I don't completely agree is there then I would suggest hasFactor(_:) (we shouldn't have to pronounce Integer, at least, because types)

At some point this reduction reaches absurdity. We have to assume some mathematical preliminaries. Can we assume users know what "+" means, or do we need to spell that out too? Swift integers don't add like normal integers; should it be x.addingOrPossiblyTrapping(y) instead? "is divisible by" is a totally standard phrase in semi-technical English, and easily searched for if anyone is confused.

4 Likes

As a non-native English speaker I immediately understand isDivisible(by:), but when I would come across isEvenlyDivisible(by:), I'd probably google for 'evenly divisible' to find out what that means.

2 Likes