Hi,
I've been reading a lot of go code lately and I really like the defer method, is this something that crystal could/should support?
Multiple defers would append the statements to the ensure block.
If an ensure block is also present for the method, I would expect that the defers are also appended to that block.
def read_file
file = File.open("somefile.txt", "r")
defer { file.close }
defer { puts "file is closed" }
puts file.read
ensure
puts "I will clean up"
end
# Would be the same as
def read_file
file = File.open("somefile.txt", "r")
puts file.read
ensure
puts "I will clean up"
file.close
puts "file is closed"
end
Well, Crystal inherited the block style way of doing this from Ruby, so the preferred version would be
def read_file
File.open("somefile.txt", "r") do |file|
puts file.read
end
ensure
puts "I will clean up"
puts "file is closed"
end
And I would still prefer it even when defer exists.
Maybe I'm missing a more compelling usecase that isn't coverable by yielding methods?
If you only have one block that is indeed the preferred version, but could become a bit messy if you have multiple nested blocks.
MUTEX = Mutex.new
def write_lines
MUTEX.synchronise do
File.open("somefile.txt", "w") do |file|
10.times do |i|
file.write "Line: #{i}"
end
end
puts "lines written"
end
end
def write_lines
MUTEX.lock
defer { MUTEX.unlock }
file = File.open("somefile.txt", "w")
defer { file.close }
10.times do |i|
file.write "Line: #{i}"
end
puts "lines written"
end
Why would you prefer to write the second version? It's longer and bug prone because you can forget to write the defer. Using the block form it's impossible to forget to close a file or to unlock a mutex (well, you can forget to use the block, but it's an idiom that's very common in a lot of APIs). Also, reading the code is straightforward, with defer I... don't know how to read that code (it's not linear).
I don't see a compelling reason to add this. Plus it will mean there will be two ways to do the same thing.
I'd say Go has defer because it doesn't have true exception, but that's not true because D has something similar, and it has exceptions. But D doesn't have blocks, so maybe that's the reason.
I write PoC just now, however I won't use it.
module Defer
@@indexes = [] of Int32
@@defers = [] of ->
@[AlwaysInline]
def self.defer(&blk : ->)
@@defers.push blk
end
@[AlwaysInline]
def self.scope
@@indexes.push @@defers.size
with Defer yield
ensure
index = @@indexes.pop
while @@defers.size > index
@@defers.pop.call
end
end
end
MUTEX = Mutex.new
def write_lines; Defer.scope do
MUTEX.lock
defer { MUTEX.unlock }
file = File.open("somefile.txt", "w")
defer { file.close }
10.times do |i|
file.write "Line: #{i}"
end
puts "lines written"
end; end
All these cases are still relatively simple, but I would argue that it could sometimes be easier to read the core of the method and in this case that's to write 10 lines of "Line:
Maybe I've been reading too much go that I started to like certain aspects, lots of times I'd like to poke my eyes out while reading go ;-)
I've worked a lot using the "deferish" style before over the years, and the Crystal style of blocks is kind of new to me, and I'm already in love with it! It's so much cleaner, less error prone, etc. There's no reason to "go back" to worse methods!
I also checked this with @waj and he doesn't like defer either (or, better put, doesn't find it useful since we have blocks and ensure). So I'm closing this. (But please keep suggesting features, we do like some things from other language :-))
Nope that's it now, I'm removing my left pad crystal lib and I will switch to go :-P
All of blocks, defer, and ensure are useful: consider temporary files, where the caller is on the hook to delete them. Without defer, simple code like this is wrong, because things can go wrong in tempfile that raise exceptions that do not result in a valid file being created.
def method
tf = File.tempfile do |tf|
tf.print("pre-change")
end
[...more code]
ensure
tf.delete
end
And it gets worse with more branches, as you might imagine, and more code before the File.tempfile expression.
defer gets the combinatorics right in more cases, with less notation:
def method
tf = File.tempfile do |tf|
tf.print("pre-change")
end
defer { tf.delete }
[...more code]
end
def do_thing
with_tmpfile do |tf|
do_stuff_to_file(tf)
do_other_stuff
end
end
def with_tmpfile
tf = File.tempfile do |tf|
yield tf
end
ensure
tf.delete
end
def do_stuff_to_file(tf)
[...more code]
end
def do_other_stuff
[...more code]
end
If you run into the risk of loosing track there, it's time to break stuff up into much smaller methods.
http://www.principles-wiki.net/principles:single_level_of_abstraction
Bloated. I think that crystal's nil checking does a lot for this, though.
Most helpful comment
Why would you prefer to write the second version? It's longer and bug prone because you can forget to write the defer. Using the block form it's impossible to forget to close a file or to unlock a mutex (well, you can forget to use the block, but it's an idiom that's very common in a lot of APIs). Also, reading the code is straightforward, with
deferI... don't know how to read that code (it's not linear).I don't see a compelling reason to add this. Plus it will mean there will be two ways to do the same thing.
I'd say Go has defer because it doesn't have true exception, but that's not true because D has something similar, and it has exceptions. But D doesn't have blocks, so maybe that's the reason.