There’s a funny thing that happens when you remove a language construct that actually provides value: you need to re-invent ways to support that construct.
The proposal Add scan, takeWhile, dropWhile, and iterate to the stdlib provides a basic way to get back the lost functionality of the C-style for-loop, specifically with iterate()
and takeWhile()
.
The key thing to remember for the implementation is that we must have a lazy version of iterate()
in order for this to be semantically comparable to the C-style for-loop that is being replaced. Further, we need to be extremely careful when using the proposed takeWhile()
(and other) extensions to be sure we’re getting the lazy versions when we need them.
So let’s look at what an implementation might look like (this is using Swift 2.2). We are going to want to replicate the following loop:
for var n = 1; n < 10; n = n \* 2 {
print("\(n)", terminator: ", ")
}
This loop simply outputs: 1, 2, 4, 8,
Ok, first we need to define the iterate
function:
// Creates a lazy sequence that begins at start. The next item in the
// sequence is calculated using the stride function.
func iterate(initial from: T, stride: T throws -> T) -> StridingSequence
This is going to require that we return a SequenceType
(this is renamed to Sequence
in Swift 3). But remember, we want this to be lazy, so we really need to conform to the LazySequenceType
protocol. That type is going to need to know the starting point and the mechanism to stride through the desired sequence.
struct StridingSequence : LazySequenceType {
let initial: Element
let stride: Element throws -> Element
init(initial: Element, stride: Element throws -> Element) {
self.initial = initial
self.stride = stride
}
func generate() -> StridingSequenceGenerator {
return StridingSequenceGenerator(initial: initial, stride: stride)
}
}
Of course, now the StridingSequence
is going to need the underlying GeneratorType
implementation: StridingSequenceGenerator
(the GeneratorType
protocol is renamed to IteratorProtocol
in Swift 3).
struct StridingSequenceGenerator : GeneratorType, SequenceType {
let initial: Element
let stride: Element throws -> Element
var current: Element?
init(initial: Element, stride: Element throws -> Element) {
self.initial = initial
self.stride = stride
self.current = initial
}
mutating func next() -> Element? {
defer {
if let c = current {
current = try? stride(c)
}
else {
current = nil
}
}
return current
}
}
OK… this is getting to be a lot of code. But there’s going to be a big payoff, right?
What we have now is an infinite sequence. We can test it out like so:
for n in iterate(initial: Int(1), stride: \{$0 \* 2}) {
if n >= 10 { break }
print("\(n)", terminator: ", ")
}
At this point, we are pretty close to getting what we want. The last question is how to move the condition out of the body of the loop and into the for-loop construct?
We have two basic options:
- Add a
while:
parameter to theiterate()
function, or - Add a
takeWhile()
function that can be chained.
The proposal that I linked to earlier proposes to add a takeWhile()
function. This is probably the “better” way to go given that we are creating a sequence and it’s feasible that we may want to do other operations, like filtering.
Unfortunately, this means a bit more code.
Let’s start with the extension to LazySequenceType
:
extension LazySequenceType \{
typealias ElementType = Self.Elements.Generator.Element
func takeWhile(predicate: ElementType -> Bool)
-> LazyTakeWhileSequence
{
return LazyTakeWhileSequence(base: self.elements, takeWhile: predicate)
}
}
This requires us to create another sequence type that knows how to walk our original sequence type but stop when the given condition is met.
struct LazyTakeWhileSequence : LazySequenceType {
let base: Base
let predicate: Base.Generator.Element -> Bool
init(base: Base, takeWhile predicate: Base.Generator.Element -> Bool) {
self.base = base
self.predicate = predicate
}
func generate() -> LazyTakeWhileGenerator {
return LazyTakeWhileGenerator(base: base.generate(), takeWhile: predicate)
}
}
And then this is going to require another generator type that can do gives us the next item in the sequence and nil
after the condition is met.
struct LazyTakeWhileGenerator : GeneratorType, SequenceType {
var base: Base
var predicate: Base.Element -> Bool
init(base: Base, takeWhile predicate: Base.Element -> Bool) {
self.base = base
self.predicate = predicate
}
mutating func next() -> Base.Element? {
if let n = base.next() where predicate(n) {
return n
}
return nil
}
}
Whew! Now we can write this:
for n in iterate(initial: Int(1), stride: \{$0 \* 2}).takeWhile({ $0 < 10 }) {
print("\(n)", terminator: ", ")
}
Of course, we could have just written this and been done with it:
for var n = 1; n < 10; n = n \* 2 {
print("\(n)", terminator: ", ")
}
Summary
It’s honestly really difficult for me to take this approach to be objectively better, especially when I have to write the supporting library code ;). Yes, there are clearly benefits to an iterate()
function that you can then perform different operations on, and maybe if I needed to perform some type of filtering with the above loop like so:
let items = iterate(initial: Int(1), stride: \{$0 \* 2})
.filter({ $0 != 4})
.takeWhile({ $0 < 10 })
for n in items \{
print("\(n)", terminator: ", ")
}
I could see the benefit for this approach for some use cases. However, there are also objectively bad things about the approach above. For one, there is a crap ton of code that needs to be written just to get this to work, and I’m not done. I need to similar stuff for collection types and the non-lazy versions as well.
The other thing, I don’t find it any less cryptic. Sure, things are labeled a bit better, but there’s a lot more syntax in the way now (using an @autoclosure
would be nice, but you cannot use anonymous variables like $0
). In fact, it’s only after moving the iterate()
code into its own line, do things start to become a bit more clear.
Anyhow, if you’re interested in how to implement this, it’s all here. And if there is actually an easier way, PLEASE let me know.
Full gist is here: iterate.swift.