flows 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +43 -0
- data/.mdlrc +1 -0
- data/.rubocop.yml +25 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +80 -25
- data/README.md +170 -44
- data/bin/benchmark +65 -42
- data/bin/examples.rb +37 -1
- data/bin/profile_10steps +48 -6
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +197 -0
- data/docs/_sidebar.md +26 -0
- data/docs/contributing/benchmarks_profiling.md +3 -0
- data/docs/contributing/local_development.md +3 -0
- data/docs/flow/direct_usage.md +3 -0
- data/docs/flow/general_idea.md +3 -0
- data/docs/index.html +30 -0
- data/docs/operation/basic_usage.md +1 -0
- data/docs/operation/inject_steps.md +3 -0
- data/docs/operation/lambda_steps.md +3 -0
- data/docs/operation/result_shapes.md +3 -0
- data/docs/operation/routing_tracks.md +3 -0
- data/docs/operation/wrapping_steps.md +3 -0
- data/docs/overview/performance.md +336 -0
- data/docs/railway/basic_usage.md +232 -0
- data/docs/result_objects/basic_usage.md +196 -0
- data/docs/result_objects/do_notation.md +139 -0
- data/flows.gemspec +2 -0
- data/forspell.dict +8 -0
- data/lefthook.yml +12 -0
- data/lib/flows.rb +2 -0
- data/lib/flows/flow.rb +1 -1
- data/lib/flows/operation.rb +1 -3
- data/lib/flows/operation/builder.rb +2 -2
- data/lib/flows/operation/dsl.rb +21 -0
- data/lib/flows/railway.rb +48 -0
- data/lib/flows/railway/builder.rb +68 -0
- data/lib/flows/railway/dsl.rb +28 -0
- data/lib/flows/railway/errors.rb +21 -0
- data/lib/flows/railway/executor.rb +23 -0
- data/lib/flows/result.rb +1 -0
- data/lib/flows/result/do.rb +30 -0
- data/lib/flows/result_router.rb +1 -1
- data/lib/flows/version.rb +1 -1
- metadata +59 -3
- data/.travis.yml +0 -8
data/docs/_sidebar.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
* Overview
|
2
|
+
* [Getting Started](README.md)
|
3
|
+
* [Performance](overview/performance.md)
|
4
|
+
|
5
|
+
* Result Objects
|
6
|
+
* [Basic Usage](result_objects/basic_usage.md)
|
7
|
+
* [Do Notation](result_objects/do_notation.md)
|
8
|
+
|
9
|
+
* Railway
|
10
|
+
* [Basic Usage](railway/basic_usage.md)
|
11
|
+
|
12
|
+
* Operation
|
13
|
+
* [Basics](operation/basic_usage.md)
|
14
|
+
* [Result Shapes](operation/result_shapes.md)
|
15
|
+
* [Routing & Tracks](operation/routing_tracks.md)
|
16
|
+
* [Lambda Steps](operation/lambda_steps.md)
|
17
|
+
* [Inject Steps](operation/inject_steps.md)
|
18
|
+
* [Wrapping Steps](operation/wrapping_steps.md)
|
19
|
+
|
20
|
+
* Flow
|
21
|
+
* [General Idea](flow/general_idea.md)
|
22
|
+
* [Direct Usage](flow/direct_usage.md)
|
23
|
+
|
24
|
+
* Contributing
|
25
|
+
* [Local Development](contributing/local_development.md)
|
26
|
+
* [Benchmarks & Profiling](contributing/benchmarks_profiling.md)
|
data/docs/index.html
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Flows</title>
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
7
|
+
<meta name="description" content="Flows documentation">
|
8
|
+
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
9
|
+
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<div id="app">Please wait...</div>
|
13
|
+
|
14
|
+
<script>
|
15
|
+
window.$docsify = {
|
16
|
+
name: 'Flows',
|
17
|
+
repo: 'ffloyd/flows',
|
18
|
+
auto2top: true,
|
19
|
+
noEmoji: true,
|
20
|
+
loadSidebar: true,
|
21
|
+
subMaxLevel: 2
|
22
|
+
}
|
23
|
+
</script>
|
24
|
+
|
25
|
+
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
|
26
|
+
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
|
27
|
+
<script src="//unpkg.com/prismjs/components/prism-ruby.min.js"></script>
|
28
|
+
<script src="//unpkg.com/docsify-plantuml/dist/docsify-plantuml.min.js"></script>
|
29
|
+
</body>
|
30
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
# Basic Usage
|
@@ -0,0 +1,336 @@
|
|
1
|
+
# Performance
|
2
|
+
|
3
|
+
Host:
|
4
|
+
|
5
|
+
* MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
|
6
|
+
* 3.1 GHz Intel Core i5
|
7
|
+
* 8 GB 2133 MHz LPDDR3
|
8
|
+
|
9
|
+
## Comparison with Trailblazer
|
10
|
+
|
11
|
+
`Flows::Railway` does not support tracks and routes, so it's reasonable to compare with `Flows::Operation` only.
|
12
|
+
|
13
|
+
`WITH_OP=1 WITH_TB=1 bin/benchmark` results:
|
14
|
+
|
15
|
+
```
|
16
|
+
--------------------------------------------------
|
17
|
+
- task: A + B, one step implementation
|
18
|
+
--------------------------------------------------
|
19
|
+
Warming up --------------------------------------
|
20
|
+
Flows::Operation (build once)
|
21
|
+
25.356k i/100ms
|
22
|
+
Flows::Operation (build each time)
|
23
|
+
9.168k i/100ms
|
24
|
+
Trailblazer::Operation
|
25
|
+
5.016k i/100ms
|
26
|
+
Calculating -------------------------------------
|
27
|
+
Flows::Operation (build once)
|
28
|
+
277.460k (± 1.2%) i/s - 1.395M in 5.027011s
|
29
|
+
Flows::Operation (build each time)
|
30
|
+
95.740k (± 2.7%) i/s - 485.904k in 5.079226s
|
31
|
+
Trailblazer::Operation
|
32
|
+
52.975k (± 1.8%) i/s - 265.848k in 5.020109s
|
33
|
+
|
34
|
+
Comparison:
|
35
|
+
Flows::Operation (build once): 277459.5 i/s
|
36
|
+
Flows::Operation (build each time): 95739.6 i/s - 2.90x slower
|
37
|
+
Trailblazer::Operation: 52974.6 i/s - 5.24x slower
|
38
|
+
|
39
|
+
|
40
|
+
--------------------------------------------------
|
41
|
+
- task: ten steps returns successful result
|
42
|
+
--------------------------------------------------
|
43
|
+
Warming up --------------------------------------
|
44
|
+
Flows::Operation (build once)
|
45
|
+
3.767k i/100ms
|
46
|
+
Flows::Operation (build each time)
|
47
|
+
1.507k i/100ms
|
48
|
+
Trailblazer::Operation
|
49
|
+
1.078k i/100ms
|
50
|
+
Calculating -------------------------------------
|
51
|
+
Flows::Operation (build once)
|
52
|
+
37.983k (± 2.9%) i/s - 192.117k in 5.062658s
|
53
|
+
Flows::Operation (build each time)
|
54
|
+
14.991k (± 4.2%) i/s - 75.350k in 5.035443s
|
55
|
+
Trailblazer::Operation
|
56
|
+
10.897k (± 2.8%) i/s - 54.978k in 5.049665s
|
57
|
+
|
58
|
+
Comparison:
|
59
|
+
Flows::Operation (build once): 37982.8 i/s
|
60
|
+
Flows::Operation (build each time): 14990.6 i/s - 2.53x slower
|
61
|
+
Trailblazer::Operation: 10896.9 i/s - 3.49x slower
|
62
|
+
```
|
63
|
+
|
64
|
+
## Comparison with Dry::Transaction
|
65
|
+
|
66
|
+
`Dry::Transaction` does not support tracks and branching so it's reasonable to compare with `Flows::Railway` only.
|
67
|
+
|
68
|
+
`WITH_RW=1 WITH_DRY=1 bin/benchmark` results:
|
69
|
+
|
70
|
+
```
|
71
|
+
--------------------------------------------------
|
72
|
+
- task: A + B, one step implementation
|
73
|
+
--------------------------------------------------
|
74
|
+
Warming up --------------------------------------
|
75
|
+
Flows::Railway (build once)
|
76
|
+
29.324k i/100ms
|
77
|
+
Flows::Railway (build each time)
|
78
|
+
11.159k i/100ms
|
79
|
+
Dry::Transaction (build once)
|
80
|
+
21.480k i/100ms
|
81
|
+
Dry::Transaction (build each time)
|
82
|
+
2.268k i/100ms
|
83
|
+
Calculating -------------------------------------
|
84
|
+
Flows::Railway (build once)
|
85
|
+
321.837k (± 1.3%) i/s - 1.613M in 5.012156s
|
86
|
+
Flows::Railway (build each time)
|
87
|
+
115.743k (± 2.6%) i/s - 580.268k in 5.016961s
|
88
|
+
Dry::Transaction (build once)
|
89
|
+
231.712k (± 1.7%) i/s - 1.160M in 5.007401s
|
90
|
+
Dry::Transaction (build each time)
|
91
|
+
23.093k (± 2.5%) i/s - 115.668k in 5.012311s
|
92
|
+
|
93
|
+
Comparison:
|
94
|
+
Flows::Railway (build once): 321837.4 i/s
|
95
|
+
Dry::Transaction (build once): 231712.5 i/s - 1.39x slower
|
96
|
+
Flows::Railway (build each time): 115743.1 i/s - 2.78x slower
|
97
|
+
Dry::Transaction (build each time): 23093.2 i/s - 13.94x slower
|
98
|
+
|
99
|
+
|
100
|
+
--------------------------------------------------
|
101
|
+
- task: ten steps returns successful result
|
102
|
+
--------------------------------------------------
|
103
|
+
Warming up --------------------------------------
|
104
|
+
Flows::Railway (build once)
|
105
|
+
5.607k i/100ms
|
106
|
+
Flows::Railway (build each time)
|
107
|
+
2.014k i/100ms
|
108
|
+
Dry::Transaction (build once)
|
109
|
+
2.918k i/100ms
|
110
|
+
Dry::Transaction (build each time)
|
111
|
+
275.000 i/100ms
|
112
|
+
Calculating -------------------------------------
|
113
|
+
Flows::Railway (build once)
|
114
|
+
57.765k (± 1.4%) i/s - 291.564k in 5.048484s
|
115
|
+
Flows::Railway (build each time)
|
116
|
+
20.413k (± 1.2%) i/s - 102.714k in 5.032467s
|
117
|
+
Dry::Transaction (build once)
|
118
|
+
29.597k (± 1.5%) i/s - 148.818k in 5.029422s
|
119
|
+
Dry::Transaction (build each time)
|
120
|
+
2.753k (± 2.0%) i/s - 14.025k in 5.096279s
|
121
|
+
|
122
|
+
Comparison:
|
123
|
+
Flows::Railway (build once): 57765.2 i/s
|
124
|
+
Dry::Transaction (build once): 29596.6 i/s - 1.95x slower
|
125
|
+
Flows::Railway (build each time): 20413.0 i/s - 2.83x slower
|
126
|
+
Dry::Transaction (build each time): 2753.2 i/s - 20.98x slower
|
127
|
+
```
|
128
|
+
|
129
|
+
## Railway vs Operation
|
130
|
+
|
131
|
+
`Flows::Railway` is created to improve performance in situations when you don't need tracks, branching and shape control (`Flows::Operation` has this features). So, it should be faster than `Flows::Operation`.
|
132
|
+
|
133
|
+
`WITH_OP=1 WITH_RW=1 bin/benchmark` results:
|
134
|
+
|
135
|
+
```
|
136
|
+
--------------------------------------------------
|
137
|
+
- task: A + B, one step implementation
|
138
|
+
--------------------------------------------------
|
139
|
+
Warming up --------------------------------------
|
140
|
+
Flows::Railway (build once)
|
141
|
+
29.440k i/100ms
|
142
|
+
Flows::Railway (build each time)
|
143
|
+
11.236k i/100ms
|
144
|
+
Flows::Operation (build once)
|
145
|
+
25.584k i/100ms
|
146
|
+
Flows::Operation (build each time)
|
147
|
+
9.161k i/100ms
|
148
|
+
Calculating -------------------------------------
|
149
|
+
Flows::Railway (build once)
|
150
|
+
315.648k (± 8.1%) i/s - 1.590M in 5.078736s
|
151
|
+
Flows::Railway (build each time)
|
152
|
+
117.747k (± 3.5%) i/s - 595.508k in 5.064191s
|
153
|
+
Flows::Operation (build once)
|
154
|
+
266.888k (±12.3%) i/s - 1.279M in 5.090531s
|
155
|
+
Flows::Operation (build each time)
|
156
|
+
91.424k (±11.0%) i/s - 458.050k in 5.097449s
|
157
|
+
|
158
|
+
Comparison:
|
159
|
+
Flows::Railway (build once): 315647.6 i/s
|
160
|
+
Flows::Operation (build once): 266888.4 i/s - same-ish: difference falls within error
|
161
|
+
Flows::Railway (build each time): 117747.2 i/s - 2.68x slower
|
162
|
+
Flows::Operation (build each time): 91423.7 i/s - 3.45x slower
|
163
|
+
|
164
|
+
|
165
|
+
--------------------------------------------------
|
166
|
+
- task: ten steps returns successful result
|
167
|
+
--------------------------------------------------
|
168
|
+
Warming up --------------------------------------
|
169
|
+
Flows::Railway (build once)
|
170
|
+
5.619k i/100ms
|
171
|
+
Flows::Railway (build each time)
|
172
|
+
2.009k i/100ms
|
173
|
+
Flows::Operation (build once)
|
174
|
+
3.650k i/100ms
|
175
|
+
Flows::Operation (build each time)
|
176
|
+
1.472k i/100ms
|
177
|
+
Calculating -------------------------------------
|
178
|
+
Flows::Railway (build once)
|
179
|
+
58.454k (± 2.8%) i/s - 292.188k in 5.002833s
|
180
|
+
Flows::Railway (build each time)
|
181
|
+
20.310k (± 2.4%) i/s - 102.459k in 5.047579s
|
182
|
+
Flows::Operation (build once)
|
183
|
+
38.556k (± 2.5%) i/s - 193.450k in 5.020871s
|
184
|
+
Flows::Operation (build each time)
|
185
|
+
15.222k (± 2.8%) i/s - 76.544k in 5.032272s
|
186
|
+
|
187
|
+
Comparison:
|
188
|
+
Flows::Railway (build once): 58453.8 i/s
|
189
|
+
Flows::Operation (build once): 38556.5 i/s - 1.52x slower
|
190
|
+
Flows::Railway (build each time): 20310.3 i/s - 2.88x slower
|
191
|
+
Flows::Operation (build each time): 15221.9 i/s - 3.84x slower
|
192
|
+
```
|
193
|
+
|
194
|
+
## Comparison with Plan Old Ruby Object
|
195
|
+
|
196
|
+
Of course, `flows` cannot be faster than naive implementation without any library usage. But it's nice to know how big infrastructure cost you pay.
|
197
|
+
|
198
|
+
`WITH_RW=1 WITH_PORO=1 bin/benchmark` results:
|
199
|
+
|
200
|
+
```
|
201
|
+
--------------------------------------------------
|
202
|
+
- task: A + B, one step implementation
|
203
|
+
--------------------------------------------------
|
204
|
+
Warming up --------------------------------------
|
205
|
+
Flows::Railway (build once)
|
206
|
+
29.276k i/100ms
|
207
|
+
Flows::Railway (build each time)
|
208
|
+
11.115k i/100ms
|
209
|
+
PORO 309.108k i/100ms
|
210
|
+
Calculating -------------------------------------
|
211
|
+
Flows::Railway (build once)
|
212
|
+
320.587k (± 3.5%) i/s - 1.610M in 5.029314s
|
213
|
+
Flows::Railway (build each time)
|
214
|
+
118.108k (± 3.0%) i/s - 600.210k in 5.086844s
|
215
|
+
PORO 9.998M (± 2.1%) i/s - 50.075M in 5.010848s
|
216
|
+
|
217
|
+
Comparison:
|
218
|
+
PORO: 9998276.0 i/s
|
219
|
+
Flows::Railway (build once): 320586.8 i/s - 31.19x slower
|
220
|
+
Flows::Railway (build each time): 118108.5 i/s - 84.65x slower
|
221
|
+
|
222
|
+
|
223
|
+
--------------------------------------------------
|
224
|
+
- task: ten steps returns successful result
|
225
|
+
--------------------------------------------------
|
226
|
+
Warming up --------------------------------------
|
227
|
+
Flows::Railway (build once)
|
228
|
+
5.671k i/100ms
|
229
|
+
Flows::Railway (build each time)
|
230
|
+
2.024k i/100ms
|
231
|
+
PORO 233.375k i/100ms
|
232
|
+
Calculating -------------------------------------
|
233
|
+
Flows::Railway (build once)
|
234
|
+
58.428k (± 1.6%) i/s - 294.892k in 5.048387s
|
235
|
+
Flows::Railway (build each time)
|
236
|
+
20.388k (± 3.9%) i/s - 103.224k in 5.070844s
|
237
|
+
PORO 4.937M (± 0.6%) i/s - 24.738M in 5.010488s
|
238
|
+
|
239
|
+
Comparison:
|
240
|
+
PORO: 4937372.3 i/s
|
241
|
+
Flows::Railway (build once): 58428.4 i/s - 84.50x slower
|
242
|
+
Flows::Railway (build each time): 20387.7 i/s - 242.17x slower
|
243
|
+
```
|
244
|
+
|
245
|
+
## All without PORO
|
246
|
+
|
247
|
+
`WITH_ALL=1 bin/benchmark` results:
|
248
|
+
|
249
|
+
```
|
250
|
+
--------------------------------------------------
|
251
|
+
- task: A + B, one step implementation
|
252
|
+
--------------------------------------------------
|
253
|
+
Warming up --------------------------------------
|
254
|
+
Flows::Railway (build once)
|
255
|
+
29.351k i/100ms
|
256
|
+
Flows::Railway (build each time)
|
257
|
+
11.044k i/100ms
|
258
|
+
Flows::Operation (build once)
|
259
|
+
25.475k i/100ms
|
260
|
+
Flows::Operation (build each time)
|
261
|
+
8.989k i/100ms
|
262
|
+
Dry::Transaction (build once)
|
263
|
+
21.082k i/100ms
|
264
|
+
Dry::Transaction (build each time)
|
265
|
+
2.272k i/100ms
|
266
|
+
Trailblazer::Operation
|
267
|
+
4.962k i/100ms
|
268
|
+
Calculating -------------------------------------
|
269
|
+
Flows::Railway (build once)
|
270
|
+
299.326k (±15.6%) i/s - 1.409M in 5.012398s
|
271
|
+
Flows::Railway (build each time)
|
272
|
+
116.186k (± 3.1%) i/s - 585.332k in 5.042902s
|
273
|
+
Flows::Operation (build once)
|
274
|
+
276.980k (± 3.1%) i/s - 1.401M in 5.064018s
|
275
|
+
Flows::Operation (build each time)
|
276
|
+
94.536k (± 2.6%) i/s - 476.417k in 5.042967s
|
277
|
+
Dry::Transaction (build once)
|
278
|
+
229.750k (± 1.6%) i/s - 1.160M in 5.048211s
|
279
|
+
Dry::Transaction (build each time)
|
280
|
+
23.381k (± 1.9%) i/s - 118.144k in 5.054920s
|
281
|
+
Trailblazer::Operation
|
282
|
+
50.936k (± 4.4%) i/s - 258.024k in 5.075897s
|
283
|
+
|
284
|
+
Comparison:
|
285
|
+
Flows::Railway (build once): 299325.9 i/s
|
286
|
+
Flows::Operation (build once): 276979.8 i/s - same-ish: difference falls within error
|
287
|
+
Dry::Transaction (build once): 229749.5 i/s - 1.30x slower
|
288
|
+
Flows::Railway (build each time): 116185.6 i/s - 2.58x slower
|
289
|
+
Flows::Operation (build each time): 94536.3 i/s - 3.17x slower
|
290
|
+
Trailblazer::Operation: 50936.0 i/s - 5.88x slower
|
291
|
+
Dry::Transaction (build each time): 23380.8 i/s - 12.80x slower
|
292
|
+
|
293
|
+
|
294
|
+
--------------------------------------------------
|
295
|
+
- task: ten steps returns successful result
|
296
|
+
--------------------------------------------------
|
297
|
+
Warming up --------------------------------------
|
298
|
+
Flows::Railway (build once)
|
299
|
+
5.734k i/100ms
|
300
|
+
Flows::Railway (build each time)
|
301
|
+
2.064k i/100ms
|
302
|
+
Flows::Operation (build once)
|
303
|
+
3.801k i/100ms
|
304
|
+
Flows::Operation (build each time)
|
305
|
+
1.502k i/100ms
|
306
|
+
Dry::Transaction (build once)
|
307
|
+
2.837k i/100ms
|
308
|
+
Dry::Transaction (build each time)
|
309
|
+
274.000 i/100ms
|
310
|
+
Trailblazer::Operation
|
311
|
+
1.079k i/100ms
|
312
|
+
Calculating -------------------------------------
|
313
|
+
Flows::Railway (build once)
|
314
|
+
58.541k (± 1.6%) i/s - 298.168k in 5.094712s
|
315
|
+
Flows::Railway (build each time)
|
316
|
+
20.626k (± 3.0%) i/s - 103.200k in 5.008021s
|
317
|
+
Flows::Operation (build once)
|
318
|
+
38.906k (± 2.7%) i/s - 197.652k in 5.084184s
|
319
|
+
Flows::Operation (build each time)
|
320
|
+
14.351k (±12.2%) i/s - 70.594k in 5.011606s
|
321
|
+
Dry::Transaction (build once)
|
322
|
+
29.588k (± 1.8%) i/s - 150.361k in 5.083603s
|
323
|
+
Dry::Transaction (build each time)
|
324
|
+
2.765k (± 1.8%) i/s - 13.974k in 5.054977s
|
325
|
+
Trailblazer::Operation
|
326
|
+
10.861k (± 2.1%) i/s - 55.029k in 5.069204s
|
327
|
+
|
328
|
+
Comparison:
|
329
|
+
Flows::Railway (build once): 58541.4 i/s
|
330
|
+
Flows::Operation (build once): 38906.4 i/s - 1.50x slower
|
331
|
+
Dry::Transaction (build once): 29587.8 i/s - 1.98x slower
|
332
|
+
Flows::Railway (build each time): 20626.0 i/s - 2.84x slower
|
333
|
+
Flows::Operation (build each time): 14351.1 i/s - 4.08x slower
|
334
|
+
Trailblazer::Operation: 10860.9 i/s - 5.39x slower
|
335
|
+
Dry::Transaction (build each time): 2765.3 i/s - 21.17x slower
|
336
|
+
```
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# Railway :: Basic Usage
|
2
|
+
|
3
|
+
`Flows::Railway` is an implementation of a Railway Programming pattern. You may read about this pattern in the following articles:
|
4
|
+
|
5
|
+
* [Programming on rails: Railway Oriented Programming](http://sandordargo.com/blog/2017/09/27/railway_oriented_programming) // it's not about Ruby on Rails
|
6
|
+
* [Railway Oriented Programming: A powerful Functional Programming pattern](https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31)
|
7
|
+
* [Railway Oriented Programming in Elixir with Pattern Matching on Function Level and Pipelining](https://medium.com/elixirlabs/railway-oriented-programming-in-elixir-with-pattern-matching-on-function-level-and-pipelining-e53972cede98)
|
8
|
+
|
9
|
+
Let's review a simple task and solve it using `Flows::Railway`: you have to get a user by ID, get all user's blog posts and convert it to an array of HTML-strings. In such situation, we have to implement three parts of our task and compose it into something we can call, for example, from a Rails controller. Also, the first and third steps may fail (user not found, conversion to HTML failed). And if a step failed - we have to return failure info immediately. Let's draw this using a UML activity diagram:
|
10
|
+
|
11
|
+
```plantuml
|
12
|
+
@startuml
|
13
|
+
|Success Path|
|
14
|
+
start
|
15
|
+
-> id: Integer;
|
16
|
+
:fetch_user;
|
17
|
+
if (success?) then (yes)
|
18
|
+
-> user: User;
|
19
|
+
:get_blog_posts;
|
20
|
+
-> posts: Array<Post>;
|
21
|
+
:convert_to_html;
|
22
|
+
if (success?) then (yes)
|
23
|
+
-> posts_html: Array<String>;
|
24
|
+
stop
|
25
|
+
else (no)
|
26
|
+
|Failure|
|
27
|
+
-> message: String;
|
28
|
+
end
|
29
|
+
endif
|
30
|
+
else (no)
|
31
|
+
|Failure|
|
32
|
+
-> message: String;
|
33
|
+
end
|
34
|
+
endif
|
35
|
+
@enduml
|
36
|
+
```
|
37
|
+
|
38
|
+
And implement using `Flows::Railway`:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class RenderUserBlogPosts
|
42
|
+
include Flows::Railway
|
43
|
+
|
44
|
+
step :fetch_user
|
45
|
+
step :get_blog_posts
|
46
|
+
step :convert_to_html
|
47
|
+
|
48
|
+
def fetch_user(id:)
|
49
|
+
user = User.find_by_id(id)
|
50
|
+
user ? ok(user: user) : err(message: "User #{id} not found")
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_blog_posts(user:)
|
54
|
+
ok(posts: User.posts)
|
55
|
+
end
|
56
|
+
|
57
|
+
def convert_to_html(posts:)
|
58
|
+
posts_html = post.map(&:text).map do |text|
|
59
|
+
html = convert(text)
|
60
|
+
return err(message: "cannot convert to html: #{text}")
|
61
|
+
end
|
62
|
+
|
63
|
+
ok(posts_html: posts_html)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# returns String or nil
|
69
|
+
def convert(text)
|
70
|
+
# some implementation here
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
And execute it:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# User with id = 1 exists and with id = 2 - doesn't
|
79
|
+
|
80
|
+
RenderUserBlogPosts.new.call(id: 1)
|
81
|
+
# => Flows::Result::Ok.new(posts_html: [...])
|
82
|
+
|
83
|
+
RenderUserBlogPosts.new.call(id: 2)
|
84
|
+
# => Flows::Result::Err.new(message: 'User 2 not found')
|
85
|
+
```
|
86
|
+
|
87
|
+
## Flows::Railway rules
|
88
|
+
|
89
|
+
* steps execution happens from the first to the last step
|
90
|
+
* input arguments (`Railway#call(...)`) becomes the input of the first step
|
91
|
+
* each step should return Result Object (`Flows::Result::Helpers` already included)
|
92
|
+
* if step returns failed result - execution stops and failed Result Object returned from Railway
|
93
|
+
* if step returns successful result - result data becomes arguments of the following step
|
94
|
+
* if the last step returns successful result - it becomes a result of a Railway execution
|
95
|
+
|
96
|
+
## Defining Steps
|
97
|
+
|
98
|
+
Two ways of step definition exist. First is by using an instance method:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
step :do_something
|
102
|
+
|
103
|
+
def do_something(**arguments)
|
104
|
+
# some implementation
|
105
|
+
# Result Object as return value
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Second is by using lambda:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
step :do_something, ->(**arguments) { ok(some: 'data') }
|
113
|
+
```
|
114
|
+
|
115
|
+
Definition with lambda exists primarily for debugging/testing purposes. I recommend you to use method-based implementations for all your business logic. Also, this is good for consistency, readability, and maintenance. __Think about Railway as about small book: you have a "table of contents" in a form of step definitions and actual "chapters" in the same order in a form of public methods. And your private methods becomes something like "appendix".__
|
116
|
+
|
117
|
+
## Dependency Injection
|
118
|
+
|
119
|
+
By default, we search for step implementation methods in a class instance. But you may override method source and inject your own:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class SayOk
|
123
|
+
include Flows::Railway
|
124
|
+
|
125
|
+
step :do_job
|
126
|
+
end
|
127
|
+
|
128
|
+
module Loud
|
129
|
+
extend Flows::Result::Helpers
|
130
|
+
|
131
|
+
def self.do_job
|
132
|
+
ok(text: 'OOOOKKKK!!!!')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
module Normal
|
137
|
+
extend Flows::Result::Helpers
|
138
|
+
|
139
|
+
def self.do_job
|
140
|
+
ok(text: 'ok')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
SayOk.new(method_source: Loud).call.unwrap
|
145
|
+
# => { text: 'OOOOKKKK!!!!' }
|
146
|
+
|
147
|
+
SayOk.new(method_source: Normal).call.unwrap
|
148
|
+
# => { text: 'ok' }
|
149
|
+
```
|
150
|
+
|
151
|
+
When you change your method source original class is no longer used for methods lookup. But what if we want to just override one of the steps? We can:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class SayOk
|
155
|
+
include Flows::Railway
|
156
|
+
|
157
|
+
step :do_job
|
158
|
+
|
159
|
+
def do_job
|
160
|
+
ok(text: 'ok')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
say_loud = -> { ok(text: 'OOOOKKKK!!!!') } # or anything with implemented #call method
|
165
|
+
|
166
|
+
SayOk.new.call.unwrap
|
167
|
+
# => { text: 'OOOOKKKK!!!!' }
|
168
|
+
|
169
|
+
SayOk.new(deps: { do_job: say_loud }).call.unwrap
|
170
|
+
# => { text: 'ok' }
|
171
|
+
```
|
172
|
+
|
173
|
+
Moreover, you can mix both approaches. Injecting using `deps:` has higher priority.
|
174
|
+
|
175
|
+
## Pre-building and Performance
|
176
|
+
|
177
|
+
As mentioned before, railway execution consists of two phases: build (`.new`) and run (`#call`). And the build phase is expensive. You may compare overheads when you build a railway each time:
|
178
|
+
|
179
|
+
```
|
180
|
+
$ WITH_RW=1 bin/benchmark
|
181
|
+
|
182
|
+
--------------------------------------------------
|
183
|
+
- task: A + B, one step implementation
|
184
|
+
--------------------------------------------------
|
185
|
+
Warming up --------------------------------------
|
186
|
+
Flows::Railway (build once)
|
187
|
+
30.995k i/100ms
|
188
|
+
Flows::Railway (build each time)
|
189
|
+
11.553k i/100ms
|
190
|
+
Calculating -------------------------------------
|
191
|
+
Flows::Railway (build once)
|
192
|
+
347.682k (± 2.1%) i/s - 1.767M in 5.083828s
|
193
|
+
Flows::Railway (build each time)
|
194
|
+
122.908k (± 4.2%) i/s - 623.862k in 5.085459s
|
195
|
+
|
196
|
+
Comparison:
|
197
|
+
Flows::Railway (build once): 347681.6 i/s
|
198
|
+
Flows::Railway (build each time): 122908.0 i/s - 2.83x slower
|
199
|
+
|
200
|
+
|
201
|
+
--------------------------------------------------
|
202
|
+
- task: ten steps returns successful result
|
203
|
+
--------------------------------------------------
|
204
|
+
Warming up --------------------------------------
|
205
|
+
Flows::Railway (build once)
|
206
|
+
6.130k i/100ms
|
207
|
+
Flows::Railway (build each time)
|
208
|
+
2.168k i/100ms
|
209
|
+
Calculating -------------------------------------
|
210
|
+
Flows::Railway (build once)
|
211
|
+
63.202k (± 1.6%) i/s - 318.760k in 5.044862s
|
212
|
+
Flows::Railway (build each time)
|
213
|
+
21.645k (± 3.6%) i/s - 108.400k in 5.014725s
|
214
|
+
|
215
|
+
Comparison:
|
216
|
+
Flows::Railway (build once): 63202.5 i/s
|
217
|
+
Flows::Railway (build each time): 21645.2 i/s - 2.92x slower
|
218
|
+
```
|
219
|
+
|
220
|
+
As the benchmark shows your infrastructure code overhead from Flows will be almost three times lower when you build your railways at 'compile' time. I mean something like that:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
class MyClass
|
224
|
+
MY_RAILWAY = MyRailway.new # this string will be executed on a class loading stage
|
225
|
+
|
226
|
+
def my_method
|
227
|
+
MY_RAILWAY.call
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
But if you don't care much about performance - build each time will be fast enough. Check out [Performance](overview/performance.md) page to see a bigger picture.
|