Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active March 20, 2019 23:16
Show Gist options
  • Save chriseidhof/0082d246a1bc983a50009a835c4022e2 to your computer and use it in GitHub Desktop.
Save chriseidhof/0082d246a1bc983a50009a835c4022e2 to your computer and use it in GitHub Desktop.
Extensible Commands in TEA
import Foundation
// Let's use a Command protocol...
protocol Command {
associatedtype Action
static func modalAlert(title: String, accept: String) -> Self
static func request(_ request: URLRequest, available: @escaping (Data?) -> Action) -> Self
}
// And let's say we built a sample Elm app which has MyAction as the Action type...
enum MyAction {
case present(text: String)
}
// In the sample app, we would return something like this:
func sample<C>() -> C where C: Command, C.Action == MyAction {
return C.modalAlert(title: "Hello", accept: "Test")
}
func sample2<C>() -> C where C: Command, C.Action == MyAction {
let r = URLRequest(url: URL(string: "http://www.objc.io")!)
return C.request(r, available: { data in
return .present(text: "got data: \(data?.count ?? 0)")
})
}
// To run an action, we can define an "interpretation context" that looks something like this:
struct Run<A> {
let run: (_ callback: @escaping (A) -> ()) -> ()
}
extension Run: Command {
typealias Action = A
static func modalAlert(title: String, accept: String) -> Run<A> {
return Run { callback in
print("Modal alert: \(title), accept: \(accept).")
}
}
static func request(_ request: URLRequest, available: @escaping (Data?) -> A) -> Run<A> {
return Run { callback in
URLSession.shared.dataTask(with: request) { data, _, _ in
callback(available(data))
}.resume()
}
}
}
// Here's an example:
(sample() as Run).run { _ in () } // this prints
// If we wanted to test, however, we can define an enum:
enum Test<A> {
case _modalAlert(title: String, accept: String)
case _request(URLRequest, available: (Data?) -> A)
}
extension Test: Command {
typealias Action = A
static func modalAlert(title: String, accept: String) -> Test<A> {
return ._modalAlert(title: title, accept: accept)
}
static func request(_ request: URLRequest, available: @escaping (Data?) -> A) -> Test<A> {
return ._request(request, available: available)
}
}
// Let's run a networking test:
let test: Test = sample2() as Test // gives us the correct enum
// We could also implement map on the Command:
struct Map<A, B, C> where C: Command, C.Action == B {
let map: (_ : @escaping (A) -> B) -> C
}
extension Map: Command {
typealias Action = A
static func modalAlert(title: String, accept: String) -> Map {
return Map { transform in
C.modalAlert(title: title, accept: accept)
}
}
static func request(_ request: URLRequest, available: @escaping (Data?) -> A) -> Map {
return Map { transform in
C.request(request, available: { data in
transform(available(data))
})
}
}
}
// We'll make views abstract, as well
protocol ViewP {
associatedtype Action
static func button(title: String, onTap: Action) -> Self
static func label(_ text: String) -> Self
static func stack(_ children: [Self]) -> Self
}
enum VirtualView<A> {
case _button(title: String, onTap: A)
case _label(String)
case _stack([VirtualView<A>])
}
extension VirtualView: ViewP {
static func button(title: String, onTap: A) -> VirtualView<A> {
return ._button(title: title, onTap: onTap)
}
static func label(_ text: String) -> VirtualView<A> {
return ._label(text)
}
static func stack(_ children: [VirtualView<A>]) -> VirtualView<A> {
return ._stack(children)
}
typealias Action = A
}
// We can build a driver:
final class Driver<State, Action> {
var state: State
let view: (State) -> VirtualView<Action>
let update: (inout State, Action) -> Run<Action>?
init(_ state: State, _ view: @escaping (State) -> VirtualView<Action>, update: @escaping (inout State, Action) -> Run<Action>?) {
self.state = state
self.view = view
self.update = update
}
}
enum SampleAction {
case increase
case decrease
case received(newState: Int)
case reload
}
struct SampleState {
var counter: Int
mutating func update<C: Command>(_ action: SampleAction) -> C? where C.Action == SampleAction {
switch action {
case .decrease: counter -= 1
case .increase: counter += 1
case .received(let s): counter = s
case .reload: return C.request(URLRequest(url: URL(string: "mycounter.com")!), available: { data in
return .received(newState: data?.count ?? 0)
})
}
return nil
}
func view<V>() -> V where V: ViewP, V.Action == SampleAction {
return V.stack([
.label("counter: \(counter)"),
.button(title: "+", onTap: .increase),
.button(title: "-", onTap: .decrease),
.button(title: "reload", onTap: .reload)
])
}
}
let d = Driver<SampleState, SampleAction>(SampleState(counter: 0), { $0.view() }, update: { state, action in
state.update(action)
})
// We can also test without mocking/stubbing:
var z = SampleState(counter: 5)
if case let ._request(url, _)? = z.update(.reload) as Test? {
print("test succeeded")
}
// And the nice thing, we can extend the command retroactively, without having to modify the driver.
protocol WriteFile: Command {
static func write(_ data: Data, to url: URL) -> Self
}
extension Run: WriteFile {
static func write(_ data: Data, to url: URL) -> Run<A> {
return Run { _ in
try! data.write(to: url)
}
}
}
extension SampleState {
mutating func updateAlt<C>(action: SampleAction) -> C? where C: Command & WriteFile, C.Action == SampleAction {
return C.write(Data(), to: URL(string: "")!)
}
}
let d2 = Driver<SampleState, SampleAction>(SampleState(counter: 0), { $0.view() }, update: { state, a in
state.updateAlt(action: a)
})
// todo we can have a diffable/updatable virtual view hierarchy as well...
import UIKit
struct Virtual<Action> {
let construct: (_ callback: @escaping (Action) -> ()) -> UIView
let update: (_ existing: UIView) -> Bool // success
}
extension Virtual: ViewP {
static func button(title: String, onTap: Action) -> Virtual<Action> {
return Virtual(construct: { callback in
let b = UIButton(type: .custom)
b.setTitle(title, for: .normal)
// todo set action
return b
}, update: { existing in
guard let b = existing as? UIButton else { return false }
// ...
return true
})
}
static func label(_ text: String) -> Virtual<Action> {
fatalError("TODO")
}
static func stack(_ children: [Virtual<Action>]) -> Virtual<Action> {
fatalError("TODO")
}
}
final class Driver2<State, Action> {
var state: State
let view: (State) -> Virtual<Action>
let update: (inout State, Action) -> Run<Action>?
init(_ state: State, _ view: @escaping (State) -> Virtual<Action>, update: @escaping (inout State, Action) -> Run<Action>?) {
self.state = state
self.view = view
self.update = update
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment