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.
Logging is a classical example use case of AOP. See the Principles and syntax Section 13.2, “Principles and syntax” section for an example.
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).
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 }
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) } }
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:
entry
method is called on the function arguments;
the decorated function is called with arguments returned by entry
;
catcher
is called with it as parameter;
exit
and the returned value is returned
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.