frypan 0.0.1 → 1.0.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/.gitignore +1 -1
- data/README.md +33 -0
- data/Rakefile +2 -0
- data/frypan.gemspec +3 -4
- data/lib/frypan.rb +191 -52
- data/lib/frypan/version.rb +1 -1
- data/tutorial/my_at.rb +17 -0
- data/tutorial/my_atd.rb +85 -0
- data/tutorial/sample_tcp_client.rb +8 -0
- data/tutorial/sample_tcp_server.rb +13 -0
- data/tutorial/tutorial.md +359 -0
- data/unit_test/test_frypan.rb +66 -0
- metadata +14 -14
- data/_README.md +0 -31
- data/bin/frypan +0 -55
- data/lib/frypan/database_sync_list.rb +0 -30
- data/lib/frypan/epg_parser.rb +0 -23
- data/lib/frypan/node.rb +0 -150
- data/lib/frypan/tuner.rb +0 -47
- data/unit_test/test_node.rb +0 -171
@@ -0,0 +1,359 @@
|
|
1
|
+
# Short Tutorial
|
2
|
+
|
3
|
+
To experience FRP and understand how to use this library, we make an imitation of Linux's atd(8) and at(1).
|
4
|
+
|
5
|
+
## atd(8) and at(1) on Linux
|
6
|
+
|
7
|
+
atd(8) is daemon to execute registered commands at specified time.
|
8
|
+
And at(1) is command to register command and time on atd(8).
|
9
|
+
|
10
|
+
For example, if you want to make a text-file named 'happy-new-century.txt' at '2101-01-01 00:00:00',
|
11
|
+
you should use at(1) command as follows.
|
12
|
+
|
13
|
+
```sh
|
14
|
+
$ at '00:00 2101-01-01'
|
15
|
+
at> touch happy-new-century.txt
|
16
|
+
at> [Ctrl-D]
|
17
|
+
|
18
|
+
```
|
19
|
+
|
20
|
+
## TCP/IP
|
21
|
+
|
22
|
+
Our imitation of atd(8) and at(1) (respectively named my_atd and my_at) talk with each other by TCP/IP.
|
23
|
+
|
24
|
+
We don't need a good comprehension of TCP/IP because we apply TCP/IP by using Ruby's standard library.
|
25
|
+
|
26
|
+
To know easy usage of Ruby's TCP/IP library, understand following server-program.
|
27
|
+
```ruby
|
28
|
+
#!/usr/bin/env ruby
|
29
|
+
|
30
|
+
require 'socket'
|
31
|
+
|
32
|
+
socket = TCPServer.new(20000)
|
33
|
+
|
34
|
+
loop do
|
35
|
+
connection = socket.accept
|
36
|
+
connection.write(connection.gets)
|
37
|
+
connection.close
|
38
|
+
end
|
39
|
+
|
40
|
+
socket.close
|
41
|
+
|
42
|
+
```
|
43
|
+
This program works as just 'echo-server' which is receiving one-line-string from client and sending same string to client.
|
44
|
+
|
45
|
+
An example of client-program talking with above server-program is shown below.
|
46
|
+
```ruby
|
47
|
+
#!/usr/bin/env ruby
|
48
|
+
|
49
|
+
require 'socket'
|
50
|
+
|
51
|
+
socket = TCPSocket.open("localhost", 20000)
|
52
|
+
socket.write(STDIN.gets)
|
53
|
+
puts socket.gets
|
54
|
+
socket.close
|
55
|
+
```
|
56
|
+
|
57
|
+
## Make `my_atd.rb`
|
58
|
+
|
59
|
+
Immediately, let's make my_atd.
|
60
|
+
|
61
|
+
At first, write statements to launch tcp-server and print message.
|
62
|
+
```ruby
|
63
|
+
socket = TCPServer.new(20000)
|
64
|
+
puts "my_atd listening port: 20000..."
|
65
|
+
```
|
66
|
+
|
67
|
+
Incidentally, define a Proc to accept and get client's request.
|
68
|
+
```ruby
|
69
|
+
accepter = proc do
|
70
|
+
connection = socket.accept
|
71
|
+
c, t = connection.gets, connection.gets
|
72
|
+
{command_str: c.chomp, time_str: t.chomp, connection: connection}
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
A Proc to get current time is too.
|
77
|
+
```ruby
|
78
|
+
timer = proc do
|
79
|
+
Time.now
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Here, use our heads a little.
|
84
|
+
What should my_atd do as output?
|
85
|
+
|
86
|
+
The answer is maybe executing command and responding to client.
|
87
|
+
So if `commands` is array of command-string and `responses` is array of
|
88
|
+
`Hash(:connection => client-socket, :message => response-string)`,
|
89
|
+
we can define Procs to execute command and respond to client.
|
90
|
+
```ruby
|
91
|
+
exec_commands = proc do |commands|
|
92
|
+
commands.each do |command|
|
93
|
+
puts "execute command '#{command}'"
|
94
|
+
pid = Process.spawn(command, :out => STDOUT, :err => STDERR)
|
95
|
+
puts "PID = #{pid}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
respond = proc do |responses|
|
100
|
+
responses.each do |res|
|
101
|
+
res[:connection].write(res[:message])
|
102
|
+
res[:connection].close
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
And define a Proc to bind these two Proc if `out` is
|
108
|
+
`Hash(:commands => commands, :responses => responses)`.
|
109
|
+
```ruby
|
110
|
+
output_processor = proc do |out|
|
111
|
+
exec_commands.call(out[:commands])
|
112
|
+
respond.call(out[:responses])
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
By the way, there is necessity for my_atd to retain registered commands (and times.)
|
117
|
+
And we suddenly realize that we can separate registered commands into three parts:
|
118
|
+
|
119
|
+
* `waitings`: commands waiting for execution
|
120
|
+
* `launcheds`: commands which should be executed right now
|
121
|
+
* `rejecteds`: commands which are requested to add but rejected
|
122
|
+
|
123
|
+
And then we can express each parts by ruby's data:
|
124
|
+
|
125
|
+
* `waitings`: `Hash(:command_str => command-string, :time => time)`
|
126
|
+
* `launcheds`: `Hash(:command_str => command-string, :time => time)`
|
127
|
+
* `rejecteds`: `Hash(:command_str => command-string, :time_str => time-string, :connection => requester-client-socket)`
|
128
|
+
|
129
|
+
If `ct` is current time, `cs` is Array of request which is
|
130
|
+
`Hash(command_str: command-string, :time_str => time-string, :connection => client-socket)` and
|
131
|
+
`commands` is current-state which is `Hash(:waitings, :launcheds, :rejecteds)`,
|
132
|
+
we can update `commands` to next-state by one function `update_commands` as follows:
|
133
|
+
|
134
|
+
```
|
135
|
+
next_commands = update_commands(commands, ct, cs)
|
136
|
+
```
|
137
|
+
|
138
|
+
`update_commands` can be, for example, wrote as follows:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
update_commands = proc do |commands, ct, cs|
|
142
|
+
{
|
143
|
+
waitings: acc[:waitings].select{|c| c[:time] > ct} +
|
144
|
+
cs.select{|c| acceptable?(c)}.map{|c| c.update(time: Time.parse(c[:time_str]))},
|
145
|
+
launcheds: acc[:waitings].select{|c| c[:time] <= ct},
|
146
|
+
rejecteds: cs.reject{|c| acceptable?(c)}
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
# helper
|
151
|
+
def acceptable?(c)
|
152
|
+
Time.parse(c[:time_str])
|
153
|
+
rescue
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Now, we can make data which is `Hash(:commands => commands, :responses => responses)` which is used as argument of Proc named `output-processor`.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
output_formatize = proc do |coms, cs|
|
162
|
+
ress = cs.map do |c|
|
163
|
+
if coms[:rejecteds].include?(c)
|
164
|
+
msg = "Your specified time '#{c[:time_str]}' seems invalid.\n"
|
165
|
+
else
|
166
|
+
msg = "OK.\n"
|
167
|
+
end
|
168
|
+
{message: msg, connection: c[:connection]}
|
169
|
+
end
|
170
|
+
{commands: coms[:launcheds].map{|c| c[:command_str]}, responses: ress}
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
Where `coms` is updated commands and `cs` is Array of request.
|
175
|
+
|
176
|
+
Phew, that was steep road. But our task has almost been completed!
|
177
|
+
So far we only define two input-proc, one output-proc, and two pure-function, but we didn't use `frypan` yet.
|
178
|
+
|
179
|
+
Do you remember `frypan`?
|
180
|
+
Yes, it's my library to do FRP.
|
181
|
+
|
182
|
+
Do you know FRP?
|
183
|
+
Don't worry, experience is the best teacher:).
|
184
|
+
|
185
|
+
|
186
|
+
As finishing of all, we do FRP by `frypan`.
|
187
|
+
```ruby
|
188
|
+
require 'frypan'
|
189
|
+
|
190
|
+
S = Frypan::Signal
|
191
|
+
|
192
|
+
# Input-Signal representing new clients.
|
193
|
+
new_clients = S.async_input(5, &accepter)
|
194
|
+
|
195
|
+
# Input-Signal representing current time.
|
196
|
+
current_time = S.input(&timer)
|
197
|
+
|
198
|
+
# Foldp-Signal representing registered commands.
|
199
|
+
initial = {waitings: [], launcheds: [], rejecteds: []}
|
200
|
+
commands = S.foldp(initial, current_time, new_clients, &update_command)
|
201
|
+
|
202
|
+
# Lift-Signal representing output datas.
|
203
|
+
main = S.lift(commands, new_clients, &output_formatize)
|
204
|
+
|
205
|
+
# FRP's main-loop.
|
206
|
+
Frypan::Reactor.new(main).loop(&output_processor)
|
207
|
+
```
|
208
|
+
|
209
|
+
All of my_atd is completed with this!
|
210
|
+
It's true! Completely Executable program (my_atd.rb) is here:
|
211
|
+
```ruby
|
212
|
+
#!/usr/bin/env ruby
|
213
|
+
|
214
|
+
require 'frypan'
|
215
|
+
require 'socket'
|
216
|
+
require 'time'
|
217
|
+
|
218
|
+
S = Frypan::Signal
|
219
|
+
|
220
|
+
socket = TCPServer.new(20000)
|
221
|
+
puts "my_atd listening port: 20000..."
|
222
|
+
|
223
|
+
accepter = proc do
|
224
|
+
connection = socket.accept
|
225
|
+
c, t = connection.gets, connection.gets
|
226
|
+
{command_str: c.chomp, time_str: t.chomp, connection: connection}
|
227
|
+
end
|
228
|
+
|
229
|
+
timer = proc do
|
230
|
+
Time.now
|
231
|
+
end
|
232
|
+
|
233
|
+
update_commands = proc do |commands, ct, cs|
|
234
|
+
{
|
235
|
+
waitings: acc[:waitings].select{|c| c[:time] > ct} +
|
236
|
+
cs.select{|c| acceptable?(c)}.map{|c| c.update(time: Time.parse(c[:time_str]))},
|
237
|
+
launcheds: acc[:waitings].select{|c| c[:time] <= ct},
|
238
|
+
rejecteds: cs.reject{|c| acceptable?(c)}
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
output_formatize = proc do |coms, cs|
|
243
|
+
ress = cs.map do |c|
|
244
|
+
if coms[:rejecteds].include?(c)
|
245
|
+
msg = "Your specified time '#{c[:time_str]}' seems invalid.\n"
|
246
|
+
else
|
247
|
+
msg = "OK.\n"
|
248
|
+
end
|
249
|
+
{message: msg, connection: cl[:connection]}
|
250
|
+
end
|
251
|
+
{commands: coms[:launcheds].map{|c| c[:command_str]}, responses: ress}
|
252
|
+
end
|
253
|
+
|
254
|
+
exec_commands = proc do |commands|
|
255
|
+
commands.each do |command|
|
256
|
+
puts "execute command '#{command}'"
|
257
|
+
pid = Process.spawn(command, :out => STDOUT, :err => STDERR)
|
258
|
+
puts "PID = #{pid}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
respond = proc do |responses|
|
263
|
+
responses.each do |res|
|
264
|
+
res[:connection].write(res[:message])
|
265
|
+
res[:connection].close
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
output_processor = proc do |out|
|
270
|
+
exec_commands.call(out[:commands])
|
271
|
+
respond.call(out[:responses])
|
272
|
+
end
|
273
|
+
|
274
|
+
# helper method
|
275
|
+
def acceptable?(c)
|
276
|
+
Time.parse(c[:time_str])
|
277
|
+
rescue
|
278
|
+
nil
|
279
|
+
end
|
280
|
+
|
281
|
+
# Input-Signal representing new clients.
|
282
|
+
new_clients = S.async_input(5, &accepter)
|
283
|
+
|
284
|
+
# Input-Signal representing current time.
|
285
|
+
current_time = S.input(&timer)
|
286
|
+
|
287
|
+
# Foldp-Signal representing registered commands.
|
288
|
+
initial = {waitings: [], launcheds: [], rejecteds: []}
|
289
|
+
commands = S.foldp(initial, current_time, new_clients, &update_command)
|
290
|
+
|
291
|
+
# Lift-Signal representing output datas.
|
292
|
+
main = S.lift(commands, new_clients, &output_formatize)
|
293
|
+
|
294
|
+
# FRP's main-loop.
|
295
|
+
Frypan::Reactor.new(main).loop(&output_processor)
|
296
|
+
```
|
297
|
+
|
298
|
+
## Make `my_at.rb`
|
299
|
+
|
300
|
+
We need not to use `frypan` in `my_at.rb`.
|
301
|
+
So, we make `my_at.rb` quickly by sloppy job.
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
#!/usr/bin/env ruby
|
305
|
+
|
306
|
+
require 'socket'
|
307
|
+
|
308
|
+
loop do
|
309
|
+
print "execution-command> "
|
310
|
+
com = STDIN.gets
|
311
|
+
print "execution-time> "
|
312
|
+
time = STDIN.gets
|
313
|
+
|
314
|
+
break unless com && time
|
315
|
+
|
316
|
+
socket = TCPSocket.open("localhost", 20000)
|
317
|
+
socket.write(com + time)
|
318
|
+
puts "response: #{socket.gets}"
|
319
|
+
socket.close
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
## Let's play `my_atd` and `my_at`
|
324
|
+
|
325
|
+
At first, make `my_atd.rb` and `my_at.rb` be executable.
|
326
|
+
```sh
|
327
|
+
# chmod +x ./my_atd.rb ./my_at.rb
|
328
|
+
```
|
329
|
+
|
330
|
+
And launch `my_atd` server!
|
331
|
+
```sh
|
332
|
+
$ ./my_atd.rb
|
333
|
+
my_atd listening port: 20000...
|
334
|
+
```
|
335
|
+
|
336
|
+
Open another terminal and run client program `my_at`.
|
337
|
+
```sh
|
338
|
+
$ ./mu_at.rb
|
339
|
+
execution-command>
|
340
|
+
```
|
341
|
+
|
342
|
+
Type your wishing task!
|
343
|
+
```sh
|
344
|
+
execution-command> touch hello-at.txt
|
345
|
+
execution-time> 2015/03/15 00:00:00 JST
|
346
|
+
response: OK.
|
347
|
+
```
|
348
|
+
At "2015/03/15 00:00:00 JST", you are going to discover a file named `hello-at.txt` on your directory!
|
349
|
+
|
350
|
+
## Thank you
|
351
|
+
|
352
|
+
Thank you for your reading!
|
353
|
+
We have made one of pratical application by `frypan`.
|
354
|
+
FRP mkes it easy to program Real-time systems such as `atd`.
|
355
|
+
|
356
|
+
|
357
|
+
|
358
|
+
|
359
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'frypan'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
module Frypan
|
6
|
+
module UnitTest
|
7
|
+
class FrypanTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
S = Frypan::Signal
|
10
|
+
|
11
|
+
def test_const
|
12
|
+
c = S::Const.new(:obj)
|
13
|
+
assert_equal([:obj, :obj, :obj], Reactor.new(c).take(3))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_input
|
17
|
+
a = (0..100).to_a
|
18
|
+
i = S::Input.new{a.shift}
|
19
|
+
assert_equal([0, 1, 2], Reactor.new(i).take(3))
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_input_thread
|
23
|
+
a = (0..1000).to_a
|
24
|
+
i = S::InputThread.new(2){a.shift}
|
25
|
+
val = Reactor.new(i).take(100).map(&:first).inject(&:+).take(10)
|
26
|
+
assert_equal((0..9).to_a, val)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_lifte
|
30
|
+
c1, c2 = S::Const.new(:c1), S::Const.new(:c2)
|
31
|
+
l = S::Lift.new(c1, c2){|a, b| a.to_s + b.to_s}
|
32
|
+
assert_equal(["c1c2", "c1c2"], Reactor.new(l).take(2))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_foldp
|
36
|
+
c1, c2 = S::Const.new(:c1), S::Const.new(:c2)
|
37
|
+
f = S::Foldp.new("", c1, c2){|acc, a, b| acc + a.to_s + b.to_s}
|
38
|
+
assert_equal(["c1c2", "c1c2c1c2"], Reactor.new(f).take(2))
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_lift_memorization
|
42
|
+
seff = []
|
43
|
+
l = S.lift(S.const(1), S.const(2)){|a, b| seff << a + b; a + b}
|
44
|
+
assert_equal([3, 3, 3, 3, 3], Reactor.new(l).take(5))
|
45
|
+
assert_equal([3], seff)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_foldp_memorization
|
49
|
+
seff = []
|
50
|
+
f = S.foldp(0, S.const(1), S.const(2)){|acc, a, b| seff << acc*a*b; acc*a*b}
|
51
|
+
assert_equal([0, 0, 0, 0, 0], Reactor.new(f).take(5))
|
52
|
+
assert_equal([0, 0], seff)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_utility
|
56
|
+
l = S.const(0)
|
57
|
+
.lift{|a| a + 1}
|
58
|
+
.foldp(0){|acc, a| acc + a}
|
59
|
+
.foldp([]){|acc, a| acc + [a]}
|
60
|
+
.select{|a| a.even?}
|
61
|
+
.map{|a| a * a}
|
62
|
+
assert_equal([[], [4], [4], [4, 16], [4, 16], [4, 16, 36]], Reactor.new(l).take(6))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frypan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- sawaken
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,29 +38,28 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
|
-
description:
|
41
|
+
description:
|
42
42
|
email:
|
43
43
|
- sasasawada@gmail.com
|
44
|
-
executables:
|
45
|
-
- frypan
|
44
|
+
executables: []
|
46
45
|
extensions: []
|
47
46
|
extra_rdoc_files: []
|
48
47
|
files:
|
49
48
|
- ".gitignore"
|
50
49
|
- Gemfile
|
51
50
|
- LICENSE.txt
|
51
|
+
- README.md
|
52
52
|
- Rakefile
|
53
|
-
- _README.md
|
54
|
-
- bin/frypan
|
55
53
|
- frypan.gemspec
|
56
54
|
- lib/frypan.rb
|
57
|
-
- lib/frypan/database_sync_list.rb
|
58
|
-
- lib/frypan/epg_parser.rb
|
59
|
-
- lib/frypan/node.rb
|
60
|
-
- lib/frypan/tuner.rb
|
61
55
|
- lib/frypan/version.rb
|
62
|
-
-
|
63
|
-
|
56
|
+
- tutorial/my_at.rb
|
57
|
+
- tutorial/my_atd.rb
|
58
|
+
- tutorial/sample_tcp_client.rb
|
59
|
+
- tutorial/sample_tcp_server.rb
|
60
|
+
- tutorial/tutorial.md
|
61
|
+
- unit_test/test_frypan.rb
|
62
|
+
homepage: https://github.com/sawaken/tiny_frp2
|
64
63
|
licenses:
|
65
64
|
- MIT
|
66
65
|
metadata: {}
|
@@ -83,5 +82,6 @@ rubyforge_project:
|
|
83
82
|
rubygems_version: 2.2.2
|
84
83
|
signing_key:
|
85
84
|
specification_version: 4
|
86
|
-
summary:
|
85
|
+
summary: Very small and simple library to do Functional Reactive Programming in Ruby
|
86
|
+
in a similar way to Elm.
|
87
87
|
test_files: []
|