Search code examples
f#computation-expression

Are Computation Expressions an alternative approach to Aspect-oriented Programming?


Are Computation Expressions an alternative approach to Aspect-oriented Programming?

Is this F#'s solution for managing cross-cutting concerns.

I viewed the following article and couldn't help but think of AOP (i.e. Aspect-oriented Programming).

In the article, the author provided an example of a Computation Expression that handled logging, but isolated the actual logging aspect of the code without obfuscating the main intent of the business logic.

Are my thoughts accurate?


Solution

  • Yes, monads are a great (and idiomatic) approach to cross-cutting concerns, among other things. A monad is a much more general concept, but one of the uses they can be put to is to model effects in systems.

    In FP jargon, the word effect often implies a side-effect. Many cross-cutting concerns like logging, auditing, performance monitoring, caching, and metering all (by definition) have side-effects. Since they involve 'writing' data to a secondary stateful resource, they can be modelled with the State or Writer monads.

    Other cross-cutting concerns like authentication, authorisation, and validation can often be addressed with the Reader monad (or, possibly, the State monad).

    F# computation expressions provide syntactic sugar over monadic combinators (return and bind, essentially), the same way that Haskell's do notation does. Contrary to Haskell, however, you have to define the computation expression builder for a monad yourself, apart from the few already built in to the language (async, seq).

    Using a monad to address a cross-cutting concern is a different approach than AOP.

    In object-oriented programming, there are two fundamentally different takes on AOP:

    • Decorators
    • Compile-time weaving, of which the archetypical .NET example is PostSharp

    As I explain in my book, I consider compile-time weaving to be a heavy-handed and inflexible way to address the problem. Using a Decorator is a more elegant and flexible way to achieve the same goal.

    My motivation for that statement is my preference for separation of concerns.

    If you use compile-time weaving in OOP, you'll often have code like this (C# example):

    [Log]
    public void SaveOrder(Order order)
    {
        // Implementation goes here...
    }
    

    The problem here is that while the concerns are separated, they are still coupled. You can't decide not to log from SaveOrder unless you recompile.

    Using a computation expression is a bit like that:

    log { return saveOrder order }
    

    Again, the cross-cutting concern is compiled together with the implementation.

    Where the similarity ends, however, is that monadic combinators can be used to compose concerns together, which isn't easily possible with compile-time weaving. In OOP, you can't easily take a method that doesn't log, and 'magically' make it log when you compose your objects together.

    Using monads, on the other hand, you can take a pure function and compose it with a monadic context. Such later-bound logging is structurally equivalent to using Decorators.

    So, I'd consider it appropriate to use computation expressions for cross-cutting concerns, as long as you defer such code to the application's entry point (its Composition Root).