lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


I've mentioned non-local exits as first-class objects a few times on this list, and the reaction has always been somewhat guarded. I think this might be because the concept is fairly unfamiliar to people.

I tried to find some Dylan tutorials on the issue but there is an unfortunate lack of Dylan material on the web, although you can read about the Dylan implementation in the online DRM (it's got quite a few more features than the one I proposed.)

So I'll just scrawl a few possibly motivating examples. I personally think that they are a brilliant idea, because they are a single easily-implemented primitive which can be used to implement a wide variety of useful control structures.

A common usage is simply to provide a cleanup clause for code execution:

function do_with_lock(fn, ...)
  local a = lock()
  block ()
    return fn(...)
  finally
    a:release()
  end
end

The nle is not even used in the above example, but it still manages to guarantee that the lock is released (and without being awkward about return values, which is a nice plus). This could just as well have been written using the nle:

function do_with_lock(fn, ...)
  local a = lock()
  return block (p)
    p(fn(...))
  finally
    a:release()
  end
end

But the nle is mostly useful for being passed into called functions, as I demonstrated with my first example a couple of messages ago.

nle's can also be used to implement an exception system. We're used to exceptions being handled after the stack is rolled back, but this implementation executes the exceptions on top of the stack, somewhat like Lua's error functions. Error functions need to execute on the top of the stack in order to be able to get a stack trace, but there are other good reasons to do it, notably the fact that it gives us the opportunity to handle an error and simply continue.

local function try(exception, func, arg)
  local val, err = func(arg)
  if val then
    return val
   else
    return try(exception, func, exception(arg, err))
  end
end

function handle_files(list)
  local f
  return block (p)
    -- The exception handler is defined here.
    local function nofile(filename, reason)
      io.write("I couldn't open ",filename," because ",reason, "\n",
               "What file should I try? ")
      filename = io.read()
      -- we invoke the nle if they don't have any useful suggestions
      if filename == "" then p(nil, "Couldn't open "..filename) end
      return filename
    end

    for _, filename in ipairs(list) do
      f = try(nofile, io.open, filename)
      ... do something with f
      f:close()
    end
    return true
  finally
    if f then f:close()
  end
end


What we would probably do, rather than the somewhat manual approach. is dynamically assign the nofile function (effectively, a restartable exception handler) to the named exception thrown by io.open. (That is, assuming we'd written the io library to use named exceptions.) Again, finally clauses can be used to maintain the mapping between named exceptions and exception handlers:

  local old_nofile = Exceptions.nofile
  ...
  return block (p)
    Exceptions.nofile = function(filename, reason) -- as above -- end
    ...
  finally
    Exceptions.nofile = old_nofile
  end

Clearly that can be simplified with a functional, too:

function Catching(exception, handler, func, ...)
  local temp = Exceptions[exception]
  Exceptions[exception] = handler
  block ()
    return func(...)
  finally
    Exceptions[exception] = temp
  end
end

Catching("nofile", my_nofile, handle_filelist, files)

Dylan actually implements exceptions in precisely this fashion, but the macro facility allows it to be written in a non-functional syntax, using exception clauses in the block expression. (It's just translated into the equivalent of the above code, though)