LeMP Macro Reference: on_return, on_throw, on_throw_catch, on_finally

20 Mar 2016
Standard macros in the LeMP namespace

Introduction

The D programming language has a really nice feature that makes error handling easier and less, well, error-prone. It’s called the scope statement, and it’s the inspiration for the “on_” statements described here. On StackOverflow, a user explains how his code is shorter and more readable thanks to the scope statement:

sqlite3* db;
sqlite3_open("some.db", &db);
scope (exit) sqlite3_close(db);

sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, 
  "SELECT * FROM foo;", &stmt);
scope (exit) sqlite3_finalize(stmt);

// Lots of stuff...

scope (failure) 
  rollback_to(current_state);
make_changes_with(stmt);

// More stuff...

return;
sqlite3* db;
sqlite3_open("some.db", &db);
try
{
  sqlite3_stmt* stmt;
  sqlite3_prepare_v2(db, 
    "SELECT * FROM foo;", &stmt);
  try
  {
    // Lots of stuff...
    try
    {
        make_changes_with(stmt);

        // More stuff...
    }
    catch( Exception e )
    {
        rollback_to(current_state);
        throw;
    }
  }
  finally
  {
    sqlite3_finalize(stmt);
  }
}
finally
{
  sqlite3_close(db);
}

The code has turned into spaghetti, spreading the error recovery all over the shop and forcing a level of indentation for every try block. The version using scope(X) is, in my opinion, significantly more readable and easier to understand.

The Enhanced C# version of this feature uses names that (in my opinion) are easier to remember, since they are based on existing C# keywords:

The on_finally statement is perhaps the most useful out of the four. The evidence of this is in the Google Go and Apple Swift languages, both of which have comparable features, called defer in both languages. It appears that defer in swift works exactly like on_finally. However, defer in Go works differently: defer puts a clean-up action on a list and executes that list of actions at the end of the current function. In contrast, on_finally is just a shortcut for a try-finally block.

Read on for more details and examples.

on_finally

// Usage:
on_finally { Action(); }

Wraps the rest of the code of the current block in a try-finally block, and puts an action in the finally block.

var old = Environment.CurrentDirectory;
Environment.CurrentDirectory = newDir;
on_finally { Environment.CurrentDirectory = old; }

foreach (var file in Directory.GetFiles("."))
  DoSomethingWith(file);
// Output of LeMP
var old = Environment.CurrentDirectory;
Environment.CurrentDirectory = newDir;
try {
  foreach (var file in Directory.GetFiles("."))
    DoSomethingWith(file);
} finally {
  Environment.CurrentDirectory = old;
}

on_throw

// Usage:
on_throw { Action(); }
on_throw (IOException exc) { Action(exc); }

Wraps the rest of the code of the current block in a try-catch block, puts an action in the catch block, after which the exception is rethrown.

on_throw (IOException e) {
    Log("IOException: " + e.Message);
}
var str = File.ReadAllText(filename);

on_throw { Log("Parse failed"); }
return Parse(str);
// Output of LeMP
try {
  var str = File.ReadAllText(filename);

  try {
    return Parse(str);
  } catch {
    Log("Parse failed");
    throw;
  }
} catch (IOException e) {
  Log("IOException: " + e.Message);
  throw;
}

on_throw_catch

// Usage:
on_throw_catch { Action(); }
on_throw_catch (IOException exc) { Action(exc); }

Wraps the rest of the code of the current block in a try-catch block, and puts an action in the catch block. The exception is not rethrown.

on_throw_catch (IOException exc) {
    MessageBox.Show(exc.Message);
}
var data1 = File.ReadAllText(fn1);
var data2 = File.ReadAllText(fn2);
// Output of LeMP
try {
  var data1 = File.ReadAllText(fn1);
  var data2 = File.ReadAllText(fn2);
} catch (IOException exc) {
  MessageBox.Show(exc.Message);
}

on_return

// Usage:
on_return { Action(); }
on_return (result) { Action(result); }
on_return (ResultType result) { Action(result); }

In the code that follows this macro, all return statements are replaced by a block that runs a copy of this code and then returns. Example:

// Counts the number of "1" bits in an integer
public static int CountOnes(uint x)
{
    on_return(retval) { Trace.WriteLine(retval); }
    x -= ((x >> 1) & 0x55555555);
    x = (((x >> 2) & 0x33333333) + (x & 0x33333333));
    x = (((x >> 4) + x) & 0x0f0f0f0f);
    x += (x >> 8);
    x += (x >> 16);
    return (int)(x & 0x0000003f);
}
// Output of LeMP
// Counts the number of "1" bits in an integer
public static int CountOnes(uint x)
{
  x -= ((x >> 1) & 1431655765);
  x = (((x >> 2) & 858993459) + (x & 858993459));
  x = (((x >> 4) + x) & 252645135);
  x += (x >> 8);
  x += (x >> 16);
  {
    var retval = (int) (x & 63);
    Trace.WriteLine(retval);
    return retval;
  }
}

Caution: In the current implementation, if there are multiple return statements, there will be multiple copies of the handler in the output. Keep that in mind if the handler is long.

on_return only affects the current block in which it is placed. For example, the following code has two return statements, but on_return only applies to the one in the “inner” block.

foreach (var item in list) {
    on_return { Trace.WriteLine("FAIL"); }
    if (!IsValid(item))
        return false;
    ProcessValidItem(item);
}
return true;
// Output of LeMP
foreach (var item in list) {
  if (!IsValid(item)) {
    var __result__ = false;
    Trace.WriteLine("FAIL");
  
    return __result__;
  }
  ProcessValidItem(item);
}
return true;

If you use on_return within a void function or property setter, it will work without any physical return statement being present:

public static void Main()
{
    on_return { WriteLine("The End."); }
    WriteLine("The Beginning.");
}
// Output of LeMP
public static void Main()
{
  WriteLine("The Beginning.");
  WriteLine("The End.");
}

Caution: Because this is a lexical macro, it lets you do things that you shouldn’t be allowed to do. For example, { on_return { x++; } int x=0; return; } will compile although the on_return block shouldn’t be allowed to access x. Please don’t do that, because if this were a built-in language feature, it wouldn’t be allowed.

scope(…)

As an homage to D, you can use the original D syntax if you feel like it:

int ScopeExample() {
    // Same as on_finally { Cleanup() }
    scope (exit) { Cleanup(); }
    // Same as on_throw { Fail() }
    scope (failure) { Fail(); }
    // Same as on_return { Ok() }
    scope (success) { Ok(); }
    
    return NormalCodePath();
}
// Output of LeMP
int ScopeExample() {
  try {
    try {
      {
        var __result__ = NormalCodePath();
        Ok();
        return __result__;
      }
    } catch {
      Fail();
    }
  } finally {
    Cleanup();
  }
}

However, in Enhanced C# the braces are required, due to the fact that scope is not a keyword.