GuideThe Silo HandbookAspects

Chapter 10 Aspects

Source

An aspect is a capability you want available throughout a program without signing for it at every call site. Logging, tracing, metrics, localisation, the current random-number generator, the current time source for testing — all of them are things that plenty of words need and that nobody wants to list as an effect on the signature of every word in between.

Aspects fill that gap. They look like effects from a declaration-site perspective, but they don't need to appear in call-site signatures. Instead, every aspect operation has a default implementation (usually a no-op), and you can override the implementation for a dynamically scoped region with :with.

If you've used AspectJ or Common Lisp's :around methods, this is the same idea in a typed-stack form. If you've used Koka, aspects are Silo's un-annotated version of its algebraic effects — less tracking at call sites, more flexibility for cross-cutting concerns.

Declaring an aspect

:aspect declares a named capability with one or more operations. Each operation gets a default body, separated from its signature by a space and ending with ;:

:aspect Log
  .log ( Str LogLevel -> ) drop drop ;
:end

Log has one operation, .log, which pops a LogLevel (top of stack) and a Str (underneath). The default drop drop discards both — the no-op case. Every aspect operation must have a default, because code calls the operation without first checking that a handler is installed.

Implementing a handler

An aspect handler is a value whose type implements the aspect for its own case:

:record StdoutLogger :end

:impl Log StdoutLogger
  .log ( Str LogLevel -> ) +Console
    "{}: {}" format print ;
:end

StdoutLogger is a zero-field record used as a tag — in this case the handler carries no state. The :impl block implements Log for StdoutLogger, replacing the default .log body with one that prints. Notice that this handler's .log declares +Console: a handler can do whatever work it needs, with whatever effects it needs, independent of what the call site sees.

Handlers with state are records with fields:

:record RingLogger
  .buffer (Vec (Pair Str LogLevel) 128)
  .pos U8
:end

and an :impl that reads/updates the state appropriately.

Importing an aspect

The :use block imports both the aspect itself and the operations it exports:

:use
  :aspect logging Log
:end

The first name (logging) is the package containing the aspect; the second (Log) is the aspect's name. After this import, the operations — here .log — are available as ordinary methods.

Calling an aspect operation

Call sites don't need any annotation:

:fn do-work ( -> )
  "working" LInfo .log         # no +Log effect declared anywhere
:end

The .log call resolves against whichever handler is currently installed. If no :with block is active, it dispatches to the default (the aspect's built-in no-op body).

This is the crucial property of aspects. do-work has a pure signature. You can call it from anywhere without worrying about whether the caller has a +Log capability, because there's no such effect to propagate. The logging happens if a handler is installed; it silently doesn't otherwise.

Installing a handler — :with

:with installs a handler for a dynamically scoped region:

:main +Console
  StdoutLogger :with Log
    do-work                    # .log calls go to StdoutLogger
  :end
  do-work                      # .log calls are no-ops again
:end

Read StdoutLogger :with Log … :end as "for the body, use StdoutLogger as the handler for the Log aspect." The handler value sits on the stack immediately before :with; the body runs with the handler installed; after :end, the previous handler (or the default) is restored.

Because :with is dynamic, nested :with blocks nest:

:main +Console
  StdoutLogger :with Log
    do-work                    # StdoutLogger
    RingLogger :with Log
      do-work                  # RingLogger, inside the inner :with
    :end
    do-work                    # back to StdoutLogger
  :end
:end

That's what makes aspects practical for tests and experiments: wrap a region in a mock handler and the code inside runs unchanged, dispatching to the mock.

Aspects versus effects

The two feel related because both concern cross-cutting behaviour, and both use :with for handler installation. The differences:

Dimension Effect Aspect
Declaration keyword :effect :aspect
Handler required? Yes (host or :with) No — every op has a default
Shows in signatures? Yes (+Effect) No
Call-site ceremony Caller must declare/discharge Call freely, dispatches to default/handler
Runtime representation Same vtable-backed handler install Same vtable-backed handler install

Use an effect when the operation has to succeed — if there's no handler, something has gone wrong and the program shouldn't compile. Use an aspect when the operation is cross-cutting — nice to have everywhere, with a sensible no-op default when nobody's listening.

When to reach for an aspect

A word needs aspects if, hypothetically, you could replace every one of its operations with a no-op and the program would still produce the right answer — just without the side observation. Logging passes that test. So does metrics collection, tracing, and "the current locale" (the default is the system locale; a :with overrides).

A word that genuinely needs the capability — reading a file, making a network call — should be an effect, not an aspect. Aspects are for behaviour that's worth providing but isn't essential to correctness.

Key points

  • An aspect is a capability with a default. Operations are callable anywhere, no +Aspect annotation needed.
  • :aspect declares; :impl (Aspect Type) provides a handler; :with installs a handler for a dynamically scoped region.
  • Handlers are values. Stateless handlers are zero-field records used as tags; stateful handlers are records with fields.
  • Nested :with blocks nest dynamically — the innermost handler wins for the scope of its body.
  • Aspects and effects have the same handler-install machinery but different call-site stories: effects demand, aspects invite.

Next: collections — the standard library's data structures, traversal traits, and iteration patterns.