frypan 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'socket'
4
+
5
+ socket = TCPServer.new(20000)
6
+
7
+ loop do
8
+ connection = socket.accept
9
+ connection.write(connection.gets)
10
+ connection.close
11
+ end
12
+
13
+ socket.close
@@ -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.1
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-02-04 00:00:00.000000000 Z
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
- - unit_test/test_node.rb
63
- homepage: ''
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: Yet another recording scheduler.
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: []