Crystal: go like defer

Created on 12 May 2016  路  11Comments  路  Source: crystal-lang/crystal

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
draft compiler

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 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.

All 11 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Sija picture Sija  路  3Comments

asterite picture asterite  路  3Comments

costajob picture costajob  路  3Comments

nabeelomer picture nabeelomer  路  3Comments

pbrusco picture pbrusco  路  3Comments