Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active June 21, 2019 03:14
Show Gist options
  • Save kristopherjohnson/68711422475ecc010e05 to your computer and use it in GitHub Desktop.
Save kristopherjohnson/68711422475ecc010e05 to your computer and use it in GitHub Desktop.
Attempt to implement a "yield" for Swift, similar to yield in Python, F#, etc.
import XCTest
import Foundation
// Generates values from a closure that invokes a "yield" function
struct YieldGenerator<T>: Generator {
var yieldedValues = Array<T>()
var index = 0
mutating func yield(value: T) {
yieldedValues.append(value)
}
init(_ yielder: ((T) -> ()) -> ()) {
yielder(yield)
}
mutating func next() -> T? {
return index < yieldedValues.count ? yieldedValues[index++] : nil
}
}
// Background task used by LazyYieldGenerator
//
// (This should be a nested class of LazyYieldGenerator, but the Xcode 6 Beta 2 editor freaks out.)
class LazyYieldTask<T> {
let yielder: ((T) -> ()) -> ()
let semValueDesired: dispatch_semaphore_t
let semValueAvailable: dispatch_semaphore_t
let syncQueue: dispatch_queue_t
var lastYieldedValue = Array<T>()
var isBackgroundTaskRunning = false
var isComplete = false
init(_ yielder: ((T) -> ()) -> ()) {
self.yielder = yielder
semValueDesired = dispatch_semaphore_create(0)
semValueAvailable = dispatch_semaphore_create(0)
syncQueue = dispatch_queue_create("LazyYieldTask syncQueue", DISPATCH_QUEUE_SERIAL)
}
// Called from background thread to yield a value to be returned by next()
func yield(value: T) {
dispatch_semaphore_wait(semValueDesired, DISPATCH_TIME_FOREVER)
dispatch_sync(syncQueue) {
self.lastYieldedValue = [value]
dispatch_semaphore_signal(self.semValueAvailable)
}
}
// Called from background thread to yield nil from next()
func yieldNil() {
dispatch_semaphore_wait(semValueDesired, DISPATCH_TIME_FOREVER)
dispatch_sync(syncQueue) {
self.lastYieldedValue = Array<T>()
dispatch_semaphore_signal(self.semValueAvailable)
}
}
// Called from generator thread to get next yielded value
func next() -> T? {
if !isBackgroundTaskRunning {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
self.run()
}
isBackgroundTaskRunning = true
}
dispatch_semaphore_signal(semValueDesired)
dispatch_semaphore_wait(semValueAvailable, DISPATCH_TIME_FOREVER)
var value: T?
dispatch_sync(syncQueue) {
if !self.lastYieldedValue.isEmpty {
value = self.lastYieldedValue[0]
self.lastYieldedValue = Array<T>()
}
}
return value
}
// Executed in background thread
func run() {
self.yielder(yield)
yieldNil()
}
}
// Generates values from a closure that invokes a "yield" function.
//
// The yielder closure is executed on another thread, and each call to yield()
// will block until next() is called by the generator's thread.
struct LazyYieldGenerator<T>: Generator {
var task: LazyYieldTask<T>?
let yielder: ((T) -> ()) -> ()
init(_ yielder: ((T) -> ()) -> ()) {
self.yielder = yielder
}
mutating func next() -> T? {
if task == nil {
task = LazyYieldTask(yielder)
}
return task!.next()
}
}
// Create a sequence from a closure that invokes a "yield" function
func sequence<T>(yielder: ((T) -> ()) -> ()) -> SequenceOf<T> {
return SequenceOf<T>({YieldGenerator(yielder)})
}
// Create a sequence from a closure that invokes a "yield" function.
//
// The closure is executed on another thread, and each call to yield()
// will block until next() is called by the generator's thread.
func lazy_sequence<T>(yielder: ((T) -> ()) -> ()) -> SequenceOf<T> {
return SequenceOf<T>({LazyYieldGenerator(yielder)})
}
class KJYieldTests: XCTestCase {
func testNumericSequence() {
// Sequence [3, 6, 9, 12, 15]
let seq: SequenceOf<Int> = sequence { yield in
for n in 0..5 { yield((n+1) * 3) }
}
var a = Array<Int>(seq)
XCTAssertEqual(5, a.count)
XCTAssertEqual(3, a[0])
XCTAssertEqual(6, a[1])
XCTAssertEqual(9, a[2])
XCTAssertEqual(12, a[3])
XCTAssertEqual(15, a[4])
}
func testFibonacciSequence() {
// Produce first 20 elements of Fibonacci sequence
let fibs = Array<Int>(sequence { yield in
var a = 0, b = 1
for _ in 0..20 {
yield(b)
let sum = a + b
a = b
b = sum
}
})
XCTAssertEqual(20, fibs.count)
XCTAssertEqual(1, fibs[0])
XCTAssertEqual(1, fibs[1])
XCTAssertEqual(2, fibs[2])
XCTAssertEqual(3, fibs[3])
XCTAssertEqual(5, fibs[4])
XCTAssertEqual(8, fibs[5])
XCTAssertEqual(55, fibs[9])
XCTAssertEqual(6765, fibs[19])
}
func testFizzBuzz() {
let fizzBuzz = Array<String>(sequence { yield in
for n in 1...100 {
if n % 3 == 0 {
if n % 5 == 0 {
yield("FizzBuzz")
}
else {
yield("Fizz")
}
}
else if n % 5 == 0 {
yield("Buzz")
}
else {
yield(n.description)
}
}
})
XCTAssertEqual(100, fizzBuzz.count)
XCTAssertEqual("1", fizzBuzz[0])
XCTAssertEqual("2", fizzBuzz[1])
XCTAssertEqual("Fizz", fizzBuzz[2])
XCTAssertEqual("4", fizzBuzz[3])
XCTAssertEqual("Buzz", fizzBuzz[4])
XCTAssertEqual("Fizz", fizzBuzz[5])
XCTAssertEqual("7", fizzBuzz[6])
XCTAssertEqual("14", fizzBuzz[13])
XCTAssertEqual("FizzBuzz", fizzBuzz[14])
XCTAssertEqual("16", fizzBuzz[15])
}
func testLazySequence() {
var yieldCount = 0
var yielderComplete = false
let seq: SequenceOf<Int> = lazy_sequence { yield in
++yieldCount
yield(1)
++yieldCount
yield(2)
++yieldCount
yield(3)
yielderComplete = true
}
var gen = seq.generate()
XCTAssertEqual(0, yieldCount, "yield should not be called until next()")
XCTAssertFalse(yielderComplete)
let val1 = gen.next()
XCTAssertEqual(1, val1!)
XCTAssertEqual(2, yieldCount, "should be blocked on second yield call")
XCTAssertFalse(yielderComplete)
let val2 = gen.next()
XCTAssertEqual(2, val2!)
XCTAssertEqual(3, yieldCount, "should be blocked on third yield call")
XCTAssertFalse(yielderComplete)
let val3 = gen.next()
XCTAssertEqual(3, val3!)
XCTAssertTrue(yielderComplete, "should have run to completion")
let val4 = gen.next()
XCTAssertNil(val4, "should have no more values")
}
func testDeckOfCards() {
let suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
let ranks = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King", "Ace"]
let seq: SequenceOf<String> = lazy_sequence { yield in
for suit in suits {
for rank in ranks {
yield("\(rank) of \(suit)")
}
}
}
let deck = Array<String>(seq)
XCTAssertEqual(52, deck.count)
XCTAssertEqual("2 of Clubs", deck[0])
XCTAssertEqual("3 of Clubs", deck[1])
XCTAssertEqual("Ace of Clubs", deck[12])
XCTAssertEqual("2 of Diamonds", deck[13])
XCTAssertEqual("King of Spades", deck[50])
XCTAssertEqual("Ace of Spades", deck[51])
}
func testAsyncReadFileByLine() {
// Use TestData.txt resource from test bundle, which has these contents:
//
// 1. First Line
// 2. Second Line
// 3. Third Line
// 4. Fourth Line
//
let testBundle = NSBundle(forClass: KJYieldTests.self)
let testDataPath: NSString! = testBundle.pathForResource("TestData", ofType: "txt")
// Determine length of null-terminated UTF8 string buffer
func UTF8StringLength(var charPointer: UnsafePointer<CChar>) -> Int {
var length = 0
while charPointer.memory != 0 {
++length
charPointer = charPointer.succ()
}
return length
}
// Read line from file. Returns line or nil if at end-of-file
func readLineFromFile(file: UnsafePointer<FILE>) -> String? {
var buffer = Array<CChar>(count: 4096, repeatedValue: 0)
let lineBytes = fgets(&buffer, CInt(buffer.count), file)
if lineBytes {
let length = UTF8StringLength(lineBytes)
let string = NSString(bytes: lineBytes, length: length, encoding: NSUTF8StringEncoding)
return string
}
else {
return nil
}
}
let lines: SequenceOf<String> = lazy_sequence { yield in
let file = fopen(testDataPath.UTF8String, "r")
while true {
if let line = readLineFromFile(file) {
yield(line)
}
else {
break
}
}
fclose(file)
}
var lineNumber = 0
for line in lines {
++lineNumber
switch (lineNumber) {
case 1:
XCTAssertEqual("1. First Line\n", line)
case 2:
XCTAssertEqual("2. Second Line\n", line)
case 3:
XCTAssertEqual("3. Third Line\n", line)
case 4:
XCTAssertEqual("4. Fourth Line\n", line)
default:
XCTFail("unexpected input line: \(line)")
}
}
}
func testEmptySequence() {
let array = Array<String>(sequence { yield in return })
XCTAssertTrue(array.isEmpty)
}
func testEmptyLazySequence() {
let array = Array<String>(lazy_sequence { yield in return })
XCTAssertTrue(array.isEmpty)
}
}
@kristopherjohnson
Copy link
Author

A fuller implementation of this idea is available here: https://github.com/kristopherjohnson/KJYield

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment