Go concurrency primitives for Swift

Swigo

Go concurrency primitives for Swift.

let msg = Chan<String>(buffer: 3)
let done = Chan<Bool>()

msg <- "Swift"
msg <- "❤️"
msg <- "Go"
msg.close()

go {
    for message in msg {
        print(message)
    }
    done <- true
}
<-done

msg := make(chan string, 3)
done := make(chan bool)

msg <- "Swift"
msg <- "❤️"
msg <- "Go"
close(msg)

go func() {
    for message := range msg {
        fmt.Println(message)
    }
    done <- true
}()
<-done

About

This repo is an experimental library to bring go style concurrency primitives to Swift. The objective is to bring nearly 1:1 API and behavior support to Swift. Do not expect comparabile performance or reliability. Swift does not have a runtime similar to go, and thus “goroutines” are just OS threads managed by GCD’s global queue.

Supported on:

  • macOS
  • iOS
  • Linux

Usage

  1. Add https://github.com/gh123man/Swigo as a Swift package dependency to your project.
  2. import Swigo and go!

Documentation & Examples

Range over chan

In Swift, Chan implements the Sequence and IteratorProtocol protocols. So you can enumerate a channel until it’s closed.

let msg = Chan<String>()
let done = Chan<Bool>()

go {
    for m in msg {
        print(m)
    }
    print("closed")
    done <- true
}

msg <- "hi"
msg.close()
<-done

msg := make(chan string)
done := make(chan bool)

go func() {
    for m := range msg {
        fmt.Println(m)
    }
    fmt.Println("closed")
    done <- true
}()

msg <- "hi"
close(msg)
<-done

Buffered Channels

Channels in Swift can be buffered or unbuffered

let count = Chan<Int>(buffer: 100)

for i in (0..<100) {
    count <- i
}
count.close()


let sum = count.reduce(0) { sum, next in
    sum + next
}
print(sum)

count := make(chan int, 100)

for i := 0; i < 100; i++ {
    count <- i
}
close(count)

sum := 0
for v := range count {
    sum += v
}
fmt.Println(sum)

Also map, reduce, etc work on channels in Swift too thanks to Sequence!

Select

Swift has reserve words for case and default and the operator support is not flexible enough to support inline channel operations in the select statement. So instead they are implemented as follows:

rx(c)

case <-c:

rx(c) { v in ... }

case v := <-c: ...

tx(c, "foo")

case c <- "foo":

none { ... }

default: ...

Gotcha: You cannot return from none to break an outer loop in Swift since it’s inside a closure. To break a loop surrounding a select, you must explicitly set some control variable (ex: while !done and done = true)

Examples

Example

chan receive

let a = Chan<String>(buffer: 1)
a <- "foo"

select {
    rx(a) { av in
        print(av!) 
    }
    none {
        print("Not called")
    }
}

a := make(chan string, 1)
a <- "foo"

select {
case av := <-a:
    fmt.Println(av)

default:
    fmt.Println("Not called")

}

chan send

let a = Chan<String>(buffer: 1)

select {
    tx(a, "foo")
    none {
        print("Not called")
    }
}
print(<-a)

a := make(chan string, 1)

select {
case a <- "foo":
default:
    fmt.Println("Not called")
}

fmt.Println(<-a)

default

let a = Chan<Bool>()

select {
    rx(a)
    none {
        print("Default case!")
    }
}

a := make(chan bool)

select {
case <-a:
default:
    fmt.Println("Default case!")

}

Closing Channels

A Chan can be closed. In Swift, the channel receive (<-) operator returns T? because a channel read will return nil when the channel is closed. If you try to write to a closed channel, a fatalError will be thrown.

It’s worth noting that this is somewhat different than how go deals with receiving on a closed channel. When a channel is closed you you need 1. a way to detect that the channel is closed and 2. something to return in place of the expected value. Swift does not have the notion of zero values so we need an alternate solution. Returning T? allows us to do both.

Because of Swift’s optional semantics and strict type system, it is not always convenient to have to unwrap an optional every time you read a channel. To solve this you can use OpenChan.

Alternatively you can simply unwrap the channel read:

let a = Chan<String>(buffer: 1)
a <- "hi"

if let val = <-a {
    print(val)
}

OpenChan

Unlike Chan, OpenChan cannot be closed – it is always open. As a result <- will return a non-optional T. This has some other side effects however:

  • If reading using the Sequence protocol, next() -> T? will never return nil and thus your loop will never terminate (without an explicit break or return).
  • There is no way to break a blocking channel read without writing to the channel.

Usage

let c = OpenChan<String>(buffer: 1)
c <- "hi"
let result: String = <-c // Not an optional

GitHub

View Github