13.3. Use cases and examples

Use cases are at least the same as aspect oriented programming (AOP) and the decorator design pattern, but your imagination is your limit. Some are presented here for illustration.

13.3.1. Logging

Logging is a classical example use case of AOP. See the Principles and syntax Section 13.2, “Principles and syntax” section for an example.

13.3.2. Pre/post conditions checking

Decorators can be used to check pre-conditions, that is conditions that must hold for arguments, and post-conditions, that is conditions that must hold for returned values, of a function.

Indeed, a decorated can execute code before delegating to the decorated function, of after the delegation.

The module gololang.Decorators provide two decorators and several utility functions to check pre and post conditions.

checkResult is a parametrized decorator taking a checker as parameter. It checks that the result of the decorated function is valid.

checkArguments is a variable arity function, taking as much checkers as the decorated function arguments. It checks that the arguments of the decorated function are valid according to the corresponding checker (1st argument checked by 1st checker, and so on).

A checker is a function that raises an exception if its argument is not valid (e.g. using require) or returns it unchanged, allowing checkers to be chained using the andThen method.

As an example, one can check that the arguments and result of a function are integers with:

function isInteger = |v| {
  require(v oftype Integer.class, v + "is not an Integer")
  return v
}

@checkResult(isInteger)
@checkArguments(isInteger, isInteger)
function add = |a, b| -> a + b

or that the argument is a positive integer:

function isPositive = |v| {
  require(v > 0, v + "is not > 0")
  return v
}

@checkArguments(isInteger: andThen(isPositive))
function inv = |v| -> 1.0 / v

Of course, again, you can take shortcuts:

let isPositiveInt = isInteger: andThen(isPositive)

@checkResult(isPositiveInt)
@checkArguments(isPositiveInt)
function double = |v| -> 2 * v

or even

let myCheck = checkArguments(isInteger: andThen(isPositive))

@myCheck
function inv = |v| -> 1.0 / v

@myCheck
function mul = |v| -> 10 * v

Several factory functions are available in gololang.Decorators to ease the creation of checkers:

  • any is a void checker that does nothing. It can used when you need to check only some arguments of a n-ary function.
  • asChecker is a factory that takes a boolean function and an error message and returns the corresponding checker. For instance:
let isPositive = asChecker(|v| -> v > 0, "is not positive")
  • isOfType is a factory function that returns a function checking types, e.g.
let isInteger = isOfType(Integer.class)

The full set of standard checkers is documented in the generated golodoc (hint: look for doc/golodoc in the Golo distribution).

13.3.3. Locking

As seen, decorator can be used to wrap a function call between checking operation, but also between a lock/unlock in a concurrent context:

import java.util.concurrent.locks

function withLock = |lock| -> |fun| -> |args...| {
  lock: lock()
  try {
    return fun: invokeWithArguments(args)
  } finally {
    lock: unlock()
  }
}

let myLock = ReentrantLock()

@withLock(myLock)
function foo = |a, b| {
  return a + b
}

13.3.4. Memoization

Memoization is the optimization technique that stores the results of a expensive computation to return them directly on subsequent calls. It is quite easy, using decorators, to transform a function into a memoized one. The decorator creates a closure on a hashmap, and check the existence of the results before delegating to the decorated function, and storing the result in the hashmap if needed.

Such a decorator is provided in the gololang.Decorators module, presented here as an example:

function memoizer = {
  var cache = map[]
  return |fun| {
    return |args...| {
      let key = [fun: hashCode(), Tuple(args)]
      if (not cache: containsKey(key)) {
        cache: add(key, fun: invokeWithArguments(args))
      }
      return cache: get(key)
    }
  }
}

The cache key is the decorated function and its call arguments, thus the decorator can be used for every module functions. It must however be put in a module-level state, since in the current implementation, the decoration is invoked at each call. For instance:

let memo = memoizer()

@memo
function fib = |n| {
  if n <= 1 {
    return n
  } else {
    return fib(n - 1) + fib(n - 2)
  }
}

@memo
function fact = |n| {
  if n == 0 {
    return 1
  } else {
    return n * fact(n - 1)
  }
}

13.3.5. Generic context

Decorators can be used to define a generic wrapper around a function, that extends the previous example (and can be used to implement most of them). This functionality is provided by the gololang.Decorators.withContext standard decorator. This decorator take a context, such as the one returned by gololang.Decorators.defaultContext function.

A context is an object with 4 defined methods:

  • entry, that takes and returns the function arguments. This method can be used to check arguments or apply transformation to them;
  • exit, that takes and returns the result of the function. This method can be used to check conditions or transform the result;
  • catcher, that deal with exceptions that occurs during function execution. It takes the exception as parameter;
  • finallizer, that is called in a finally clause after function execution.

The context returned by gololang.Decorators.defaultContext is a void one, that is entry and exit return their parameters unchanged, catcher rethrow the exception and finallizer does nothing.

The workflow of this decorator is as follow:

  1. the context entry method is called on the function arguments;
  2. the decorated function is called with arguments returned by entry;

    1. if an exception is raised, catcher is called with it as parameter;
    2. else the result is passed to exit and the returned value is returned
  3. the finallizer method is called.

Any of theses methods can modify the context internal state.

Here is an usage example:

module samples.ContextDecorator

import gololang.Decorators

let myContext = defaultContext():
  count(0):
  define("entry", |this, args| {
    this: count(this: count() + 1)
    println("hello:" + this: count())
    return args
  }):
  define("exit", |this, result| {
    require(result >= 3, "wrong value")
    println("goobye")
    return result
  }):
  define("catcher", |this, e| {
    println("Caught " + e)
    throw e
  }):
  define("finallizer", |this| {println("do some cleanup")})


@withContext(myContext)
function foo = |a, b| {
  println("Hard computation")
  return a + b
}

function main = |args| {
  println(foo(1,2))
  println("====")
  println(withContext(myContext)(|a| -> 2*a)(3))
  println("====")
  try {
    println(foo(1, 1))
  } catch (e) { }
}

which prints

hello:1
Hard computation
goobye
do some cleanup
3
====
hello:2
goobye
do some cleanup
6
====
hello:3
Hard computation
Caught java.lang.AssertionError: wrong value
do some cleanup

Since the context is here shared between decorations, the count attribute is incremented by each call to every decorated function, thus the output.

This generic decorator can be used to easily implement condition checking, logging, locking, and so on. It can be more interesting if you want to provide several functionalities, instead of stacking more specific decorators, since stacking, or decorator composition, adds indirection levels and deepen the call stack.