Notes on numerics in Swift

Numeric types in Foundation

Foundation.Decimal

Available on Apple platforms and on Linux, Decimal is a Foundation value type; on Apple platforms, it bridges the NSDecimalNumber class. The binary representation of values is the same across platforms and does not align with any decimal floating-point representation defined in the IEEE 754-2008 standard, since NSDecimalNumber itself pre-dates that standard and has been available since Mac OS X 10.0. Each value requires 160 bits of memory, and the stored properties of Decimal are defined in swift-corelibs-foundation as follows:

public struct Decimal {
    /* ... */
    fileprivate var __exponent: Int8
    fileprivate var __lengthAndFlags: UInt8
    fileprivate var __reserved: UInt16
    /* ... */
    public var _mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)
    /* ... */
}

The purpose of this discussion is not to rehash the existing documentation but to place this type in the context of Swift’s other numeric types and protocols.

Many methods available on Float and Double are also available on Decimal. However, Decimal does not and cannot conform to FloatingPoint because it does not adhere to all of the requirements of that protocol (which align with IEEE 754 requirements). For example, Decimal has no representation for negative zero or infinity.

Facilities uniquely available for Decimal values are those that perform arithmetic operations using a specified rounding mode (Decimal.RoundingMode) and return a result signaling loss of precision, overflow, underflow, or other errors (Decimal.CalculationError).

(A rounding mode to fit a result to a given precision is not necessarily the same as a rounding rule for rounding a value to the nearest integer. Recall that Swift provides no way to use a dynamic rounding mode for calculations involving binary floating-point types, and that Swift provides no way to interrogate the global flags that signal binary floating-point exceptions.)

Facilities not yet available on Decimal include instance methods such as addingProduct(_:_:), remainder(dividingBy:), truncatingRemainder(dividingBy:), squareRoot(), and rounded(_:); and static methods such as minimum(_:_:) and maximum(_:_:).

Because the significand of a Decimal value is represented differently than that of a binary floating-point value, the unit in the last place (or ulp) of a Decimal value is not equivalent to the distance between itself and the nearest representable value greater in magnitude, and the properties nextUp and nextDown consequently do not behave as documented at present:

import Foundation

(10 as Decimal).ulp.description    // "10"
(10 as Decimal).nextUp.description // "20"

Note that addition and subtraction of Decimal values produced erroneous results on Linux prior to Swift 4.1.3.

Float literals (redux)

As previously discussed, initalization of any type conforming to ExpressibleByFloatLiteral from a float literal involves first initializing a value of type _MaxBuiltinFloatType, which is a type alias for Float80 if supported and Double otherwise, and then converting that value to the desired type.

Since _MaxBuiltinFloatType is a binary floating-point type, a decimal floating-point type that conforms to the protocol ExpressibleByFloatLiteral cannot distinguish between two values that have the same binary floating-point representation when rounded to fit _MaxBuiltinFloatType:

import Foundation

(0.1 as Decimal).description
// "0.1"
(0.10000000000000001 as Decimal).description
// "0.1"

Decimal(string: "0.1")!.description
// "0.1"
Decimal(string: "0.10000000000000001")!.description
// "0.10000000000000001"

Foundation.NSNumber

Available on Apple platforms and on Linux, NSNumber is a Foundation reference type that wraps (or “boxes”) C numeric values. It is a subclass of NSValue and bridges the Core Foundation types CFNumber and CFBoolean both on Apple platforms and on Linux; its instances are always immutable.

The underlying reason for the existence of such a wrapper type is grounded in Objective-C:

Objective-C has a divide between objects and non-objects…. [N]on-objects are everything that comes from C, from the integer 42 to the string "Hello, world" to complicated structs. Boxing is the process of placing these non-objects into an object so that they can be used like other objects, typically so that they can be placed in a collection. NSNumber is the [Foundation] class used to box C numbers. You can’t have an NSArray of int, but you can have an NSArray of NSNumber. NSNumber shows up a lot in Cocoa programming.

— Mike Ash, Jul. 6, 2012

NSNumber provides, in addition to boxing functionality for Boolean and numeric types, functionality to convert among these types. Since not all values are representable in all types, conversion is sometimes lossy, resulting in loss of precision or what Apple documentation calls “erroneous” results (which will be explained below):

import Foundation

let x = -42 as NSNumber
x.uintValue // 18446744073709551574

In Swift, NSNumber supports bridging using the dynamic cast operators (as?, as!, is) to and from Boolean and numeric types in addition to a handful of different converting initializers. The purpose of this discussion is principally to survey the behavior of these different ways of converting among numeric types via NSNumber. These conversions differ among themselves and from similarly named conversions between standard library types; moreover, their behavior has changed over time.

Note that support for NSNumber bridging using as?, as!, and is is available for Linux only in Swift 4.2+.

In Swift 3, an NSNumber instance created from a Swift value preserved the original type information and could be bridged using dynamic casting (on macOS) back to the original type, whereas an NSNumber instance created from Cocoa could be bridged using dynamic casting to any type for which the value is exactly representable.

The design was problematic for optimizations in Foundation and caused inconsistent behavior depending on the context in which an NSNumber instance was created. It was abandoned with adoption of SE-0170: NSNumber bridging and Numeric types, implemented in Swift 4.

Conversions among integer types

When a value source of integer type T is boxed into an NSNumber instance boxed, the following conversions are possible to an integer type U:

  1. boxed as? U
    Failable. Equivalent to U(exactly: boxed) and U(exactly: source).
    Converts the given value if it can be represented exactly as a value of type U.
    Otherwise, returns nil.

  2. U(exactly: boxed)
    Failable initializer. Equivalent to boxed as? U and U(exactly: source).

  3. U(truncating: boxed)
    Equivalent to boxed.{int|uint|int8...}Value and U(truncatingIfNeeded: {Int64|UInt64}(source)).
    Creates a new value of type U from the binary representation in memory of source (notionally).
    When T and U are not of the same bit width, the binary representation of source is truncated or sign-extended as necessary.

  4. boxed.{int|uint|int8...}Value
    Equivalent to U(truncating: boxed) and U(truncatingIfNeeded: {Int64|UInt64}(source)).

Conversions among binary floating-point types

When a value source of floating-point type T is boxed into an NSNumber instance boxed, the following conversions are possible to a floating-point type U:

  1. boxed as? U
    Failable. Not equivalent to U(exactly: boxed) and U(exactly: source).
    The result of an inexact conversion is either nil (if strict) or rounded to the nearest representable value (if lenient).
    The result of an overflowing conversion is nil.
    The result of an underflowing conversion is either nil (if strict) or zero (if lenient).
    The result of converting NaN is U.nan.

  2. U(exactly: boxed)
    Failable initializer. Equivalent to U(exactly: source).
    Converts the given value if it can be represented exactly as a value of type U; any result that is not nil can be converted back to a value of type T that compares equal to source.
    The result of an inexact conversion is nil.
    The result of an overflowing conversion is nil.
    The result of an underflowing conversion is nil.
    The result of converting NaN (however encoded) is nil, since NaN never compares equal to NaN.

  3. U(truncating: boxed)
    Equivalent to boxed.{float|double}Value and U(source).
    The result of an inexact conversion is rounded to the nearest representable value.
    The result of an overflowing conversion is infinite.
    The result of an underflowing conversion is zero.
    The result of converting NaN is some encoding of NaN that varies based on the underlying architecture; any signaling NaN is always converted to a quiet NaN.

  4. boxed.{float|double}Value
    Equivalent to U(truncating: boxed) and U(source).

Conversions between numeric types and Bool

When a value of numeric type is boxed into an NSNumber instance boxed, the following conversions are possible to Bool:

  1. boxed as? Bool
    Failable. Equivalent to Bool(exactly: boxed).
    Converts zero to false, one to true, and any other value to nil.

  2. Bool(exactly: boxed)
    Failable initializer. Equivalent to boxed as? Bool.

  3. Bool(truncating: boxed)
    Equivalent to boxed.boolValue.
    Converts zero to false and almost any other value to true; the one exception is that Int64.min and any value source for which (source as! NSNumber).int64Value == Int64.min are converted to false.

  4. boxed.boolValue
    Equivalent to Bool(truncating: boxed).

When a value of type Bool is boxed into an NSNumber instance boxed, the following conversions are possible to a numeric type U:

  1. boxed as? U
    Spelled as though failable but always succeeds. Equivalent to U(exactly: boxed).
    Converts false to zero and true to one.

  2. U(exactly: boxed)
    Spelled as failable initializer but always succeeds. Equivalent to boxed as? U.

  3. U(truncating: boxed)
    Equivalent to boxed.{int...float|double}Value.
    Converts false to zero and true to one.

  4. boxed.{int...float|double}Value
    Equivalent to U(truncating: boxed).

Conversions between integer types and binary floating-point types

Incomplete


Previous:
Concrete binary floating-point types, part 4

Next:
Numeric protocols

Draft: 3–5 August 2018
Updated 1 September 2018