Do "Optional.reduce" and "Bool.reduce" have common names?

I feel like everybody needs this stuff all the time. Maybe I'm missing something that's already in the standard library?

public extension Optional {
  /// Modify a wrapped value if not `nil`.
  /// - Parameters:
  ///   - makeResult: arguments: (`resultWhenNil`, `self!`)
  /// - Returns: An unmodified value, when `nil`.
  func reduce<Result>(
    _ resultWhenNil: Result,
    _ makeResult: (_ resultWhenNil: Result, _ self: Wrapped) throws -> Result
  ) rethrows -> Result {
    try map { try makeResult(resultWhenNil, $0) }
    ?? resultWhenNil
  }
public extension Bool {
  /// Modify a value if `true`.
  /// - Returns: An unmodified value, when false.
  @inlinable func reduce<Result>(
    _ resultWhenFalse: Result,
    _ makeResult: (_ resultWhenFalse: Result) throws -> Result
  ) rethrows -> Result {
    self
    ? try makeResult(resultWhenFalse)
    : resultWhenFalse
  }
}
var int: Int? = nil
XCTAssertEqual(int.reduce(1, +), 1)

int = 2
XCTAssertEqual(int.reduce(1, +), 3)


var optionalArray: [Int]? = nil
let array: [Int] = [1, 2]

XCTAssertEqual(
  optionalArray.reduce(array) { $1 + $0 },
  array
)

optionalArray = [0]
XCTAssertEqual(
  optionalArray.reduce(array) { $1 + $0 },
  [0, 1, 2]
)
var isSnackTime = false

XCTAssertEqual(
  isSnackTime.reduce("🐈", "🐹".appending),
  "🐈"
)

isSnackTime = true
XCTAssertEqual(
  isSnackTime.reduce("🐈") { "🏃 \($0)" },
  "🏃 🐈"
)

I usually use ?? with a value that would make a correct result after modification, for example

int.reduce(1, +)

would be

(int ?? 0) + 1
1 Like

I've seen that a lot. And it works, a lot, because there's often an identity value to work with, like "", 0, or 1.

What about the Bool case though? I usually just see people copy a portion of a string :nauseated_face:, or use an inline closure that matches the extension method above.

var foo = "🐈"
if isSnackTime {
  foo = "🐹" + foo
}

But then foo isn't a constant.

2 Likes

Maybe something like this:

let trueValue = "🐈"
let value = isSnackTime ? trueValue : "🏃 \(trueValue)"

Yes, that's what I mean by

let value: String = {
  let resultWhenFalse = "🐈"
  return
    isSnackTime
    ? "🏃 \(resultWhenFalse)"
    : resultWhenFalse
} ()

The scope of the false-result doesn't need to be at the same level as the result.

No, none that I know of.

I guess the reason I never thought of Bool.reduce is because it's not super common to have the "false" case be a subset of the "true" case, and also because there's not an obvious reason to privilege "true" over "false". The Optional case comes up more but I usually try to go for @cukr's solution instead, leaving the cases where that doesn't work rare enough that I don't feel the need to define a helper.

(I also personally wouldn't pick the name reduce for the Bool case because it's not removing any wrapping layer, but I see the parallel in the "how many times is the function applied".)

1 Like

Fair enough.

But why would we get this built-in…

CollectionOfOne(isSnackTime).reduce("🐈") { $1 ? "🏃 \($0)" : $0 }

…yet not this?

Optional(isSnackTime).reduce("🐈") { $1 ? "🏃 \($0)" : $0 }

Reasonable. Please let us know if you figure out the right name!

There's no reduce specifically defined on CollectionOfOne; it's fallout from being a Collection (technically Sequence). We certainly wouldn't add it if it weren't present, since it is useless. But the general question stands: why have, say, map on both Collections and Optionals, but not reduce?

I think a big part of the answer is that it really isn't a common operation if you're not trying to chain everything in one expression, and even then, it's only useful if your "nil" value is relevant to your "non-nil" value, which I think is pretty uncommon. The fully general pattern I'd use for this is foo.map { 1 + $0 } ?? 1 (i.e. your implementation). Is foo.reduce(1, +) simpler than that? Well, yes, but it's also more restrictive—it comes out worse for foo.map { 1 + $0 } ?? 0. (Specifically, foo.reduce(0) { $1 + 1 }) That certainly doesn't mean "reduce shouldn't be added", but if it's not going to be the right tool for the job very often, does it belong in the standard library?

…But on the other hand, what harm would it do?

FWIW, the only other thread I've found on this is one you participated in: Add Optional.filter to the Standard Library - #41 by gwendal.roue. I'm personally of the opinion that (a) Optional is not a Collection, but also that (b) that shouldn't stop us from adding the Collection-like operations (monadic, foldable, whatever) that make sense on Optional.*

* Whether there should be, say, a Foldable protocol is a separate issue that unfortunately gets into binary compatibility problems.

I wish it were! But it's not, because closures can't be applied to values, using dot syntax, unless they're Sequences or Optionals. So people come up with stuff like this:

…which is more easily served by:

public extension Sequence {
  /// `reduce`, disregarding all sequence elements.
  @inlinable func reduce<Result>(
    _ initialResult: Result,
    _ nextPartialResult: (_ partialResult: Result) throws -> Result
  ) rethrows -> Result {
    try reduce(initialResult) { partialResult, _ in
      try nextPartialResult(partialResult)
    }
  }

  /// `reduce`, disregarding all sequence elements.
  @inlinable func reduce<Result>(
    into initialResult: Result,
    _ updateAccumulatingResult: (_ partialResult: inout Result) throws -> Void
  ) rethrows -> Result {
    try reduce(into: initialResult) { partialResult, _ in
      try updateAccumulatingResult(&partialResult)
    }
  }

…along with:

public extension CollectionOfOne where Element == Never? {
  /// A collection whose element is irrelevant.
  init() {
    self.init(nil)
  }
}

…allowing for the useful hackery:

CollectionOfOne().reduce(into: "🐈") {
  if isSnackTime {
    $0 = "🏃 \($0)"
  }
}

…and:

CollectionOfOne().reduce("🐈") { isSnackTime ? "🏃 \($0)" : $0 }

…which is just this, but more pragmatic because of code completion.

{ isSnackTime ? "🏃 \($0)" : $0 } ("🐈")

I mean, yes, this is the hidden premise behind a lot of these threads. Maybe the answer is "there should be a way to put methods on every type (extension Any)" and maybe the answer is "yeah, you don't need to put everything in one expression". In either case, though, Optional and CollectionOfOne aren't intended to be the answer to this problem; either we should have a way to put methods on every type or we shouldn't.

2 Likes

SwiftUI is partially an experiment to test those waters, I think. They're nice! :ocean::surfing_man:

But there are other possibilities, if we can get a standard library boxing type. :gift::boxing_glove:

struct Struct {
  @Wrapped var value = "🐈"
}

Struct().$value { "🏃 \($0)" }
Struct().$value.if(isSnackTime) { "🏃 \($0)" }
@propertyWrapper struct Wrapped<Value> {
  let wrappedValue: Value
  var projectedValue: Self { self }
}

extension Wrapped {
  typealias Transform = (Value) throws -> Value

  func callAsFunction(transform: Transform) rethrows -> Value {
    try transform(wrappedValue)
  }

  func `if`(_ condition: Bool, transform: Transform) rethrows -> Value {
    condition
      ? try transform(wrappedValue)
      : wrappedValue
  }
}

…just make the $ do what the ¢ does here, and remove the need for parentheses.

prefix operator ¢
prefix func ¢<Value>(value: Value) -> Wrapped<Value> {
  .init(wrappedValue: value)
}

(¢"🐈") { "🏃 \($0)" }
(¢"🐈").if(isSnackTime) { "🏃 \($0)" }
1 Like