flows 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|