Last active
June 21, 2019 03:14
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A fuller implementation of this idea is available here: https://github.com/kristopherjohnson/KJYield