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.
@@ -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: []