frypan 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|