Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Created January 2, 2018 13:30
Show Gist options
  • Save chriseidhof/a542d0a074cbf8d418d25a8b8253ff33 to your computer and use it in GitHub Desktop.
Save chriseidhof/a542d0a074cbf8d418d25a8b8253ff33 to your computer and use it in GitHub Desktop.
//
// main.swift
// ExtensibleEffects
//
// Created by Chris Eidhof on 02.01.18.
// Copyright © 2018 objc.io. All rights reserved.
//
import Foundation
protocol SideEffect {
associatedtype Action
static func nothing() -> Self
static func readFile(path: String, transform: @escaping (Data?) -> Action) -> Self
static func sequence(_ commands: [Self]) -> Self
}
struct Execute<A> {
let run: (_ callback: @escaping (A) -> ()) -> ()
}
import Foundation
extension Execute: SideEffect {
typealias Action = A
static func nothing() -> Execute<A> {
return Execute { _ in () }
}
static func readFile(path: String, transform: @escaping (Data?) -> A) -> Execute<A> {
return Execute { (callback: (A) -> ()) in
let data = try? Data(contentsOf: URL(fileURLWithPath: path))
callback(transform(data))
}
}
static func sequence(_ commands: [Execute<A>]) -> Execute<A> {
return Execute { callback in
for c in commands {
c.run(callback)
}
}
}
}
// It's straightforward to create a new enum and make that conform to SideEffect so I'll omit it (but very useful for testing, of course).
// Here's a sample reducer state with a reduce method.
struct State {
var counter: Int = 0
enum Action {
case load
case didReadFile(contents: String?)
}
mutating func reduce<S: SideEffect>(action: Action) -> S where S.Action == Action {
switch action {
case .load:
return S.readFile(path: "counter.txt", transform: { data in
.didReadFile(contents: data.flatMap { String(data: $0, encoding: .utf8) })
})
case let .didReadFile(contents: contents):
if let s = contents, let i = Int(s) {
counter = i
}
return .nothing()
}
}
}
func sampleDriver() {
var state = State()
func interpret(action: State.Action) {
let result: Execute<State.Action> = state.reduce(action: action)
result.run(interpret)
}
interpret(action: .load) // example
}
// Now for the really cool part, we can also extend SideEffect! (For example, in a different module).
protocol Networking: SideEffect {
static func readURL(_ url: URL, transform: @escaping (Data?) -> Action) -> Self
}
extension Execute: Networking {
static func readURL(_ url: URL, transform: @escaping (Data?) -> A) -> Execute<A> {
return Execute { callback in
URLSession.shared.dataTask(with: url) { (data, _, _) in
callback(transform(data))
}.resume()
}
}
}
struct State2 {
var counter: Int = 0
enum Action {
case load
case didReadFile(contents: String?)
}
// Note that the type of our side-effect has changed:
mutating func reduce<S: SideEffect & Networking>(action: Action) -> S where S.Action == Action {
switch action {
case .load:
return S.readURL(URL(string: "http://localhost:8000/counter.txt")!, transform: { data in
.didReadFile(contents: data.flatMap { String(data: $0, encoding: .utf8) })
})
case let .didReadFile(contents: contents):
if let s = contents, let i = Int(s) {
counter = i
}
return .nothing()
}
}
}
func sampleDriver2() {
var state = State2()
func interpret(action: State2.Action) {
let result: Execute<State2.Action> = state.reduce(action: action)
result.run(interpret)
}
interpret(action: .load) // example
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment