1

Im looking through examples were you can use reduce to sum an array, but mostly finding examples with numbers. How would you add up all Bar that are inside Foo that has isAvailable set to true using reduce()?

Would you prefer to write it as I've done it? (readability & efficiency in mind)

struct Foo {
    var isAvailable: Bool
    var bars: [Bar]
}

struct Bar {
    var name: String
}

let array = [
    Foo(isAvailable: true, bars: [ Bar(name: "Bill"), Bar(name: "Donald") ]),
    Foo(isAvailable: false, bars: [ Bar(name: "Barack"), Bar(name: "Joe") ]),
    Foo(isAvailable: true, bars: [ Bar(name: "George"), Bar(name: "Ronald") ])
]


// Do this with reduce??
var totalCount = 0
for foo in array where foo.isAvailable {
    totalCount += foo.bars.count
}
eric.rl
  • 151
  • 1
  • 7
  • 2
    In a short way: `let reduced = array.reduce(0) { $1.isAvailable ? $0 + $1.bars.count : $0 }` – Larme May 25 '22 at 13:13

3 Answers3

4

In a short way:

let reduced = array.reduce(0) { $1.isAvailable ? $0 + $1.bars.count : $0 }

Now, let's explicit it:

The logic is quite simple:

  • We set the initial value to 0
  • In the closure, we have two parameters, the first one is the current value (at start it's the initial value), which we'll increment at each iteration, and the second one is the element of the array. We return then the new value (partial) that we incremented or not according to your condition (here is isAvailable)

Explicitly:

let reduced2 = array.reduce(0) { partial, current in
    if current.isAvailable {
        return partial + current.bars.count
    } else {
        return partial
    }
}

With a ternary if:

let reduced3 = array.reduce(0) { partial, current in
    return current.isAvailable ? partial + current.bars.count : partial
}

Getting rid of the return, see Functions With an Implicit Return in Functions of Swift or Implicit Returns from Single-Expression Closures of Closures of Swift.

let reduced4 = array.reduce(0) { partial, current in
    current.isAvailable ? partial + current.bars.count : partial
}

With Shorthand Argument Names on Closures of Swift.

let reduced5 = array.reduce(0) { $1.isAvailable ? $0 + $1.bars.count : $0 }
Larme
  • 24,190
  • 6
  • 51
  • 81
  • Thanks for the breakdown explanation! – eric.rl May 25 '22 at 13:26
  • If you still have problems understanding the method, in `reduced2`, print `partial` and `current` at the beginning of the method and read the logs, it should help you understand what's going on (that's how I studied the method at first). – Larme May 25 '22 at 13:26
  • Would you prefer writing this with `reduce` or as I've done it in the question? Efficiency & readability in mind. – eric.rl May 25 '22 at 13:32
  • 1
    I prefer `reduce(_:_:)` (I like that method, even if I even prefer `reduce(into:_:)`). BUT it's all about readability. There is nothing wrong about doing manually a for loop. It's all about readability. if your current skill level doesn't allow it, prefer the for loop, at least you understand it. If tomorrow, it brokes, or you need to change the conditions, if you aren't able to do it on the `reduce`, then don't do the `reduce`. You need to "master" your code. – Larme May 25 '22 at 13:37
  • With my current level, I'd use `reduced4` or `reduced5` solution, but I think I'd prefer `reduce4`, if in two months I read it, it'd be simpler, more explicit to debug, and I might even rename `current` with `aFoo` to let me know what struct I'm using. But it depends on how much I'm using that structs, I'm familiar with, etc. – Larme May 25 '22 at 13:39
2

You can achieve this using a single reduce operation. Start from a 0 result, then in the closure, check foo.isAvailable and only increment the count if it is true.

let totalCount = array.reduce(0, { accumulatingResult, foo in
  guard foo.isAvailable else { return accumulatingResult }
  return accumulatingResult + foo.bars.count
})
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
1
let sum = array
    .filter { $0.isAvailable }
    .map { $0.bars.count }
    .reduce(0, +)
Vadim Belyaev
  • 2,456
  • 2
  • 18
  • 23
  • I would add a `lazy`. – Sulthan May 25 '22 at 13:16
  • @Sulthan, I'm not sure how `lazy` is allowed in this context, could you provide an example? – Vadim Belyaev May 25 '22 at 13:22
  • 1
    https://stackoverflow.com/questions/51917054/why-and-when-to-use-lazy-with-array-in-swift for `lazy`. The downside of your code is that it iterates 3 times over the array. A `compactMap` at least combining `filter` & `map` would do one less iteration. – Larme May 25 '22 at 13:24
  • @Larme, thanks for the link, I learned something new today. I really like chaining simple operations like filter and map for clarity, and I guess that in most cases the performance difference is negligible when the array is relatively small, but I'll keep an eye on it going forward. You've inspired me to dig deeper into how these methods work. – Vadim Belyaev May 25 '22 at 13:32
  • 2
    No worries, I learn on SO every day too. Indeed, your code is not "optimized", but at least you reduced it into a model you know how to use `reduce(_:_:)`, on a `[Int]`. Which is really great too (joining the dots is too often a skill missed by developers). It really depends if you are looking for that efficiency, or just need to make it work. – Larme May 25 '22 at 13:35
  • 1
    @VadimBelyaev You are absolutely right that the difference would be negligible for small arrays. – Sulthan May 25 '22 at 14:40