pipe_operator 0.0.1 → 0.0.2
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/README.md +97 -51
- data/lib/pipe_operator.rb +4 -4
- data/lib/pipe_operator/closure.rb +4 -0
- data/pipe_operator.gemspec +1 -1
- data/spec/pipe_operator_spec.rb +7 -9
- metadata +2 -3
- data/lib/pipe_operator/autoload.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddf66c989601033a8afbc02f4ae7689052eecaa61d361d477f2cf92bec30ce97
|
4
|
+
data.tar.gz: fa3b1c43271d8ee56032fe7b485a81aec78152602e39ece819929c2accf246f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32a09b3839ee5f41e35623300a0501c8fdc3365c38996d6433ad89d659e055d514afa170946c10c58fb5646236c077fe5463c047c2e96c2ddd48e13d141ba196
|
7
|
+
data.tar.gz: b3e31722a99368a65a8daf790693d0712008862c558daea345321d8bdbaaccf5eaf09037c516e1210fdf0fbc967130459cbbb545e91ad28e4381c40ff6608227
|
data/README.md
CHANGED
@@ -3,22 +3,47 @@
|
|
3
3
|
> Elixir/Unix style pipe operations in Ruby - **PROOF OF CONCEPT**
|
4
4
|
|
5
5
|
```ruby
|
6
|
-
|
7
|
-
```
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
[9, 64].map(&Math.|.sqrt.to_i) #=> [3, 8]
|
11
|
-
```
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
"https://api.github.com/repos/ruby/ruby".| do
|
6
|
+
"https://api.github.com/repos/ruby/ruby".pipe do
|
15
7
|
URI.parse
|
16
8
|
Net::HTTP.get
|
17
9
|
JSON.parse.fetch("stargazers_count")
|
18
10
|
yield_self { |n| "Ruby has #{n} stars" }
|
19
11
|
Kernel.puts
|
20
12
|
end
|
21
|
-
#=> Ruby has
|
13
|
+
#=> Ruby has 15120 stars
|
14
|
+
```
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
-9.pipe { abs; Math.sqrt; to_i } #=> 3
|
18
|
+
|
19
|
+
# Method chaining is supported:
|
20
|
+
-9.pipe { abs; Math.sqrt.to_i } #=> 3
|
21
|
+
|
22
|
+
# Pipe | for syntactic sugar:
|
23
|
+
-9.pipe { abs | Math.sqrt.to_i } #=> 3
|
24
|
+
|
25
|
+
# Pipe blocks are instance_exec within the context
|
26
|
+
# of a PipeOperator::Pipe object which defines its
|
27
|
+
# own implementation for how `|` should behave.
|
28
|
+
#
|
29
|
+
# If we actually need to pipe the method `|` on
|
30
|
+
# some other object then we can just use `send`:
|
31
|
+
-2.pipe { abs | send(:|, 4) } #=> 6
|
32
|
+
```
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
sqrt = Math.pipe.sqrt #=> #<PipeOperator::Closure:0x00007fc1172ed558@pipe_operator/closure.rb:18>
|
36
|
+
sqrt.call(9) #=> 3.0
|
37
|
+
sqrt.call(64) #=> 8.0
|
38
|
+
|
39
|
+
[9, 64].map(&Math.pipe.sqrt) #=> [3.0, 8.0]
|
40
|
+
[9, 64].map(&Math.pipe.sqrt.to_i.to_s) #=> ["3", "8"]
|
41
|
+
|
42
|
+
# Still not concise enough for you?
|
43
|
+
Module.alias_method(:|, :__pipe__)
|
44
|
+
|
45
|
+
[9, 64].map(&Math.|.sqrt) #=> [3.0, 8.0]
|
46
|
+
[9, 64].map(&Math.|.sqrt.to_i.to_s) #=> [3.0, 8.0]
|
22
47
|
```
|
23
48
|
|
24
49
|
## Why?
|
@@ -31,6 +56,16 @@ There's been some recent activity related to `Method` and `Proc` composition in
|
|
31
56
|
|
32
57
|
This gem was created to **propose an alternative syntax** for this kind of behavior.
|
33
58
|
|
59
|
+
## Matz on Ruby
|
60
|
+
|
61
|
+
Source: [ruby-lang.org/en/about](https://www.ruby-lang.org/en/about)
|
62
|
+
|
63
|
+
Ruby is a language of careful **balance of both functional and imperative programming**.
|
64
|
+
|
65
|
+
Matz has often said that he is **trying to make Ruby natural, not simple**, in a way that mirrors life.
|
66
|
+
|
67
|
+
Building on this, he adds: Ruby is **simple in appearance, but is very complex inside**, just like our human body.
|
68
|
+
|
34
69
|
## Concept
|
35
70
|
|
36
71
|
The general idea is to **pass the result of one expression as an argument to another expression** - similar to [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)):
|
@@ -46,7 +81,7 @@ The [Elixir pipe operator documentation](https://elixirschool.com/en/lessons/bas
|
|
46
81
|
JSON.parse(Net::HTTP.get(URI.parse(url)))
|
47
82
|
```
|
48
83
|
|
49
|
-
To be **
|
84
|
+
To be **inverted** and rewritten as **left to right** or **top to bottom** which is more **natural to read** in English:
|
50
85
|
|
51
86
|
```ruby
|
52
87
|
# left to right
|
@@ -83,7 +118,7 @@ end
|
|
83
118
|
While the ability to perform a job correctly and efficiently is certainly important - the **true beauty of a program lies in its clarity and conciseness**:
|
84
119
|
|
85
120
|
```ruby
|
86
|
-
"https://api.github.com/repos/ruby/ruby"
|
121
|
+
"https://api.github.com/repos/ruby/ruby".pipe do
|
87
122
|
URI.parse
|
88
123
|
Net::HTTP.get
|
89
124
|
JSON.parse.fetch("stargazers_count")
|
@@ -96,22 +131,12 @@ end
|
|
96
131
|
There's nothing really special here - it's just a **block of expressions like any other Ruby DSL** and the pipe `|` operator has been [around for decades](https://en.wikipedia.org/wiki/Pipeline_(Unix))!
|
97
132
|
|
98
133
|
```ruby
|
99
|
-
|
100
|
-
objects.map(&Marshal.method(:dump))
|
101
|
-
|
102
|
-
# but this is more concise
|
103
|
-
objects.map(&Marshal.|.dump)
|
104
|
-
```
|
105
|
-
|
106
|
-
The **simplicity and elegance of Ruby** is one of the many reasons that people fall in love with the language!
|
107
|
-
|
108
|
-
```ruby
|
109
|
-
Ruby.is.so(:simple, &elegant).that(you can) do
|
134
|
+
Ruby.is.so(elegant, &:expressive).that(you can) do
|
110
135
|
pretty_much ANYTHING if it.compiles!
|
111
136
|
end
|
112
137
|
```
|
113
138
|
|
114
|
-
This concept of **
|
139
|
+
This concept of **pipe operations could be a great fit** like it has been for many other languages:
|
115
140
|
|
116
141
|
* [Caml composition operators](http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#1_Compositionoperators)
|
117
142
|
* [Closure threading macros](https://clojure.org/guides/threading_macros)
|
@@ -128,22 +153,11 @@ This concept of **pipes could be a great fit** like it has been for many other l
|
|
128
153
|
|
129
154
|
**WARNING - EXPERIMENTAL PROOF OF CONCEPT**
|
130
155
|
|
131
|
-
This has only been **tested in isolation with RSpec**!
|
156
|
+
This has only been **tested in isolation with RSpec and Ruby 2.5.3**!
|
132
157
|
|
133
158
|
```ruby
|
134
159
|
# First `gem install pipe_operator`
|
135
160
|
require "pipe_operator"
|
136
|
-
|
137
|
-
# Then use PipeOperator as a refinement
|
138
|
-
using ::PipeOperator
|
139
|
-
|
140
|
-
# Or include PipeOperator in classes/modules
|
141
|
-
class AnyClass
|
142
|
-
include ::PipeOperator
|
143
|
-
end
|
144
|
-
|
145
|
-
# Or monkey patch PipeOperator into ALL objects
|
146
|
-
require "pipe_operator/autoload"
|
147
161
|
```
|
148
162
|
|
149
163
|
## Implementation
|
@@ -151,37 +165,40 @@ require "pipe_operator/autoload"
|
|
151
165
|
The [PipeOperator](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator.rb) module has a method named `__pipe__` which is aliased as `pipe` for convenience and `|` for syntactic sugar:
|
152
166
|
|
153
167
|
```ruby
|
154
|
-
|
155
|
-
|
168
|
+
module PipeOperator
|
169
|
+
def __pipe__(*args, &block)
|
170
|
+
Pipe.new(self, *args, &block)
|
171
|
+
end
|
156
172
|
end
|
157
173
|
|
158
|
-
|
159
|
-
|
174
|
+
BasicObject.send(:include, PipeOperator)
|
175
|
+
Kernel.alias_method(:pipe, :__pipe__)
|
176
|
+
Module.alias_method(:|, :__pipe__)
|
160
177
|
```
|
161
178
|
|
162
179
|
When no arguments are passed to `__pipe__` then a [PipeOperator::Pipe](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb) object is returned:
|
163
180
|
|
164
181
|
```ruby
|
165
|
-
Math
|
182
|
+
Math.pipe #=> #<PipeOperator::Pipe:Math>
|
166
183
|
```
|
167
184
|
|
168
185
|
Any methods invoked on this object returns a [PipeOperator::Closure](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb) which **calls the method on the object later**:
|
169
186
|
|
170
187
|
```ruby
|
171
|
-
sqrt = Math
|
172
|
-
sqrt.call(16)
|
188
|
+
sqrt = Math.pipe.sqrt #=> #<PipeOperator::Closure:0x00007fc1172ed558@pipe_operator/closure.rb:18>
|
189
|
+
sqrt.call(16) #=> 4.0
|
173
190
|
|
174
|
-
missing = Math
|
175
|
-
missing.call
|
191
|
+
missing = Math.pipe.missing #=> #<PipeOperator::Closure:0x00007fc11726f0e0@pipe_operator/closure.rb:18>
|
192
|
+
missing.call #=> NoMethodError: undefined method 'missing' for Math:Module
|
176
193
|
|
177
|
-
Math.method(:missing)
|
194
|
+
Math.method(:missing) #=> NameError: undefined method 'missing' for class '#<Class:Math>'
|
178
195
|
```
|
179
196
|
|
180
197
|
When `__pipe__` is called **with arguments but without a block** then it behaves similar to `__send__`:
|
181
198
|
|
182
199
|
```ruby
|
183
|
-
sqrt = Math
|
184
|
-
sqrt.call(16)
|
200
|
+
sqrt = Math.pipe(:sqrt) #=> #<PipeOperator::Closure:0x00007fe52e0cdf80@pipe_operator/closure.rb:18>
|
201
|
+
sqrt.call(16) #=> 4.0
|
185
202
|
|
186
203
|
sqrt = Math.pipe(:sqrt, 16) #=> #<PipeOperator::Closure:0x00007fe52fa18fd0@pipe_operator/closure.rb:18>
|
187
204
|
sqrt.call #=> 4.0
|
@@ -191,13 +208,13 @@ sqrt.call(16) #=> ArgumentError: wrong number of arguments (given
|
|
191
208
|
These [PipeOperator::Closure](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb) objects can be [bound as block arguments](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/proxy.rb#L10-L13) just like any other [Proc](https://ruby-doc.org/core-2.5.3/Proc.html):
|
192
209
|
|
193
210
|
```ruby
|
194
|
-
[16, 256].map(&Math
|
211
|
+
[16, 256].map(&Math.pipe.sqrt) #=> [4.0, 16.0]
|
195
212
|
```
|
196
213
|
|
197
214
|
Simple **closure composition is supported** via [method chaining](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb#L56):
|
198
215
|
|
199
216
|
```ruby
|
200
|
-
[16, 256].map(&Math
|
217
|
+
[16, 256].map(&Math.pipe.sqrt.to_i.to_s) #=> ["4", "16"]
|
201
218
|
```
|
202
219
|
|
203
220
|
The **block** form of `__pipe__` behaves **similar to instance_exec** but can also [call methods on other objects](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb#L81):
|
@@ -292,7 +309,35 @@ define_method(method) do |*args, &block|
|
|
292
309
|
end
|
293
310
|
```
|
294
311
|
|
295
|
-
These proxy modules are prepended everywhere
|
312
|
+
These **proxy modules are prepended everywhere**!
|
313
|
+
|
314
|
+
It's certainly something that **could be way more efficient as a core part of Ruby**.
|
315
|
+
|
316
|
+
Maybe somewhere **lower level where methods are dispatched**? Possibly somewhere in this [vm_eval.c switch](https://github.com/ruby/ruby/blob/trunk/vm_eval.c#L111)?
|
317
|
+
|
318
|
+
```c
|
319
|
+
again:
|
320
|
+
switch (cc->me->def->type) {
|
321
|
+
case VM_METHOD_TYPE_ISEQ
|
322
|
+
case VM_METHOD_TYPE_NOTIMPLEMENTED
|
323
|
+
case VM_METHOD_TYPE_CFUNC
|
324
|
+
case VM_METHOD_TYPE_ATTRSET
|
325
|
+
case VM_METHOD_TYPE_IVAR
|
326
|
+
case VM_METHOD_TYPE_BMETHOD
|
327
|
+
case VM_METHOD_TYPE_ZSUPER
|
328
|
+
case VM_METHOD_TYPE_REFINED
|
329
|
+
case VM_METHOD_TYPE_ALIAS
|
330
|
+
case VM_METHOD_TYPE_MISSING
|
331
|
+
case VM_METHOD_TYPE_OPTIMIZED
|
332
|
+
case OPTIMIZED_METHOD_TYPE_SEND
|
333
|
+
case OPTIMIZED_METHOD_TYPE_CALL
|
334
|
+
case VM_METHOD_TYPE_UNDEF
|
335
|
+
}
|
336
|
+
```
|
337
|
+
|
338
|
+
Then we'd **only need Ruby C API ports** for [PipeOperator::Pipe](https://github.com/LendingHome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb) and [PipeOperator::Closure](https://github.com/LendingHome/pipe_operator/blob/master/lib/pipe_operator/closure.rb)!
|
339
|
+
|
340
|
+
All other objects in this proof of concept are related to **method interception** and would no longer be necessary.
|
296
341
|
|
297
342
|
## Bugs
|
298
343
|
|
@@ -366,6 +411,7 @@ bundle exec rspec
|
|
366
411
|
* https://github.com/tiagopog/piped_ruby
|
367
412
|
* https://github.com/jicksta/methodphitamine
|
368
413
|
* https://github.com/jicksta/superators
|
414
|
+
* https://github.com/baweaver/xf
|
369
415
|
|
370
416
|
## Contributing
|
371
417
|
|
data/lib/pipe_operator.rb
CHANGED
@@ -13,10 +13,6 @@ module PipeOperator
|
|
13
13
|
def __pipe__(*args, &block)
|
14
14
|
Pipe.new(self, *args, &block)
|
15
15
|
end
|
16
|
-
alias | __pipe__
|
17
|
-
alias pipe __pipe__
|
18
|
-
|
19
|
-
refine(::BasicObject) { include PipeOperator }
|
20
16
|
|
21
17
|
class << self
|
22
18
|
def gem
|
@@ -47,3 +43,7 @@ module PipeOperator
|
|
47
43
|
end
|
48
44
|
end
|
49
45
|
end
|
46
|
+
|
47
|
+
BasicObject.send(:include, PipeOperator)
|
48
|
+
Kernel.alias_method(:pipe, :__pipe__)
|
49
|
+
Module.alias_method(:|, :__pipe__)
|
data/pipe_operator.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.required_ruby_version = ">= 2.0.0"
|
10
10
|
s.summary = "Elixir/Unix style pipe operations in Ruby"
|
11
11
|
s.test_files = `git ls-files -- spec/* 2>/dev/null`.split("\n")
|
12
|
-
s.version = "0.0.
|
12
|
+
s.version = "0.0.2"
|
13
13
|
|
14
14
|
s.rdoc_options = %w[
|
15
15
|
--all
|
data/spec/pipe_operator_spec.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
RSpec.describe PipeOperator do
|
2
|
-
using PipeOperator
|
3
|
-
|
4
2
|
describe ".gem" do
|
5
3
|
it "returns a Gem::Specification" do
|
6
4
|
gem = PipeOperator.gem
|
@@ -61,7 +59,7 @@ RSpec.describe PipeOperator do
|
|
61
59
|
end
|
62
60
|
|
63
61
|
it "curries arguments and blocks" do
|
64
|
-
actual = "testing"
|
62
|
+
actual = "testing".pipe.sub("test").("TEST")
|
65
63
|
expect(actual).to eq("TESTing")
|
66
64
|
|
67
65
|
actual = ["testing"].pipe.map(&:upcase).call
|
@@ -106,22 +104,22 @@ RSpec.describe PipeOperator do
|
|
106
104
|
end
|
107
105
|
|
108
106
|
it "supports pipe and stream expressions" do
|
109
|
-
actual = "-9"
|
107
|
+
actual = "-9".pipe{to_i}
|
110
108
|
expect(actual).to eq(-9)
|
111
109
|
|
112
|
-
actual = "-9"
|
110
|
+
actual = "-9".pipe{to_i; abs}
|
113
111
|
expect(actual).to eq(9)
|
114
112
|
|
115
|
-
actual = "-9"
|
113
|
+
actual = "-9".pipe{to_i; abs; Math.sqrt}
|
116
114
|
expect(actual).to eq(3)
|
117
115
|
|
118
116
|
actual = "-9".pipe { to_i | abs | Math.sqrt }
|
119
117
|
expect(actual).to eq(3)
|
120
118
|
|
121
|
-
actual = "-9"
|
119
|
+
actual = "-9".pipe{to_i; abs; Math.sqrt; to_i; send(:*, 2)}
|
122
120
|
expect(actual).to eq(6)
|
123
121
|
|
124
|
-
actual = "-16"
|
122
|
+
actual = "-16".pipe{
|
125
123
|
to_i
|
126
124
|
abs
|
127
125
|
Math.sqrt
|
@@ -174,7 +172,7 @@ RSpec.describe PipeOperator do
|
|
174
172
|
end
|
175
173
|
end
|
176
174
|
|
177
|
-
actual = Markdown.new
|
175
|
+
actual = Markdown.new.pipe.format.call("test")
|
178
176
|
expect(actual).to eq("TEST")
|
179
177
|
|
180
178
|
actual = "test".pipe(Markdown.new, &:format)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pipe_operator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LendingHome
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-12-
|
11
|
+
date: 2018-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: engineering@lendinghome.com
|
@@ -26,7 +26,6 @@ files:
|
|
26
26
|
- README.md
|
27
27
|
- Rakefile
|
28
28
|
- lib/pipe_operator.rb
|
29
|
-
- lib/pipe_operator/autoload.rb
|
30
29
|
- lib/pipe_operator/closure.rb
|
31
30
|
- lib/pipe_operator/observer.rb
|
32
31
|
- lib/pipe_operator/pipe.rb
|