Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Yet Another Program on Closures, 2025

Yet Another Program on Closures, 2025

Closures provide a simple, lightweight, flexible mechanism for breaking tasks down into components. This year we look at a quick review of closures in general and how to combine them with :lvalue (or "is rw") signatures in Perl5 & Raku to encapsulate attributes in classes, combine them with Sentinel and Proxy to add boilerplate, and use Sub::Name to clean up messages from the subs.

Avatar for Steven Lembark

Steven Lembark

July 01, 2025
Tweet

More Decks by Steven Lembark

Other Decks in Technology

Transcript

  1. Programmers love closures At least you all keep showing up

    :-) And they are simple, powerful constructs. This time: Review the basics Look at lvalue subs in Perl & Raku
  2. What’s a closure? A Perl sub using lexical variables kept

    alive in its scope. The lexical lives on within the sub’s scope.
  3. What’s a closure? A Perl sub using lexical variables kept

    alive in its scope. The lexical lives on within the sub’s scope. The lexical unavailable outside of the sub(s) that use it. Note: Closures do not have to be anonymous!
  4. What’s in a closure? A Perl sub using lexical variables

    kept alive in its scope. $verbose is meaningfull in debug(). my $verbose = $ENV{ VERBOSE }; # stable copy, %ENV can be changed anywhere. sub debug { say @_ if $verbose # $verbose is accessible here. } sub info { say @_; # $verbose is meaningless here }
  5. What’s in a closure? A Perl sub using lexical variables

    kept alive in its scope. $verbose doesn’t exist in info. my $verbose = $ENV{ VERBOSE }; # stable copy, %ENV can be changed anywhere. sub debug { say @_ if $verbose # $verbose is accessible here. } sub info { say @_; # $verbose is meaningless here }
  6. What’s in a closure? A Perl sub using lexical variables

    kept alive in its scope. Blocks can serve the same purpose. { my $verbose = $ENV{ VERBOSE } // ‘’; sub debug { say @_ if $verbose } };
  7. What’s in a closure? State var’s also create closures. Save

    wrapping subs in blocks. sub debug { state $verbose = $ENV{ VERBOSE } // ‘’; say @_ if $verbose }
  8. What’s in a closure? Closures can be anonymous. Created in

    loops or factories. sub gen_debug { my $prefix = shift; sub { @_ ? say join ‘ ’ => $prefix, @_ : carp “Bogus $prefix: no message” } }
  9. What’s in a closure? New storage for $prefix is allocated

    for each sub. sub gen_debug { my $prefix = shift; sub { @_ ? say join ‘ ’ => $prefix, @_ : carp “Bogus $prefix: no message” } }
  10. What’s in a name? Aside: Sub::Name allows for better exceptions.

    use Sub::Name; use Carp; sub gen_debug { my $prefix = shift || die ‘Bogus gen_debug: False prefix’; subname debug => sub { @_ ? say join ‘ ’ => $prefix, @_ : carp “Bogus $prefix: no message” } }
  11. What’s in a name? Aside: Sub::Name allows for better exceptions.

    Warning from __ANON__, etc. Bogus Foobar: missing message at (eval ... main::__ANON__ [(eval 8)[/opt/perl/5.40.0/lib/5.40.0/perl5db.pl:742]:2]() called at ...
  12. What’s in a name? Aside: Sub::Name allows for better exceptions.

    Warning from “debug” using subname. Bogus Foobar: missing message at (eval ... main::__ANON__ [(eval 8)[/opt/perl/5.40.0/lib/5.40.0/perl5db.pl:742]:2]() called at ... vs. main::debug() called at ...
  13. What’s in a name? Skeleton for a handler-generator. $name and

    $pref allow flexible, specific messages. use Sub::Name; sub gen_debug { my $name = shift // croak ‘Bogus gen_debug: false name’; my $pref = shift // croak “Bogus gen_debug: false prefix for ‘$name’”; $name =~ m{\W} && die “Botched gen_debug: non-word in ‘$name’”; subname $name => sub { @_ ? say join “\n\t” => $pref, @_ : carp “Bogus debug ‘$pref’: no message” } }
  14. Dispatching closures Methods can be dispatched using variables. sub foo(

    $object, $method ) { ... $object->$method( @argz ); # dispatch the method }
  15. Dispatching closures Dispatching to derived classes or roles. Passed by

    name the use normal method lookup. sub foo( $object, $method ) { ... $object->$method( @argz ); # dispatch the method } sub frobnicate { ... } $obj->foo( ‘frobnicate’ );
  16. Dispatching closures Passed as subrefs they can be dispatched as-is.

    Bypass inheritence lookups. sub foo( $object, $method ) { ... $object->$method( @argz ); # dispatch the method } sub frobnicate { ... } $obj->foo( \&Bar::frobnicate );
  17. Dispatching closures Passed as subrefs they can be dispatched as-is.

    Behave as private methods. sub foo( $object, $method ) { ... $object->$method( @argz ); # dispatch the method } my $closure = sub { ... }; $obj->foo( $closure );
  18. Dispatching closures Installed via glob. Dispatched by name. sub foo(

    $object, $method ) { ... $object->$method( @argz ); # dispatch the method } *frobnicate = sub { ... } $obj->foo( ‘frobnicate’ );
  19. Dispatching closures Installed via glob. Dispatched by name. Use local

    *X for transient or override methods. sub foo( $object, $method ) { ... $object->$method( @argz ); # dispatch the method } { local *frobnicate = subname frobnicate => sub { ... }; $obj->foo( ‘frobnicate’ ) };
  20. Why use them? Encapsulation Dispatching code is insulated from the

    contents. It does’t have to know what’s inside $sub my $sub = shift; $sub->(); # dispatching requires no knowlege. $sub->( 'frobnicate' ); # can take arguments. $object->$sub; # use as a private method $object->$sub( ‘foo’ ); # private method with args.
  21. Why use them? Delayed dispatch Trampoline closure, called in later

    AUTOLOAD. pacakge Object::Trampoline; AUTOLOAD # constructor generates closure object { ... bless sub { $pkg->$const( @argz ) }, ‘Object::Trampoline::Bounce’; } package Object::Trampoline::Bounce; AUTOLOAD { $_[0] = $_[0]->(); # replace trampoline with ‘real’ object. my $obj = shift; # use $obj to dispatch $AUTOLOAD with @_. … # see Object::Trampoline for details. }
  22. Why use them? Added eval is encapsulated. Any class can

    pass a closure without changing O::T::B. pacakge Object::Trampoline::Use; AUTOLOAD # constructor generates closure object { ... bless sub { eval “use $pkg”; $pkg->$const( @argz ) }, ... } package Object::Trampoline::Bounce; AUTOLOAD { $_[0] = $_[0]->(); # replace trampoline with ‘real’ object. my $obj = shift; # use $obj to dispatch $AUTOLOAD with @_. … # see Object::Trampoline for details. }
  23. Why use them? Division of labor no_blanks, no_comments can supply

    arbitrary code. sub no_blanks { print_lines sub { map { split “[\r\n]+” } @_ } } sub no_comment { print_lines sub { map { grep “^[^#]” => split “\n” } @_ }; sub print_lines( $extract, $input = *STDIN, $output = *STDOUT ) { # closure $extract saves logic for map vs. grep vs. whatever # sanity checks on $input $output are in common code. state $fmt = “%03d\t%s\n”; local $/ = “\n”; printf $output, $fmt => ++$line, $_ for $extract->( readline $input ); }
  24. Why use them? Division of labor Avoids kitchen-sink complexity in

    print_lines. sub no_blanks { print_lines sub { map { split qr{\n+} } @_ } } sub no_comment { print_lines sub { map { grep “^[^#]” => split qr{[\r\n]} } @_ }; sub print_lines( $extract, $input = *STDIN, $output = *STDOUT ) { # closure $extract saves logic for map vs. grep vs. whatever # sanity checks on $input $output are in common code. state $fmt = “%03d\t%s\n”; local $/ = “\n”; printf $output, $fmt => ++$line, $_ for $extract->( readline $input ); }
  25. Examples Applying :lvalue closures as set/get methods. Use :lvalue for

    simple set/get syntax. Sentinel adds boilerplate. Dealing with Raku’s call stack.
  26. What’s an lvalue? Whatever can appear on the left side

    of an assignment. $a = 1; # $a is an lvalue $a = $b; # $a is still an lvalue $a = $b = $c; # $b is an lvalue, $c is not. Ditto @a, %a, &{ *$x }
  27. lvalue subs can be assigned substr() can be assigned a

    replacement string. Paren’s avoid “9 = ‘Over the Rainbow’” my $input = ‘For some definition of Christmas.’; substr( $input, 23, 9 ) = ‘Over the Rainbow’; say $input; For some definition of Over the Rainbow.
  28. lvalue subs can be assigned Simplest sub: a bare state

    var. state keeps ‘$i’ alive between calls to foo. sub foo :lvalue { state $i = 1 }
  29. lvalue subs can be assigned foo returns its current $i

    value. sub foo :lvalue { state $i = 1 } $a = foo; # $a == 1
  30. lvalue subs can be assigned assigning foo updates $i. sub

    foo :lvalue { state $i = 1 } $a = foo; # $a == 1 foo = 10; $b = foo; # $b == 10
  31. Using an :lvalue Which makes for nice set/get methods. verbose()

    encapsulates $verb. package Foo::Bar; sub verbose :lvalue { state $verb = $ENV{ VERBOSE } // ‘’ }
  32. Using an :lvalue Which makes for nice set/get methods. $obj->verbose

    gets. $obj->verbose = sets. package Foo::Bar; sub verbose :lvalue { state $verb = $ENV{ VERBOSE } // ‘’ } my $object = Foo::Bar->fubar; if( $object->verbose ) { … } # get the current verbosity $object->verbose = 42; # set the current verbosity
  33. Using an :lvalue Which makes for nice set/get methods. Avoids

    global $ENV{ VERBOSE } changing. package Foo::Bar; sub verbose :lvalue { state $verb = $ENV{ VERBOSE } // ‘’ } my $object = Foo::Bar->fubar; if( $object->verbose ) { … } # unaffected by changes in $ENV{ VERBOSE }. $object->verbose = 42; # doesn’t affect $ENV{ VERBOSE }.
  34. Applying the closure Say you want a template class. Given

    a list attributes, install them on the fly. Install the class from YAML or a struct.
  35. Applying the closure Given a package & list of names.

    Manufacture closures for each attribute. Initialized to undef. install_attrs( $pkg, @namz ) { for my $name ( @namz ) { my $ref = qualify_to_ref $name => $pkg; *{ $ref }{ CODE } and next; *$ref = subname $name => sub :lvalue { state $val } } }
  36. Applying the closure Install them as code refs. Symbol::qualify_to_ref makes

    this trivial. install_attrs( $pkg, @namz ) { for my $name ( @namz ) { my $ref = qualify_to_ref $name => $pkg; *{ $ref }{ CODE } and next; *$ref = subname $name => sub :lvalue { state $val } } }
  37. Applying the closure Closure fully encapsulates the value. Only way

    in or out of the attribute’s value is its method. install_attrs( $pkg, @namz ) { for my $name ( @namz ) { my $ref = qualify_to_ref $name => $pkg; *{ $ref }{ CODE } and croak “Botched attr: ‘$name’ exists in $pkg”; *$ref = subname $name => sub :lvalue { state $val } } }
  38. Applying the closure Initialize the values from hash or zipped

    arrays. Repace the state var with lexical variable. install_attrs( $pkg, %initz ) { while( my( $name, $val ) = each %initz ) { my $ref = qualify_to_ref $name => $pkg; *{ $ref }{ CODE } and croak “Botched attr: ‘$name’ exists in $pkg”; *$ref = subname $name => sub :lvalue { $val } } }
  39. What about boilerplate? The examples above lack sanity checks. Not

    always wise for methods... Fix: More anonymous subs, and closures!
  40. What about boilerplate? Sentinel adds get/set handlers. Subs can be

    anonymous, named, or even more closures. From the Sentinel doc: package Some::Class use Sentinel sub foo :lvalue { my $self = shift; sentinel get => sub { $self->get_foo } , set => sub { $self->set_foo( $_[0] ) } ; }
  41. What about boilerplate? https://metacpan.org/pod/Sentinel sentinel decides if the context is

    assignment. package Some::Class use Sentinel sub foo :lvalue { my $self = shift; sentinel get => sub { $self->get_foo } , set => sub { $self->set_foo( $_[0] ) } ; }
  42. What about boilerplate? Add sanity handler to installer. Whatever it

    returns is assigned (translation, bitmap...). package Generate::Fields use Sentinel; sub install_attr( $pkg, $name, $val, $sanity ) { *{ qualify_to_ref $name => $pkg } = subname $name => sub :lvalue { my $obj = shift; sentinel get => sub { $val } , set => sub { $val = $obj->$sanity( @_ ) } } }
  43. What about boilerplate? $obj->$sanity can be anything dispatchable. By name

    with a string, using inheritence. package Has::Fields sub validate_foobar { … } install_field __PACKAGE__ , foobar => ‘’ , ‘validate_foobar’ # method name as scalar ;
  44. What about boilerplate? $sanity can be anything dispatchable. By ref

    to a named sub, bypassing inheritence. package Has::Fields sub validate_foobar { … } install_field __PACKAGE__ , foobar => ‘’ , ‘validate_foobar’ # method name as scalar ; install_field … \&validate_foobar; # pass as coderef
  45. What about boilerplate? $sanity can be anything dispatchable. To an

    anonymous sub... package Has::Fields sub validate_foobar { … } install_field __PACKAGE__ , foobar => ‘’ , ‘validate_foobar’ # method name as scalar ; install_field … \&validate_foobar; # pass as coderef install_field … sub { ... } ; # anon sub (private method)
  46. Boilerplate can be closures package Some::Class; for my $name (

    read_config ‘field_names’ ) { my $value = read_config “default_$name”; my $lower = read_config “min_$name”; my $upper = read_config “max_$name”; install_fields __PACKAGE__ , $name => $value , subname “validate_$name” => sub( $, $new ) # ‘$’ ignores unused object { $new >= $lower or croak “Botched $name: $new >= $lower”; $new < $upper or croak “Botched $name: $new < $upper”; $new } ; };
  47. Encapsulation The class cannot bypass its own get/set methods. $val

    is private to install_fields. The only places it exists is in the sentinel subs. Without the overhead of hashes with inside-out classes. sub install_field( $pkg, $name, $val, $sanity ) get => sub { $val } , set => sub { $val = obj->$sanity( @_ ); }
  48. Benchmark my $bare_state = sub :lvalue { state $val =

    'fee' } my $bare_assign = sub :lvalue { my $new = shift } do { my $val = 'fie'; sub bare_lexical :lvalue { $val } }; Compare overhead of state vs. external lexical.
  49. Benchmark sub with_sentinel :lvalue { my $val = 'foe'; sentinel

    get => sub { $val } , set => sub { my $new = shift // croak 'Botched with_sentinel: cannot assign undef.'; $val = $new } } Compare overhead of sentinel & handlers. $new avoids overwrite if exception is handled.
  50. Absolutely nothing to do with anything else... sub _____ {

    say ‘_____’ } benchmark $count, { blech => ... } _____; benchmark $count, { blort => ... } _____; Nice dividing line between benchmark outputs. Source reads a bit more like the output.
  51. Absolutely nothing to do with anything else... Benchmark: timing 67108864

    iterations of bletch ... _____ Benchmark: timing 67108864 iterations of blort … _____ Nice dividing line between benchmark outputs.
  52. Benchmark my $count = 2 ** 28; # min number

    to get useful timing timethese $count , { bare_dispatch => sub {} , bare_assign => sub { my $new = shift } , lval_assign => sub :lvalue { my $new = shift } , random => sub { rand } } ;
  53. Benchmark my $count = 2 ** 28; # min number

    to get useful timing timethese $count , { get_bare_lexical => sub { bare_lexical } , get_bare_state => sub { bare_state } , get_with_sentinel => sub { with_sentinel } } ; timethese $count , { rand_value => sub { rand } , set_bare_lexical => sub { bare_lexical = rand } , set_bare_state => sub { bare_state = rand } , set_with_sentinel => sub { with_sentinel = rand } } ;
  54. Benchmark Prior: bare_state fee bare_lexical fie with_sentinel foe After: bare_state

    1 bare_lexical 2 with_sentinel 3 _____ Benchmark: timing 268435456 iterations of bare_assign, bare_dispatch, lval_assign, random... bare_assign: 6 wallclock secs ( 5.53 usr + 0.00 sys = 5.53 CPU) @ 48541673.78/s (n=268435456) bare_dispatch: 0 wallclock secs (-0.07 usr + 0.00 sys = -0.07 CPU) @ -3834792228.57/s (n=268435456) (warning: too few iterations for a reliable count) (warning: too few iterations for a reliable count) lval_assign: 5 wallclock secs ( 5.17 usr + 0.00 sys = 5.17 CPU) @ 51921751.64/s (n=268435456) random: 3 wallclock secs ( 2.55 usr + 0.00 sys = 2.55 CPU) @ 105268806.27/s (n=268435456) _____ Benchmark: timing 268435456 iterations of get_bare_lexical, get_bare_state, get_with_sentinel... get_bare_lexical: 11 wallclock secs (10.90 usr + 0.00 sys = 10.90 CPU) @ 24627106.06/s (n=268435456) get_bare_state: 10 wallclock secs (11.14 usr + 0.00 sys = 11.14 CPU) @ 24096540.04/s (n=268435456) get_with_sentinel: 176 wallclock secs (175.18 usr + 0.01 sys = 175.19 CPU) @ 1532253.30/s (n=268435456) _____ Benchmark: timing 268435456 iterations of set_bare_lexical, set_bare_state, set_with_sentinel... set_bare_lexical: 19 wallclock secs (18.32 usr + 0.02 sys = 18.34 CPU) @ 14636611.56/s (n=268435456) set_bare_state: 19 wallclock secs (18.58 usr + 0.00 sys = 18.58 CPU) @ 14447548.76/s (n=268435456) set_with_sentinel: 228 wallclock secs (226.92 usr + 0.00 sys = 226.92 CPU) @ 1182951.95/s (n=268435456)
  55. Benchmark Prior: bare_state fee bare_lexical fie with_sentinel foe After: bare_state

    1 bare_lexical 2 with_sentinel 3 _____ Good news: Defaults work! So do asignments!
  56. Benchmark Dispatch overhead won’t affect timing. Rest are not quite

    zero. Scrape ~7.5s off times for assignment tests. _____ Benchmark: timing 268435456 iterations of bare_assign, bare_dispatch, lval_assign, random... bare_assign: 6 wallclock secs ( 5.53 usr + 0.00 sys = 5.53 CPU) @ 48541673.78/s bare_dispatch: 0 wallclock secs (-0.07 usr + 0.00 sys = -0.07 CPU) @ -3834792228.57/s (warning: too few iterations for a reliable count) lval_assign: 5 wallclock secs ( 5.17 usr + 0.00 sys = 5.17 CPU) @ 51921751.64/s random: 3 wallclock secs ( 2.55 usr + 0.00 sys = 2.55 CPU) @ 105268806.27/s
  57. Benchmark State and lexcal are nearly equal. Take ~3s or

    ~10s for get/set overhead. 89MHz, 26MHz for reads/writes is good. _____ Benchmark: timing 268435456 iterations of get_bare_lexical, get_bare_state, get_with_sentinel... get_bare_lexical: 10 wallclock secs @ 27114692.53/s get_bare_state: 10 wallclock secs @ 25910758.30/s get_with_sentinel: 166 wallclock secs @ 1620204.35/s _____ Benchmark: timing 268435456 iterations of set_bare_lexical, set_bare_state, set_with_sentinel... set_bare_lexical: 16 wallclock secs @ 16328190.75/s set_bare_state: 17 wallclock secs @ 15707165.36/s set_with_sentinel: 211 wallclock secs @ 1273352.57/s
  58. Benchmark Sentinel is a lot slower. Still usable @ 1.6/1.3

    MHz. _____ Benchmark: timing 268435456 iterations of get_bare_lexical, get_bare_state, get_with_sentinel... get_bare_lexical: 10 wallclock secs @ 27114692.53/s get_bare_state: 10 wallclock secs @ 25910758.30/s get_with_sentinel: 166 wallclock secs @ 1620204.35/s _____ Benchmark: timing 268435456 iterations of set_bare_lexical, set_bare_state, set_with_sentinel... set_bare_lexical: 16 wallclock secs @ 16328190.75/s set_bare_state: 17 wallclock secs @ 15707165.36/s set_with_sentinel: 211 wallclock secs @ 1273352.57/s
  59. Benchmark: Takeaway Sentinel is slower. Suitable for public interfaces. Use

    bare :lvaues for ‘bottom half’ methods. Install as anon subs in the class. Call only from ‘safe’ methods.
  60. Raku lvalue methods Raku supports lvalue methods. With, of course,

    more options :-) class BareLvalue { has $!bar; # private attribute method bar () is rw { $!bar } # rw accessor allows write } $obj.bar = 42; say $obj.bar; # 42 $obj.bar = -1; say $obj.bar; # -1
  61. Raku lvalue methods $!bar closed over by each object, not

    the class. Each object’s methods close over its fields. class BareLvalue { has $!bar; # private attribute method bar () is rw { $!bar } # rw accessor allows write } $obj.bar = 42; say $obj.bar; # 42 $obj.bar = -1; say $obj.bar; # -1
  62. Raku lvalue methods Or use a public attribute. Which has

    a default accessor. class BareLvalue { has $.bar is rw; # public attribute w/ default accessor. } $obj.bar = 42; say $obj.bar; # 42 $obj.bar = -1; say $obj.bar; # -1
  63. Raku’s builtin “Proxy” adds boilerplate Proxy acts like Sentinel: Dispatching

    FETCH/STORE. class WithProxy { has $.bar; method bar() # explicit accessor is rw # ‘is rw’ acts as an lvalue. { Proxy.new: FETCH => sub ( $proxy ) { $!bar } , STORE => sub ( $proxy, $new ) { $!bar = max( 0, $new ) } ; } }
  64. Raku’s builtin “Proxy” adds boilerplate Proxy.new is a constructor: the

    sub’s close over $.bar. class WithProxy { has $.bar; method bar() is rw { Proxy.new: FETCH => sub ( $proxy ) { $.bar } , STORE => sub ( $proxy, $new ) { $.bar = max( 0, $new ) } ; } }
  65. Raku’s builtin “Proxy” adds boilerplate STORE handler avoids negativity. STORE

    => sub ( $proxy, $new ) { $.bar = max( 0, $new ) } my $obj = WithProxy.new; $obj.bar = 42; say $obj.bar; # "42" $obj.bar = -1; say $obj.bar; # "0"
  66. Multimethods apply to closures They apply to all methods in

    Raku. The signature object is used to dispatch each method | name | signature | body ... | method foo ( Int $count ) { ... } method foo ( Float $count ) { ... } method foo ( IO::File $count ) { ... }
  67. Multimethods apply to closures Signature dispatches to faster ++ with

    no argument. Int handled natively, otherwise use an explicit cast. Class Counter { has Int $!curr; # private attribute, no accessor method. method next() { ++$!curr } method next( Int ) { $!curr += $. } method next( any ) { $!curr += Int( $. ) } method new ( Int $prior = 0 ) { method reset { $!curr = $prior } self.reset } }
  68. Multimethods apply to closures Constructor defaults initial value. Initial value

    can be used to reset the object. Class Counter { has Int $!curr; # private attribute, no accessor method. method next() { ++$!curr } method next( Int ) { $!curr += $. } method next( any ) { $!curr += Int( $. ) } method new ( Int $prior = 0 ) { method reset { $!curr = $prior } self.reset } }
  69. Summary lvalue functions allow simple, fast interfaces. :lvalue in Perl

    is rw in Raku Boilerplate via added classes Sentinel w/ get/set in Perl Proxy with FETCH/STORE in Raku Multimethods add flexibility in Raku
  70. Bedside reading: Raku Welcome to the Land of Raku: https://raku.land/

    Benchmarking Raku: https://raku.land/zef:raku-community-modules/Benchmark Raku constructors & objects: https://docs.raku.org/language/classtut