swift-numerics: Usage of "unsafe" does not match Swift’s standards

In the standard library, Swift uses “unsafe” to denote operations that are memory-unsafe. This sets an expectation which I think we should respect and follow.

Currently, Complex has a member named unsafeLengthSquared. This operation is memory-safe, so in accordance with Swift’s established precedent it should not have “unsafe” in its name.

I propose renaming it to lengthSquared, and documenting that the calculation could overflow (or underflow).

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 22 (21 by maintainers)

Most upvoted comments

@natecook1000, in a slack, nails how to follow numerics tradition and avoid stomping swift usage: “Okay, you can call this unsafLengthSquared” 😂

I’m going to rename this property lengthSquared. This will land together with some documentation work early next week (WIP here: https://github.com/stephentyrone/swift-numerics/tree/lengthSquared). unsafeLengthSquared will be marked unavailable and renamed.

It is constant time for any fixed-width floating-point type, which is 99.9999% of use for this operation (that’s an under-estimate). For the remaining .0001% of uses, it is fast enough that the difference is not observable except in the finest microbenchmark. Having it be a computed property is correct.

This API already has a means to indicate failure (zero or infinity is returned)–so throwing adds some small overhead for no additional benefit. Here’s what a typical “careful” use case looks like (from divide):

let lengthSquared = w.unsafeLengthSquared
guard lengthSquared.isNormal else { return rescaledDivide(z, w) }
return z * (w.conjugate.divided(by: lengthSquared))

Making it throw instead wouldn’t really simplify this at all. Making it optional would simplify it, just the tiniest bit:

guard let lengthSquared = w.unsafeLengthSquared else {
  return rescaledDivide(z, w)
}
return z * (w.conjugate.divided(by: lengthSquared))

However, the primary use of an API like this in contexts where the programmer can simply assume that their data will be well scaled (like many graphics workloads), so there’s no “fallback” path. In those contexts, affordances for the “failure” path just add noise. If it’s an optional, most uses will just force unwrap it. If it throws, most uses end up decorated with try blocks with no meaningful error recovery path. This use pattern is also why unsafe is somewhat appropriate–in typical usage, if the data is out of range, who knows what happens? It’s not undefined, but it’s likely to either be unspecified or not to matter to the broader algorithm.