Skip to content

Commit b337468

Browse files
committed
perf(polymorphic): drop the intermediate Result array in release builds
Address CodeRabbit feedback on #29: the lossy element helper built a full [Result] array and callers built a second array via compactMap, doubling transient storage in release builds where per-element results are unobservable. Return a LossyPolymorphicElements value instead, which carries the element array always and the per-element results only in DEBUG builds, so release decodes a lossy array with a single collection allocation. Behavior is unchanged in both configurations. Includes house-style formatting (trailing commas, redundant self) that the format hook applied to the touched files. Claude-Session: https://claude.ai/code/session_017kn3Hf2khxiFy1TqimTGx9
1 parent c2283fa commit b337468

3 files changed

Lines changed: 53 additions & 26 deletions

File tree

Sources/KarrotCodableKit/PolymorphicCodable/Extensions/UnkeyedDecodingContainer+LossyPolymorphicElements.swift

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,49 @@
88

99
import Foundation
1010

11+
/// The outcome of a lossy polymorphic array pass. Per-element results are retained only in
12+
/// DEBUG builds, so release builds allocate a single element array and no `Result` storage.
13+
struct LossyPolymorphicElements<Element> {
14+
let elements: [Element]
15+
#if DEBUG
16+
let results: [Result<Element, Error>]
17+
#endif
18+
}
19+
1120
extension UnkeyedDecodingContainer {
12-
/// Decodes every element with the polymorphic `strategy`, capturing each element's result
13-
/// instead of throwing, so lossy array wrappers can keep valid elements and record failures.
14-
mutating func decodeLossyPolymorphicElementResults<Strategy: PolymorphicCodableStrategy>(
21+
/// Decodes every element with the polymorphic `strategy`, skipping elements that fail to
22+
/// decode instead of throwing, so lossy array wrappers can keep the valid elements.
23+
/// In DEBUG builds each element's `Result` is also captured for resilient decoding outcomes.
24+
mutating func decodeLossyPolymorphicElements<Strategy: PolymorphicCodableStrategy>(
1525
of strategy: Strategy.Type
16-
) throws -> [Result<Strategy.ExpectedType, Error>] {
26+
) throws -> LossyPolymorphicElements<Strategy.ExpectedType> {
27+
var elements = [Strategy.ExpectedType]()
28+
#if DEBUG
1729
var results = [Result<Strategy.ExpectedType, Error>]()
30+
#endif
1831

1932
while !isAtEnd {
2033
// Decoding through the element's super decoder always advances the container,
2134
// even when the element fails to decode.
2235
let elementDecoder = try superDecoder()
2336
do {
24-
try results.append(.success(PolymorphicValue<Strategy>(from: elementDecoder).wrappedValue))
37+
let value = try PolymorphicValue<Strategy>(from: elementDecoder).wrappedValue
38+
elements.append(value)
39+
#if DEBUG
40+
results.append(.success(value))
41+
#endif
2542
} catch {
2643
#if DEBUG
2744
elementDecoder.reportError(error)
28-
#endif
2945
results.append(.failure(error))
46+
#endif
3047
}
3148
}
3249

33-
return results
50+
#if DEBUG
51+
return LossyPolymorphicElements(elements: elements, results: results)
52+
#else
53+
return LossyPolymorphicElements(elements: elements)
54+
#endif
3455
}
3556
}

Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public struct OptionalPolymorphicLossyArrayValue<PolymorphicType: PolymorphicCod
6666
init(
6767
wrappedValue: [PolymorphicType.ExpectedType]?,
6868
outcome: ResilientDecodingOutcome,
69-
results: [Result<PolymorphicType.ExpectedType, Error>] = []
69+
results: [Result<PolymorphicType.ExpectedType, Error>] = [],
7070
) {
7171
self.wrappedValue = wrappedValue
7272
self.outcome = outcome
@@ -97,18 +97,21 @@ extension OptionalPolymorphicLossyArrayValue: Decodable {
9797

9898
do {
9999
var container = try decoder.unkeyedContainer()
100-
let results = try container.decodeLossyPolymorphicElementResults(of: PolymorphicType.self)
101-
let elements = results.compactMap(\.success)
100+
let decoded = try container.decodeLossyPolymorphicElements(of: PolymorphicType.self)
102101

103102
#if DEBUG
104-
if results.contains(where: \.isFailure) {
105-
let error = ResilientDecodingOutcome.ArrayDecodingError(results: results)
106-
self.init(wrappedValue: elements, outcome: .recoveredFrom(error, wasReported: false), results: results)
103+
if decoded.results.contains(where: \.isFailure) {
104+
let error = ResilientDecodingOutcome.ArrayDecodingError(results: decoded.results)
105+
self.init(
106+
wrappedValue: decoded.elements,
107+
outcome: .recoveredFrom(error, wasReported: false),
108+
results: decoded.results,
109+
)
107110
} else {
108-
self.init(wrappedValue: elements, outcome: .decodedSuccessfully, results: results)
111+
self.init(wrappedValue: decoded.elements, outcome: .decodedSuccessfully, results: decoded.results)
109112
}
110113
#else
111-
self.init(wrappedValue: elements, outcome: .decodedSuccessfully)
114+
self.init(wrappedValue: decoded.elements, outcome: .decodedSuccessfully)
112115
#endif
113116
} catch {
114117
// Same policy as `PolymorphicLossyArrayValue`: an invalid array-level value recovers to `[]`.

Sources/KarrotCodableKit/PolymorphicCodable/PolymorphicLossyArrayValue.swift

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ public struct PolymorphicLossyArrayValue<PolymorphicType: PolymorphicCodableStra
6060

6161
public init(wrappedValue: [PolymorphicType.ExpectedType]) {
6262
self.wrappedValue = wrappedValue
63-
self.outcome = .decodedSuccessfully
63+
outcome = .decodedSuccessfully
6464
#if DEBUG
65-
self.results = []
65+
results = []
6666
#endif
6767
}
6868

6969
#if DEBUG
7070
init(
7171
wrappedValue: [PolymorphicType.ExpectedType],
7272
outcome: ResilientDecodingOutcome,
73-
results: [Result<PolymorphicType.ExpectedType, Error>] = []
73+
results: [Result<PolymorphicType.ExpectedType, Error>] = [],
7474
) {
7575
self.wrappedValue = wrappedValue
7676
self.outcome = outcome
@@ -98,7 +98,7 @@ extension PolymorphicLossyArrayValue: Decodable {
9898
#if DEBUG
9999
let context = DecodingError.Context(
100100
codingPath: decoder.codingPath,
101-
debugDescription: "Value was nil but property is non-optional"
101+
debugDescription: "Value was nil but property is non-optional",
102102
)
103103
let error = DecodingError.valueNotFound([PolymorphicType.ExpectedType].self, context)
104104
decoder.reportError(error)
@@ -111,18 +111,21 @@ extension PolymorphicLossyArrayValue: Decodable {
111111

112112
do {
113113
var container = try decoder.unkeyedContainer()
114-
let results = try container.decodeLossyPolymorphicElementResults(of: PolymorphicType.self)
115-
let elements = results.compactMap(\.success)
114+
let decoded = try container.decodeLossyPolymorphicElements(of: PolymorphicType.self)
116115

117116
#if DEBUG
118-
if results.contains(where: \.isFailure) {
119-
let error = ResilientDecodingOutcome.ArrayDecodingError(results: results)
120-
self.init(wrappedValue: elements, outcome: .recoveredFrom(error, wasReported: false), results: results)
117+
if decoded.results.contains(where: \.isFailure) {
118+
let error = ResilientDecodingOutcome.ArrayDecodingError(results: decoded.results)
119+
self.init(
120+
wrappedValue: decoded.elements,
121+
outcome: .recoveredFrom(error, wasReported: false),
122+
results: decoded.results,
123+
)
121124
} else {
122-
self.init(wrappedValue: elements, outcome: .decodedSuccessfully, results: results)
125+
self.init(wrappedValue: decoded.elements, outcome: .decodedSuccessfully, results: decoded.results)
123126
}
124127
#else
125-
self.init(wrappedValue: elements)
128+
self.init(wrappedValue: decoded.elements)
126129
#endif
127130
} catch {
128131
// An invalid array-level value (e.g., not an array) recovers to an empty array.

0 commit comments

Comments
 (0)