pipe_operator 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|