Bug #14472
closed`File::open` closes the file too early when used with callcc
Description
First of all, I know callcc is deprecated feature and I'm not in trouble with this bug.
I was just curious and happened to find this bug.
Bug Description¶
This code throws an IOError, but expected to exit normally:
require 'continuation'
f1 = callcc {|k| File::open("test1", 'w') {|f| k.(f)}}
f1.write("hello")
$ ruby openfile_cont.rb
/usr/lib/x86_64-linux-gnu/ruby/2.3.0/continuation.so: warning: callcc is obsolete; use Fiber instead
openfile_cont.rb:14:in `write': closed stream (IOError)
from openfile_cont.rb:14:in `<main>'
I think this is a bug because the code above must be the same as code below, which works fine:
File::open("test1", 'w') {|f1| f1.write("hello")}
In fact, an equivalent scheme code works fine:
(let* ((f1 (call/cc (lambda (k) (call-with-output-file "test1" k)))))
(display "hello" f1))
$ gosh openfile_cont.scm
$ cat test1
hello
Importance¶
Again, I'm not in trouble with this bug.
The bugging code is useful to rewrite nested open blocks to flat style like this:
File::open("test1", 'w') do |f1|
File::open("test2", 'w') do |f2|
f1.write("hello")
f2.write("hello")
end
end
f1 = callcc {|k| File::open("test1", 'w') {|f| k.(f)}}
f2 = callcc {|k| File::open("test2", 'w') {|f| k.(f)}}
f1.write("hello")
f2.write("hello")
Or, even code that cannot be written with nested opens can be written with callcc:
["test1", "test2", "test3"].map{|path| callcc {|k| File::open(path, 'w') {|f| k.(f)}}}.each do |f|
f.write("hello")
end
Again, equivalent scheme code works fine:
(let ((paths '("test1" "test2" "test3")))
(let ((ports (map (lambda (path) (call/cc (lambda (k) (call-with-output-file path k)))) paths)))
(dolist (port ports)
(display "hello" port))))
Updated by nobu (Nobuyoshi Nakada) over 7 years ago
- Related to Bug #9105: callcc による不整合(例:Hash) added
Updated by nobu (Nobuyoshi Nakada) over 7 years ago
- Status changed from Open to Rejected
blackenedgold (Sunrin SHIMURA) wrote:
This code throws an IOError, but expected to exit normally:
callcc rollbacks ensures.
The bugging code is useful to rewrite nested
openblocks to flat style like this:f1 = callcc {|k| File::open("test1", 'w') {|f| k.(f)}} f2 = callcc {|k| File::open("test2", 'w') {|f| k.(f)}} f1.write("hello") f2.write("hello")
You can do it by File.open without a block.
If callcc weren't fire ensures, it's unpredictable when f1 and f2 will get closed.
It's same as w/o-block form open.
Updated by blackenedgold (Sunrin SHIMURA) over 7 years ago
You can do it by
File.openwithout a block.
I know that. Again, I'm not in trouble.
callccrollbacks ensures.
I understand it isn't a bug, but an expected behaviour.
Then the question is why this behaviour?
If
callccweren't fire ensures, it's unpredictable whenf1andf2will get closed.
Why? Is firing when control reached to the end not sufficient?
In scheme, closing port before reaching the end is explicitly forbidden
In #9105 case, error was occurring because control reached the end twice and ensure was fired twice. I think it's natural
In this case, error is occurring because ensure is fired even though control doesn't reach the end. I think it's unnatural.
Updated by blackenedgold (Sunrin SHIMURA) over 7 years ago
I used raw continuations for the simplicity of explanation but I found it was a bit confusing (and I was also confused).
... code above must be the same as code below, which works fine:
It was wrong. and
You can do it by
File.openwithout a block.
is absolutely correct.
I wrote wrong code. I intended to write code with delimited continuations like below.
require 'continuation'
def shift
callcc {|c1| $ctn.(yield(proc {|v| callcc {|c2| $ctn = c2; c1.(v) } })) }
end
def reset
callcc {|c| $ctn = c; v = yield; $ctn.(v) }
end
reset do
f1 = shift {|k| File::open("test1", 'w') {|f| k.(f)}}
f1.write("hello")
end
This is exactly the same as
File::open("test1", 'w') {|f1| f1.write("hello")}
including file is closed after the write is done.