Even and Odd Integers

Thanks for all the great feedback everybody. Overall the reaction seems pretty positive. I’ll try to sum up the discussion so far.

isDivisible

A couple of people brought up isDivisible(by n: Self) -> Bool as an alternative and/or addition to isEven/isOdd. I think this could be a useful addition to BinaryInteger but it probably deserves a fuller discussion with regards to the motivation criteria. It is certainly more general but in my opinion it loses some expressiveness and discoverability.

buttonSave.isEnabled = photos.count.isOdd
buttonSave.isEnabled = !photos.count.isDivisible(by: 2)

I’m not familiar with the details, but it looks like there is decent support for key path getter promotion which would make working with properties like isEven/isOdd in a point-free fashion quite nice vs a more general isDivisible(by:).

xs.filter(\.isEven)

I will include it in the Alternatives section of the proposal but if you feel very strongly about it, it may be worth starting a separate pitch thread for it!

Property declaration on protocol vs extension

It does seem like isEven/isOdd should be defined directly on the BinaryInteger protocol and not on an extension. Is there any argument against doing that?

More examples of isEven/isOdd in the wild

There are some more great examples of the % operator being used incorrectly to determine parity, but it’d be great to get a few more examples of how you’ve used custom isEven/isOdd properties or functions in your own code (or examples of code where you would have used it had it been available).

Digging through the archives

This one is more for fun. I was able to track down the Ruby Change Request (RCR) for Ruby’s even? and odd? methods all the way back from 2006 using the web archive. The proposal was short and the vote tally was positive, but it was interesting to see odd? proposed as self % 2 == 1 which would be incorrect for negative integers in Swift due to differences in the % operator implementations.

3 Likes

I don't think anyone (at least not me) was proposing isDivisible(by:) instead of isEven/isOdd, but rather as an addition. The reasoning for me is as follows:

  • I'm a mathematician and I work on a product that deals with maths, so divisibility just seems like a very natural thing for me to have (different people might have different needs, though).
  • The concept of divisibility leads to modular arithmetic which a lot of people probably use without realising it. :slight_smile: If you work with clock times for example, you're basically working mod 24 (or mod 12), etc.
  • It might make sense to implement isEven in terms of isDivisible(by:), at least unless this would lead to performance issues (can't judge that)
  • I don't think isDivisible(by:) is not discoverable. If you don't know what divisibility is, you won't reach for it, but if you do, it seems like the most obvious way to call it; also, the moment I see that the language has isEven/isOdd, I would wonder whether there is also something more general, after all, what's so special about the number 2? :wink:
  • Additionally, the modulo operators are basically (mathematically) broken in about every language I know of, because they don't deal with negative numbers correctly. Most annoyingly, Haskell has two modulo operators, and they're both broken (in different ways); short of introducing a proper mod function (and teaching people to use that instead of %), adding isDivisible(by:) (if implemented properly) would at least mitigate some of the issues.

About the key path getter promotion thing: Yes, you're right. IMHO, it's unfortunate that Swift has a distinction between computed properties and methods (in Ruby, e.g., there is no difference, everything is a method). If this didn't exist, and if there was an easy way to partially apply functions, then the point free style would work with isDivisible(by:), too, for example in Haskell:

isDivisible d n = ...
filter (isDivisible 2) xs
4 Likes

I read Steve's comment as more in favour of isDivisible instead of isEven / isOdd.

In the proposal, I'll modify the Alternatives title to Alternatives & Possible Additions :slight_smile:

You're right, the discoverability argument in my previous post isn't very strong. I originally had a blurb about the naming of the function - I think it would likely end up being divisible(by:) - but I didn't want to get too sidetracked. I think discoverability is mostly equivalent if the is prefix is there but I don't believe there are many examples of Boolean functions in the standard library or foundation that are prefixed like that. Even if the prefix is dropped, discoverability is only impacted slightly, though.

I think this is a pretty important point and has been brought up in the discussion of 'Double modulo' operator. I would personally be more inclined to introduce properly named functions for remainder and mod/modulo and discourage the use of % with linter rules in my own projects, rather than adding divisible(by:) in an attempt to mitigate some of the issues with %. I could still see there being good reasons for including divisible, it'd just need to be pitched and evaluated more thoroughly, in my opinion.

1 Like

I read Steve's comment as more in favour of isDivisible instead of isEven / isOdd.

Right, I don't think isEven / isOdd pulls it's weight as API. It's commonly used, but mainly because it's makes a simple example that's accessible to almost everyone. Is there any other language provide these as stdlib operations?

2 Likes

Ruby does on Integer. Class: Integer (Ruby 2.5.1)

edit:
Clojure too odd? - clojure.core | ClojureDocs - Community-Powered Clojure Documentation and Examples
Haskell (Prelude) Prelude

2 Likes

I don't feel like the case has been made for adding these yet – as it stands, they don't seem to me to clear the bar for addition to the standard library.

To run down the usual list of criteria:

  • commonality: as mentioned above, it's common to want to define these for example code (for example, to demonstrate what a higher-order function is). But I'm not sure the specific need to check a number is even/odd comes up all that often in real-world code. One of the examples given, of only enabling saving when there are an odd number of photos, suggests real-world use cases are pretty thin. See below.
  • readability: the key motivation, so meets this criteria.
  • not trivially composable: this is clearly trivially composable. Note also, this also trips over this when you consider the proposal is for both isEven and isOdd. We don't, as a rule, have trivial inverses (for example, we have isEmpty but not notEmpty – you're expected to write !isEmpty.
  • sufficiently general: it fails on this front, per the discussion of isDivisible(by:).
  • discoverability: Discoverability is mainly about operations that are hard to figure out how to achieve, and about deciding once an addition is justified what the most discoverable spelling for that operation is, not about giving a name to every possible composable operation. People really need to learn about % – it's a very fairly basic feature.
  • discourages/encourages misuse: doesn't discourage any existing possible misuse AFAICT. I've seen some potential misuse e.g. arc4random().isEven instead of Bool.random() now that we have that, but this is probably minor.
  • performance traps: I don't think this applies. There's no easily stumbled-over inefficient way to do this.
  • correctness traps: I don't think this applies. There's no edge cases involved here.
  • can be implemented more efficiently in the std lib For trivial types at least, this isn't the case. For more complex types, there may be a case for isDivisible(by:).

So the key question, assuming we live with the trivial composability is how often this operation is useful in practice. Searching the compatibility suite is probably a good guide here. This isn't always a valid thing to do—for example, if something is less common, but hard to implement or get right, it might belong in the std lib even though it doesn't come up. But for something trivially composable it would definitely need to appear multiple times in a sample like this to be justified.

Looking through the compatibility suite, I do see a fair number of uses of % 2 == 0/1. However, of the 20 projects where the pattern appears:

  • 9 are used as an arbitrary test of for producing a bool, mainly for testing higher-order functions
  • 4 are replicating Bool.random()
  • 3 define isEven/Odd, but don't use it except as above

The main uses I could spot where they were genuine tests of even/oddness were:

  • 3 are validating hex digits are paired as part of an init?(_:String) – seems like we should add this feature!
  • 1 was using it to intersperse a space every two hex digits
  • 1 was using it to alternate between two sequences
  • 1 was using it to thin out data for testing
  • 1 was using it as an inefficient way to pair elements of a sequence
  • 1 was using it to thin test input

(note, some projects did more than one so numbers sum to > 20 :slight_smile:)

Now, the compatibility suite is not necessarily representative of all Swift code. For example, it has very little UI code, and % 2 may perhaps appear often as a way of alternating colors in a UITableView. But based on this data, I'd say the justification isn't there given how trivially composable the equivalent is.

6 Likes

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:

:one: they increase readability and therefore expressivity

:two: 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