Hi all!
While swift 2 is not open source yet, there isn't a specific place for discussing new feature proposals. Maybe for now this is the most appropriate place? I want to propose swift support for "yielding functions" a concept similar to coroutines, generators, fibers, etc in other languages. These constructs provide an excellent framework for working with asynchronous code, making it feel more like synchronous code, and making the job of handling errors much easier than with callbacks.
My proposal is something along the lines of:
func createYieldingFunction(foo: String) -> String yields -> String {
return { bar in
let baz = yield foo + bar
return foo + baz
}
}
do {
let yieldingFunction = createYieldingFunction(foo: "foo") // yieldingFunction will be: String yields -> String
let foobar = try yieldingFunction("bar") // foobar will be: "foobar"
if yieldingFunction.isAlive {
print("this will print")
}
let foobaz = try yieldingFunction("baz") // foobaz will be: "foobaz"
if yieldingFunction.isAlive {
print("this will not print")
}
try yieldingFunction("blah") // will throw error
} catch {
print(error) // will print something like: Dead yielding function called
}
The function createYieldingFunction is a regular function that returns a "yielding function". We use a function that generates a "yielding function" because these kinds of functions have a limited lifetime. If the "yielding function" returns, it turns into a "dead" state, meaning that it can't execute code anymore. So, if we have a dead yielding function, we can just create another one by calling createYieldingFunction again.
Yielding functions are very similar to "throwing functions", actually, every yielding function is implicitly marked as throws (or act like they're marked as throws) and should be called with the "try", "try!" or "try?" keywords. It "throws" by default because if you try to call a dead yielding function it wouldn't be capable of honoring the call, so it should throw a standard error like DeadYieldingFunctionError. If we want to know at runtime if the function is still alive we can call "isAlive" on it, which in turns returns true if the yielding function is still alive or false if it is dead.
The first time you call a yielding function you pass the required parameters like a regular function. When the function yields, it returns the yielded value like a regular return and passes the control back to the caller. The next time the caller calls the yielding function passing the required parameters, the code continues execution from where it left off. The "yield" keyword inside the yielding function returns the passed parameters and the execution goes on untill a new yield or a return appears.
Another important feature of yielding functions is the ability to throw errors from the caller scope into the yielding function. This is done by calling throw before the yielding function with the specified error.
func createYieldingFunction() -> Void yields -> Void {
return {
do {
yield
} catch {
print(error) // will print Error(description: "Error that will be thrown into the yielding function")
}
}
}
do {
let yieldingFunction = createYieldingFunction()
try yieldingFunction() // executes untill the yield
throw yieldingFunction Error(description: "Error that will be thrown into the yielding function")
} catch {
print(error) // will never be called
}
If the the yielding function catches the error, it can deal with the error inside the yielding function. It's important to know that even if you catch errors inside the yielding function the yielding function itself will still throw errors if it is called in a dead state.
func createYieldingFunction() -> Void yields -> Void {
return {
do {
yield
} catch {
print(error) // will print Error(description: "Error that will be thrown into the yielding function")
}
}
}
do {
let yieldingFunction = createYieldingFunction()
try yieldingFunction() // executes untill the yield
throw yieldingFunction Error(description: "Error that will be thrown into the yielding function")
try yieldingFunction() // throws DeadYieldingFunctionError
} catch {
print(error) // will print something like: Dead yielding function called
}
If the yielding function doesn't catch the error, the error will be retrown back to the caller.
func createYieldingFunction() -> Void yields -> Void {
return {
yield
}
}
do {
let yieldingFunction = createYieldingFunction()
try yieldingFunction() // executes untill the yield
throw yieldingFunction Error(description: "Error that will be thrown into the yielding function")
} catch {
print(error) // will print Error(description: "Error that will be thrown into the yielding function")
}
TL;DR
With yielding functions, instead of this pyramid of doom:
getString { string, error in
if let error = error {
print(error)
} else {
let upperCaseString = string!.uppercaseString
getOtherString(upperCaseString) { anotherString, anotherError in
if let error = anotherError {
print(error)
} else {
print(anotherString!)
}
}
}
}
We can write this:
async { _ in
try {
let string = yield getString()
let upperCaseString = string.uppercaseString
let anotherString = yield getOtherString(upperCaseString)
print(anotherString)
} catch {
print(error)
}
}
We basically turned asynchronous code into synchronous looking code, solving the readability and easing error handling. (getString() and getOtherString() are still async)