|
| 1 | +# Make `Numeric` Refine a new `AdditiveArithmetic` Protocol |
| 2 | + |
| 3 | +* Proposal: TBD |
| 4 | +* Author: [Richard Wei](https://github.com/rxwei) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting review** |
| 7 | +* Implementation: [apple/swift#19989](https://github.com/apple/swift/pull/19989) |
| 8 | + |
| 9 | +## Introduction |
| 10 | + |
| 11 | +This proposal introduces a weakening of the existing `Numeric` protocol named `AdditiveArithmetic` , which defines additive arithmetic operators and a zero, making conforming types roughly correspond to the mathematic notion of an [additive group](https://en.wikipedia.org/wiki/Additive_group). This makes it possible for vector types to share additive arithmetic operators with scalar types, which enables generic algorithms over `AdditiveArithmetic` to apply to both scalars and vectors. |
| 12 | + |
| 13 | +Discussion thread: [Should Numeric not refine ExpressibleByIntegerLiteral](https://forums.swift.org/t/should-numeric-not-refine-expressiblebyintegerliteral/15106) |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +The `Numeric` protocol today refines `ExpressibleByIntegerLiteral` and defines all arithmetic operators. The design makes it easy for scalar types to adopt arithmetic operators, but makes it hard for vector types to adopt arithmetic operators by conforming to this protocol. |
| 18 | + |
| 19 | +What's wrong with `Numeric`? Assuming that we need to conform to `Numeric` to get basic arithmetic operators and generic algorithms, we have three problems. |
| 20 | + |
| 21 | +### 1. Vectors conforming to `Numeric` would be mathematically incorrect. |
| 22 | + |
| 23 | +`Numeric` roughly corresponds to a [ring](https://en.wikipedia.org/wiki/Ring_(mathematics)). Vector spaces are not rings. Multiplication is not defined between vectors. Requirements `*` and `*=` below would make vector types inconsistent with the mathematical definition. |
| 24 | + |
| 25 | +``` |
| 26 | +static func * (lhs: Self, rhs: Self) -> Self |
| 27 | +static func *= (lhs: inout Self, rhs: Self) |
| 28 | +``` |
| 29 | + |
| 30 | +### 2. Literal conversion is undefined for dynamically shaped vectors. |
| 31 | + |
| 32 | +Vectors can be dynamically shaped, in which case the the shape needs to be provided when we initialize a vector from a scalar. Dynamically shaped vector types often have an initializer `init(repeating:shape:)`. |
| 33 | + |
| 34 | +Conforming to `Numeric` requires a conformance to `ExpressibleByIntegerLiteral`, which requires `init(integerLiteral:)`. However, the conversion from a scalar to a dynamically shaped vector is not defined when there is no given shape. |
| 35 | + |
| 36 | +```swift |
| 37 | +struct Vector<Scalar: Numeric>: Numeric { |
| 38 | + // Okay! |
| 39 | + init(repeating: Scalar, shape: [Int]) { ... } |
| 40 | + |
| 41 | + // What's the shape? |
| 42 | + init(integerLiteral: Int) |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +### 3. Common operator overloading causes type checking ambiguity. |
| 47 | + |
| 48 | +Vector types mathematically represent [vector spaces](https://en.wikipedia.org/wiki/Vector_space). Vectors by definition do not have multiplication between each other, but they come with scalar multiplication. |
| 49 | + |
| 50 | +```swift |
| 51 | +static func * (lhs: Vector, rhs: Scalar) -> Vector { ... } |
| 52 | +``` |
| 53 | + |
| 54 | +By established convention in numerical computing communities such as machine learning, many libraries define a multiplication operator `*` between vectors as element-wise multiplication. Given that scalar multiplication has to exist by definition, element-wise multiplication and scalar multiplication would overload the `*` operator. |
| 55 | + |
| 56 | +```swift |
| 57 | +static func * (lhs: Vector, rhs: Vector) -> Vector { ... } |
| 58 | +static func * (lhs: Vector, rhs: Scalar) -> Vector { ... } |
| 59 | +``` |
| 60 | + |
| 61 | +This compiles, but does not work in practice. The following trivial use case would fail to compile, because literal `1` can be implicitly converted to both a `Scalar` and a `Vector` , and `*` is overloaded for both `Vector` and `Scalar` . |
| 62 | + |
| 63 | +```swift |
| 64 | +let x = Vector<Int>(...) |
| 65 | +x * 1 // Ambiguous! Can be both `x * Vector(integerLiteral: 1)` and `x * (1 as Int)`. |
| 66 | +``` |
| 67 | + |
| 68 | +## Proposed solution |
| 69 | + |
| 70 | +We keep `Numeric` 's behavior and requirements intact, and introduce a new protocol that |
| 71 | +- does not require `ExpressibleByIntegerLiteral` conformance, and |
| 72 | +- shares common properties and operators between vectors and scalars. |
| 73 | + |
| 74 | +To achieve these, we can try to find a mathematical concept that is close enough to makes practical sense without depending on unnecessary algebraic abstractions. This concept is additive group, containing a zero and all additive operators that are defined on `Numeric` today. `Numeric` will refine this new protocol, and vector types/protocols will conform to/refine the new protocol as well. |
| 75 | + |
| 76 | +## Detailed design |
| 77 | + |
| 78 | +We define a new protocol called `AdditiveArithmetic` . This protocol requires all additive arithmetic operators that today's `Numeric` requires, and a zero. Zero is a fundamental property of an additive group. |
| 79 | + |
| 80 | +```swift |
| 81 | +public protocol AdditiveArithmetic: Equatable { |
| 82 | + /// A zero value. |
| 83 | + static var zero: Self { get } |
| 84 | + |
| 85 | + /// Adds two values and produces their sum. |
| 86 | + /// |
| 87 | + /// The addition operator (`+`) calculates the sum of its two arguments. For |
| 88 | + /// example: |
| 89 | + /// |
| 90 | + /// 1 + 2 // 3 |
| 91 | + /// -10 + 15 // 5 |
| 92 | + /// -15 + -5 // -20 |
| 93 | + /// 21.5 + 3.25 // 24.75 |
| 94 | + /// |
| 95 | + /// You cannot use `+` with arguments of different types. To add values of |
| 96 | + /// different types, convert one of the values to the other value's type. |
| 97 | + /// |
| 98 | + /// let x: Int8 = 21 |
| 99 | + /// let y: Int = 1000000 |
| 100 | + /// Int(x) + y // 1000021 |
| 101 | + /// |
| 102 | + /// - Parameters: |
| 103 | + /// - lhs: The first value to add. |
| 104 | + /// - rhs: The second value to add. |
| 105 | + static func + (lhs: Self, rhs: Self) -> Self |
| 106 | + |
| 107 | + /// Adds two values and stores the result in the left-hand-side variable. |
| 108 | + /// |
| 109 | + /// - Parameters: |
| 110 | + /// - lhs: The first value to add. |
| 111 | + /// - rhs: The second value to add. |
| 112 | + static func += (lhs: inout Self, rhs: Self) -> Self |
| 113 | + |
| 114 | + /// Subtracts one value from another and produces their difference. |
| 115 | + /// |
| 116 | + /// The subtraction operator (`-`) calculates the difference of its two |
| 117 | + /// arguments. For example: |
| 118 | + /// |
| 119 | + /// 8 - 3 // 5 |
| 120 | + /// -10 - 5 // -15 |
| 121 | + /// 100 - -5 // 105 |
| 122 | + /// 10.5 - 100.0 // -89.5 |
| 123 | + /// |
| 124 | + /// You cannot use `-` with arguments of different types. To subtract values |
| 125 | + /// of different types, convert one of the values to the other value's type. |
| 126 | + /// |
| 127 | + /// let x: UInt8 = 21 |
| 128 | + /// let y: UInt = 1000000 |
| 129 | + /// y - UInt(x) // 999979 |
| 130 | + /// |
| 131 | + /// - Parameters: |
| 132 | + /// - lhs: A numeric value. |
| 133 | + /// - rhs: The value to subtract from `lhs`. |
| 134 | + static func - (lhs: Self, rhs: Self) -> Self |
| 135 | + |
| 136 | + /// Subtracts the second value from the first and stores the difference in the |
| 137 | + /// left-hand-side variable. |
| 138 | + /// |
| 139 | + /// - Parameters: |
| 140 | + /// - lhs: A numeric value. |
| 141 | + /// - rhs: The value to subtract from `lhs`. |
| 142 | + static func -= (lhs: inout Self, rhs: Self) -> Self |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +Remove arithmetic operator requirements from `Numeric` , and make `Numeric` refine `AdditiveArithmetic` . |
| 147 | + |
| 148 | +```swift |
| 149 | +public protocol Numeric: AdditiveArithmetic, ExpressibleByIntegerLiteral { |
| 150 | + associatedtype Magnitude: Comparable, Numeric |
| 151 | + init?<T>(exactly source: T) where T : BinaryInteger |
| 152 | + var magnitude: Self.Magnitude { get } |
| 153 | + static func * (lhs: Self, rhs: Self) -> Self |
| 154 | + static func *= (lhs: inout Self, rhs: Self) -> Self |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +To make sure today's `Numeric` -conforming types do not have to define a `zero` , we provide an extension to `AdditiveArithmetic` constrained on `Self: ExpressibleByIntegerLiteral` . |
| 159 | + |
| 160 | +```swift |
| 161 | +extension AdditiveArithmetic where Self: ExpressibleByIntegerLiteral { |
| 162 | + public static var zero: Self { |
| 163 | + return 0 |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +In the existing standard library, prefix `+` is provided by an extension to |
| 169 | +`Numeric`. Since additive arithmetics are now defined on `AdditiveArithmetic`, |
| 170 | +we change this extension to apply to `AdditiveArithmetic`. |
| 171 | + |
| 172 | +```swift |
| 173 | +extension AdditiveArithmetic { |
| 174 | + /// Returns the given number unchanged. |
| 175 | + /// |
| 176 | + /// You can use the unary plus operator (`+`) to provide symmetry in your |
| 177 | + /// code for positive numbers when also using the unary minus operator. |
| 178 | + /// |
| 179 | + /// let x = -21 |
| 180 | + /// let y = +21 |
| 181 | + /// // x == -21 |
| 182 | + /// // y == 21 |
| 183 | + /// |
| 184 | + /// - Returns: The given argument without any changes. |
| 185 | + public static prefix func + (x: Self) -> Self { |
| 186 | + return x |
| 187 | + } |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +## Source compatibility |
| 192 | + |
| 193 | +The proposed change is fully source-compatible. |
| 194 | + |
| 195 | +## Effect on ABI stability |
| 196 | + |
| 197 | +The proposed change will affect the existing ABI of the standard library, because it changes the protocol hierarchy and protocol requirements. As such, this protocol must be considered before the Swift 5 branching date. |
| 198 | + |
| 199 | +## Effect on API resilience |
| 200 | + |
| 201 | +The proposed change will affect the existing ABI, and there is no way to make it not affect the ABI because it changes the protocol hierarchy and protocol requirements. |
| 202 | + |
| 203 | +## Alternatives considered |
| 204 | + |
| 205 | +1. Make `Numeric` no longer refine `ExpressibleByIntegerLiteral` and not introduce any new protocol. This can solve the type checking ambiguity problem in vector protocols, but will break existing code: Functions generic over `Numeric` may use integer literals for initialization. Plus, Steve Canon also pointed out that it is not mathematically accurate -- there's a canonical homomorphism from the integers to every ring with unity. Moreover, it makes sense for vector types to conform to `Numeric` to get arithmetic operators, but it is uncommon to make vectors, esp. fixed-rank vectors, be expressible by integer literal. |
| 206 | + |
| 207 | +2. On top of `AdditiveArithmetic`, add a `MultiplicativeArithmetic` protocol that refines `AdditiveArithmetic`, and make `Numeric` refine `MultiplicativeArithmetic`. This would be a natural extension to `AdditiveArithmetic`, but the practical benefit of this is unclear. |
| 208 | + |
| 209 | +3. Instead of a `zero` static computed property requirement, an `init()` could be used instead, and this would align well with Swift's preference for initializers. However, this would force conforming types to have an `init()`, which in some cases could be confusing or misleading. For example, it would be unclear whether `Matrix()` is creating a zero matrix or an identity matrix. Spelling it as `zero` eliminates that ambiguity. |
0 commit comments