Side effects in Meta

August 12th, 2009 | Tags:

If Meta is to be the static meta-programming language of choice (I believe Ruby to be the dynamic meta-programming language of choice), we must be able to run lots of user code at compile time. Let’s say we have this function:

square(x) = x*x;

If the compiler sees the expression “square(4)” in the code, it can replace it with “16″ at compile time, correct? Well what about this equivalent function?:

square(x) = system(sprintf("echo %d*%d |bc", x, x));

Aha! Side effects are a problem! This now gets a lot messier, since the compiler needs to know which functions it can run at compile time and for which functions it must generate run-time instructions. Language compilers are typically very conservative at estimating this, but this will not do for Meta!

We can say the first function is “pure”. In other words, given the function and the same input parameters, it will produce the same result. It keeps no internal state and uses no operating system objects such as files or sockets. In other words, it is not only a computer programming function, it is also a mathematical function.

Enter the world of the Scheme language for a moment. By unenforced convention, Scheme functions which mutate objects have names which end with an exclamation point. They jump out at the programmer, saying “I’m actually modifying an object.” This isn’t exactly consistent with the purity concept, but we are going to steal the idiom. In Meta, all non-pure functions must end with ‘!’. This not only solves our problem about running user code at compile time, but it allows programmers to reason much better about the code at a glance and helps us with concurrent programming.

The rules are:

  • All impure library functions will be labeled with ‘!’.
  • Functions labeled with ‘!’ aren’t required to have side effects:
    g!() = 0; // g!() actually has no side effects.  Perhaps it will later.
  • Functions which directly call functions labeled with ‘!’ must also be labeled with ‘!’.
    g() = f!();  // error, g() cannot call f!()
    g!() = f!(); // OK, g!() can call f!()
  • Impure functions may not be passed to functions which don’t expect them.
    // OK, h is pure
    let f(g) = g() in
    let h() = 0 in
    f(h); 
     
    // Compile error
    let f(g) = g() in
    let h!() = 0 in
    f(h!);
     
    // OK, f(g!) indicates that g! can be impure.  Note that we
    // haven't labeled f because it doesn't actually invoke g!.
    let f(g!) = g! in
    let h!() = 0 in
    f(h!);
     
    // OK, now we must label f
    let f!(g!) = g!() in
    let h!() = 0 in
    f!(h!);
     
    // We can always pass a pure function where an impure one is acceptable.
    let f!(g!) = g!() in
    let h() = 0 in
    f!(h);
  • Functions whose purity depends on the parameters passed may annotate parameters with ‘!?’. In these cases, the function itself need not be annotated with ‘!’, but whether a particular invocation will require the enclosing function to be labeled will be determined from the parameters passed.
    // This expression does not require the function containing it
    // to be labeled with '!'.
    let h() = 0 in
    let f(g!?) = g!?() in
    f(h);
     
    // This expression does.
    let h!() = 0 in
    let f(g!?) = g!?() in
    f(h!);
No comments yet.
TOP