It's A Wrap
In my last post, I briefly touched on the concept of wrapping functions. I also learned that they are similar to decorators in Python. Apart from one time I used the @property
decorator in a Python class to make some attributes read-only, I didn't really know what they were. I just figured it was some weird Python syntax. I've since learned a little be more and played around with them in Python, Perl, and Raku.
A decorator is a function that takes another function as it's argument, and typically does something "around" that function, which is why it's also referred to "wrapping" a function. A decorator can't change what the wrapped function does internally, but it can can run code before or after calling that function, or not call it at all.
I may use the words 'wrapper' and 'decorator' interchangeably, by which I mean 'a function that wraps another function'
There are some quintessential applications for decorators; the main ones being caching, logging, and timing of functions. As a reference point, here is a timing decorator in Python 3.6.
import time def timed(func): name = func.__name__ def wrapped(*args): start = time.time() res = func(*args) print(f"Run time for function '{name}' was {time.time() - start:f}") return res return wrapped @timed def costly(n): time.sleep(n); return 'Have a string' x = costly(3) # OUTPUT: Run time for function 'costly' was 3.02231 print(x) # OUTPUT: Have a string
In the above example, I grab the name of the function, then create the wrapper function. My wrapper kicks off a timer, then runs the original (decorated) function and assigns the result to a variable res
. I then stop the time, print out the stats then return the result.
So without further ado, or much explanation, here's a Raku sub trait that achieves the same result.
multi sub trait_mod:<is>(Routine $func, :$timed) { $func.wrap({ my $start = now; my $res = callsame; note "Run time for function '{$func.name}' was {now - $start}"; $res; }) } sub costly($n) is timed { sleep($n); return 'Have a string'; } my $x = costly(3); # OUTPUT: Run time for function 'costly' was 3.0030732 say $x; # OUTPUT: Have a string
Most of this should be fairly obvious, except maybe callsame
, which I covered in my last post... but if you need a refresher, it tells the dispatcher to call the same function that was just called. Also, note the note
function, which is exactly like say
except that it outputs to STDERR.
Traits wrap a function at (some time around) compile time, but sometimes you might want to wrap a function at runtime, or rather... You might want to decide whether you want to wrap a function at runtime; which functions you want wrapped with what; and when.
Take debugging for example. It would be trivial to create a trait that reports to STDERR when a function has been called, and with what arguments... but adding and removing a trait everytime you want to debug - especially on multiple functions - can get a little unwieldy.
Typically when you debug with print statements (we all do it!) you might manage your programs DEBUG
mode via a global variable. At runtime you can inspect the variable and wrap your desired functions accordingly.
constant DEBUG = True; sub foo($n) { return $n × $n; } &foo.wrap(&debug) if DEBUG; my $x = foo(42); sub debug(|c) { my &func = nextcallee; my $res = func(|c); note "Calling '{&func.name}' with args {c.raku} returned: {$res.raku}"; $res; } # STDERR: Calling 'foo' with args \(42) returned: 1764
The .wrap()
method actually returns something called a WrapHandle, which is handy if you want to be able to unwrap your function at any point. It also means you can decide which wrappers get removed.
Perhaps you have a logging wrapper, something that performs a similar role as the debug wrapper, but instead punts the information to your logger of choice, or maybe just a text file. You want to disable the debugger at some point, but keep logging.
my $wh-logger = &foo.wrap(&logger); my $wh-debug = &foo.wrap(&debug) if DEBUG; my $x = foo(42); # Success threshold, debugging is no longer required &foo.unwrap($wh-debug) if DEBUG; # Calls to 'foo' still hit the logger my $y = foo(19);
The beauty of wrappers is your wrapped functions don't have to know they are being wrapped. They can concern themselves with their core purpose. Additionally they only need to be wrapped once, instead of, for example, manually calling your logger
function all over the place.
So these decorator things are nice, but I still use Perl quite a lot, and I wanted to know if there was a way to wrap functions in Perl with the same syntactic niceness that trait's provide. What I eventually landed on was attributes, and Attribute::Handlers.
Like trait mods (and Python decorators), attributes are added at the point of your function declarations. Attribute::Handles
just makes working with them a little easier. Here's the example from up top, implemented with Perl.
use v5.26; use warnings; no warnings 'redefine'; use experimental 'signatures'; use Time::HR 'gethrtime'; use Attribute::Handlers; sub Timed($pkg, $sym, $code, @) :ATTR { my $func = substr(${$sym}, length($pkg) + 3); *$sym = sub (@args) { my $start = gethrtime(); my $res = $code->(@args); my $time = (gethrtime() - $start) / 1_000_000_000; say {*STDERR} "Run time for function '$func' was $time"; return $res; } } sub costly($n) :Timed { sleep($n); return 'Have a string'; } my $x = costly(3); # STDERR: Run time for function 'costly' was 3.001124 say $x; # OUTPUT: Have a string
A few caveats to note about Perl... This is classed as redefining a symbol that already exists, and Perl will give a warning that the wrapped function has been redefined, so I disabled that warning. It will also give a warning if you give your attribute an all-lowercase name, as lowercase attributes are reserved for possible future use. Also, as far as I found, the only way to import attributes from a module is to declare them into the UNIVERSAL
namespace, for example: UNIVERSAL::Timed
, which technically means you don't even need to export them from your module, so... Yay, I guess.
One final note. It's curious to me that I'm talking about "wrapping" and "decorating" this close to December, when those words typically mean something else entirely. Happy holidays!