Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Actor based approach in practice for Swift deve...

Actor based approach in practice for Swift developers

Avatar for Bartosz Polaczyk

Bartosz Polaczyk

October 04, 2018

Other Decks in Programming

Transcript

  1. Theory What actor is Benefits iOS Actors Use cases Solution

    Benefits Future Swift language support Implementation Mini framework Actor sample Unit tests Challenges
  2. Proposed in 1973 by Carl Hewitt Used in distributed, high

    reliability systems, telecommunication industry Successful in Scala (framework Akka) and C#/F# (Akka.NET) Intro Actor
  3. Actor principles Has unique address Receives messages from different sources

    Processes one message a time Reacts by: Reading/writing local state (memory) Creating new actor(s) Sending message(s) to other actors
  4. Actor 
 limits Unidirectional - fire and forget No global

    state manipulation No concurrency within Actor Messages carry only information Order of messages is indeterministic
  5. Asynchronous problems [Actor] provides an "island of serialization in a

    sea of concurrency". Chris Lattner, Concurrency in Swift: One approach
  6. class Counter{ private var value = 0 public func add(){

    value += 1 } ... } class Counter{ private var value = 0 private let queue = DispatchQueue() public func add(){ queue.async{ value += 1 } } ... } class Counter{ private var value = 0 private let queue = DispatchQueue() public func add(){ queue.async{ value += 1 } } public func addTwice() { queue.async{ value += 2 } } public func add(from:TimesProvider){ queue.async{ value += from.value } } Traditional thread-safety API
  7. Developer’s responsibility for exclusive critical sections calls #1 Manual queuing

    class Counter{ private var value = 0 private let queue = DispatchQueue() public func add(){ queue.async{ value += 1 } } public func addTwice() { queue.async{ value += 2 } } public func add(from:TimesProvider){ queue.async{ value += from.value } } } External dependencies (e.g. arguments) may be thread-unsafe #2 Dependencies thread-safety Pitfalls Traditional way // nonatomic
  8. class Counter{ func add(_ value:Int){…} func subtract(_ value:Int){…} } counter.add(1)

    class CounterActor { func send(_ message:CounterMessage){…} } enum CounterMessage { case add(Int) case subtract(Int) } counterActor.send(.add(1)) Actor Swift
  9. • Message may not contain any shared state • Serialization/deserialization

    possible Full value semantics Actor message “Codable” “Codable” #2 Dependencies thread-safety Actor
  10. class ActorDriver<Actor:Actorable>{ private let queue:DispatchQueue private var actor:Actor private let

    externals:Actor.Command.Externals fileprivate init(actor:Actor, externals: Actor.Command.Externals, queue: DispatchQueue){ self.actor = actor self.externals = externals self.queue = queue } public func send(_ message:Actor.Message){ queue.async{ self.actor.onReceive(message).forEach{$0.interpret(externals: self.externals)} } } convenience init(actor:Actor, externals: Actor.Command.Externals){ self.init(actor: actor, externals: externals, queue: DispatchQueue(label:String(describing:ActorDriver<Actor>.self))) } } • Single function (send) • Statically typed message Input ActorDriver
  11. 01 protocol Actorable { associatedtype Message associatedtype Command:Commandable where Command.Message

    == Message mutating func onReceive(_ : Message) -> [Command] } protocol Commandable{ associatedtype Message:Messagable associatedtype Externals func interpret(externals: Externals, feedback: @escaping AnyActorDriver<Message> ) } 03 protocol Messagable {}
 02 typealias AnyActorDriver<Message:Messagable> = (Message) -> () 04 Input + Logic + Output Actor core logic
  12. class ActorDriver<Actor:Actorable>{ private let queue:DispatchQueue private var actor:Actor private let

    externals:Actor.Command.Externals fileprivate init(actor:Actor, externals: Actor.Command.Externals, queue: DispatchQueue){ self.actor = actor self.externals = externals self.queue = queue } func send(_ message:Actor.Message){ queue.async{ self.actor.onReceive(message).forEach{$0.interpret(externals: self.externals)} } } convenience init(actor:Actor, externals: Actor.Command.Externals){ self.init(actor: actor, externals: externals, queue: DispatchQueue(label:String(describing:ActorDriver<Actor>.self))) } } protocol Messagable {} protocol Actorable { associatedtype Message associatedtype Command:Commandable where Command.Message == Message mutating func onReceive(_ : Message) -> [Command] } protocol Commandable{ associatedtype Message:Messagable associatedtype Externals func interpret(externals: Externals, feedback: @escaping AnyActorDriver<Message> ) } Actor framework snippet
  13. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .initial .reloadIntent
  14. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .initial .reloadIntent
  15. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .reloadIntent .loading
  16. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading .loadElements
  17. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .loadElements
  18. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .loadElements
  19. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .resultsReady
  20. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .resultsReady
  21. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .resultsReady .reloadIntent
  22. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .resultsReady .reloadIntent
  23. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI .loading Network Actor .resultsReady .reloadIntent
  24. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI Network Actor .resultsReady .reloadIntent .presenting
  25. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI Network Actor .reloadIntent .presentElements .presenting
  26. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI Network Actor .reloadIntent .presenting
  27. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input ` UI Network Actor .reloadIntent .presenting
  28. enum Message{ case reloadIntent case resultsReady(elements:[Element]) } enum Command {

    case loadElements case presentElements(elements:[Element]) } enum ListScreenActor: Actorable { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.reloadIntent, .presenting(let elements)): self = .refreshing(elements: elements) return [.loadElements] case (.resultsReady(let elements), .loading), (.resultsReady(let elements), .refreshing): self = .presenting(elements: elements) return [.presentElements(elements: elements)] default: return [] } } } Output Logic ListScreenActor Input UI Network Actor .reloadIntent .presenting
  29. enum ListScreenActor { case initial case loading case presenting(elements:[Element]) case

    refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.resultsReady(let elements), .presenting): self = .presenting(elements: elements) return [.presentElements(elements: elements)] ... } } } Actor tests Initialize an actor Send message/ messages Observe commands func testFirstReload_startsLoading(){ var actor = ListScreenActor.initial let commands = actor.onReceive(.reloadIntent) XCTAssertEqual(commands, [.loadElements]) }
  30. enum ListScreenActor { case initial case loading case presenting(elements:[Element]) case

    refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.resultsReady(let elements), .presenting): self = .presenting(elements: elements) return [.presentElements(elements: elements)] ... } } } Actor tests Initialize an actor Send message/ messages Observe commands func testLoadedResults_presentElements(){ var actor = ListScreenActor.initial _ = actor.onReceive(.reloadIntent) let commands = actor.onReceive(.resultsReady(elements: ["1", "2"])) XCTAssertEqual(commands, [.presentElements(elements: [“1”,”2”])]) }
  31. func testMultiplereloads_loadOnce(){ var actor = ListScreenActor.initial let commands1 = actor.onReceive(.reloadIntent)

    let commands2 = actor.onReceive(.reloadIntent) let commands = actor.onReceive(.resultsReady(elements: ["1", “2"])) let commands3 = actor.onReceive(.reloadIntent) XCTAssertEqual(commands1.onReceive(), ...) XCTAssertEqual(commands2.onReceive(), ...) XCTAssertEqual(commands3.onReceive(), ...) XCTAssertEqual(commands.onReceive(), ...) } enum ListScreenActor { case initial case loading case presenting(elements:[Element]) case refreshing(elements:[Element]) mutating func onReceive(_ message: Message) -> [Command] { switch (message, self) { case (.reloadIntent,.initial): self = .loading return [.loadElements] case (.resultsReady(let elements), .presenting): self = .presenting(elements: elements) return [.presentElements(elements: elements)] ... } } } Actor tests Initialize an actor Send message/ messages Observe commands
  32. enum Message:Messagable{ case retrievePath(path:String, id: UUID) ... } enum Message:Messagable{

    case retrievePath(path:String, completion:(Int)->()) ... } #1 Completion handlers • Limitation: • No reference types in a message • Solution: • unique identifier
  33. • Limitation: • Actor reacts only for messages • Solution:

    • Separate object for sending message with timer events enum ScreenActor{ ... mutating func onReceive(_ message: Message) -> [Command] { switch (message, self){ case (.reloadIntent,.initial): self = .loading Timer.scheduledTimer(withTimeInterval: 60 , repeats: false) {_ in self = .initial } return [.loadElements] ... #2 Timers enum Message:Messagable{ … case timerEvent … }
  34. struct Actor:Actorable { mutating func onReceive(_ message: Message) -> [Command]

    { switch message{ ... case .contentError(_): ... } } throw #3 Error handling • Limitation: • No automatic thrown Errors propagation • Solution: • Manual propagation
  35. Concurrency in Swift: One approach actor keyword reference type actor

    Counter{ private var value = 0 private let otherActor:OtherActor actor func add(){ // queue.async{ value += 1 //} } actor func addTwice() { // queue.async{ value += 2 //} } public func add(from:TimesProvider){ value += 2 } } Compiler’s addition
  36. // Swift actor MainActor{ actor private init() {} } //

    App Extension MainActor{ // App logic } Concurrency in Swift: One approach support for MainActor ValueSemantical protocol protocol ValueSemantical{ ... } struct State: ValueSemantical{ let some: SomeType }