This repository has been archived by the owner on Dec 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathAutomaton.swift
163 lines (139 loc) · 6.51 KB
/
Automaton.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//
// Automaton.swift
// ReactiveAutomaton
//
// Created by Yasuhiro Inami on 2016-05-07.
// Copyright © 2016 Yasuhiro Inami. All rights reserved.
//
import Result
import ReactiveSwift
/// Deterministic finite state machine that receives "input"
/// and with "current state" transform to "next state" & "output (additional effect)".
public final class Automaton<State, Input>
{
/// Basic state-transition function type.
public typealias Mapping = (State, Input) -> State?
/// Transducer (input & output) mapping with
/// `SignalProducer<Input, NoError>` (additional effect) as output,
/// which may emit next input values for continuous state-transitions.
public typealias EffectMapping = (State, Input) -> (State, SignalProducer<Input, NoError>)?
/// `Reply` signal that notifies either `.success` or `.failure` of state-transition on every input.
public let replies: Signal<Reply<State, Input>, NoError>
/// Current state.
public let state: Property<State>
fileprivate let _replyObserver: Signal<Reply<State, Input>, NoError>.Observer
fileprivate var _disposable: Disposable?
///
/// Initializer using `Mapping`.
///
/// - Parameters:
/// - state: Initial state.
/// - input: `Signal<Input, NoError>` that automaton receives.
/// - mapping: Simple `Mapping` that designates next state only (no additional effect).
///
public convenience init(state initialState: State, input inputSignal: Signal<Input, NoError>, mapping: @escaping Mapping)
{
/// - Warning: Strangely, casting and setting to temporal function is need in Swift 4.2 `swift build`...
let mapping2 = _compose(_toEffectMapping as (State?) -> (State, SignalProducer<Input, NoError>)?, mapping)
self.init(state: initialState, input: inputSignal, mapping: mapping2)
}
///
/// Initializer using `EffectMapping`.
///
/// - Parameters:
/// - state: Initial state.
/// - input: `Signal<Input, NoError>` that automaton receives.
/// - mapping: `EffectMapping` that designates next state and also generates additional effect.
/// - strategy: `FlattenStrategy` that flattens additional effect generated by `EffectMapping`.
///
public init(state initialState: State, input inputSignal: Signal<Input, NoError>, mapping: @escaping EffectMapping, strategy: FlattenStrategy = .merge)
{
let stateProperty = MutableProperty(initialState)
self.state = Property(stateProperty)
(self.replies, self._replyObserver) = Signal<Reply<State, Input>, NoError>.pipe()
/// Recursive input-producer that sends inputs from `inputSignal`
/// and also from additional effect generated by `EffectMapping`.
func recurInputProducer(_ inputProducer: SignalProducer<Input, NoError>, strategy: FlattenStrategy) -> SignalProducer<Input, NoError>
{
return SignalProducer<Input, NoError> { observer, disposable in
inputProducer
.withLatest(from: stateProperty.producer)
.map { input, fromState in
return (input, fromState, mapping(fromState, input)?.1)
}
.startWithSignal { mappingSignal, mappingSignalDisposable in
//
// NOTE:
// `mergedProducer` (below) doesn't emit `.Interrupted` although `mappingSignal` sends it,
// so propagate it to returning producer manually.
//
disposable += mappingSignal.observeInterrupted {
observer.sendInterrupted()
}
//
// NOTE:
// Split `mappingSignal` into `successSignal` and `failureSignal` (and merge later) so that
// inner producers of `flatMap(strategy)` in `successSignal` don't get interrupted by mapping failure.
//
let successSignal = mappingSignal
.filterMap { input, fromState, effect in
return effect.map { (input, fromState, $0) }
}
.flatMap(strategy) { input, _, effect -> SignalProducer<Input, NoError> in
return recurInputProducer(effect, strategy: strategy)
.prefix(value: input)
}
let failureSignal = mappingSignal
.filterMap { input, _, effect -> Input? in
return effect == nil ? input : nil
}
let mergedProducer = SignalProducer(values: failureSignal, successSignal).flatten(.merge)
disposable += mergedProducer.start(observer)
disposable += mappingSignalDisposable
}
}
}
recurInputProducer(SignalProducer(inputSignal), strategy: strategy)
.withLatest(from: stateProperty.producer)
.flatMap(.merge) { input, fromState -> SignalProducer<Reply<State, Input>, NoError> in
if let (toState, _) = mapping(fromState, input) {
return .init(value: .success(input, fromState, toState))
}
else {
return .init(value: .failure(input, fromState))
}
}
.startWithSignal { replySignal, disposable in
self._disposable = disposable
stateProperty <~ replySignal
.flatMap(.merge) { reply -> SignalProducer<State, NoError> in
if let toState = reply.toState {
return .init(value: toState)
}
else {
return .empty
}
}
replySignal.observe(self._replyObserver)
}
}
deinit
{
self._replyObserver.sendCompleted()
self._disposable?.dispose()
}
}
// MARK: Private
private func _compose<A, B, C>(_ g: @escaping (B) -> C, _ f: @escaping (A) -> B) -> (A) -> C
{
return { x in g(f(x)) }
}
private func _toEffectMapping<State, Input>(_ toState: State?) -> (State, SignalProducer<Input, NoError>)?
{
if let toState = toState {
return (toState, .empty)
}
else {
return nil
}
}