develooper Front page | perl.perl5.porters | Postings from June 2021

RFC 0004 - defer {} syntax

Thread Next
From:
Paul "LeoNerd" Evans
Date:
June 16, 2021 22:15
Subject:
RFC 0004 - defer {} syntax
Message ID:
20210616231540.60c9d356@shy.leonerd.org.uk
Here's an RFC formalization of my currently-open github feature issue:

  https://github.com/Perl/perl5/issues/17949

which was itself an issue tracking the previous mailing list
conversation:

  https://www.nntp.perl.org/group/perl.perl5.porters/2020/06/msg257611.html

If we accept this one into the RFC process, I'll likely close the
github issue and point at the RFC instead.

---

# defer {} syntax

## Preämble

    Author:  Paul Evans <PEVANS>
    Sponsor:
    ID:      0004
    Status:  Draft

## Abstract

Add a new control-flow syntax written as `defer { BLOCK }` which
enqueues a block of code for later execution, when control leaves its
surrounding scope for whatever reason.

## Motivation

Sometimes a piece of code performs some sort of "setup" operation, that
requires a corresponding "teardown" to happen at the end of its task.
Control flow such as exception throwing, or loop controls, means that
it cannot always be written in a simple style such as

  {
    setup();
    operation();
    teardown();
  }

as in this case, if the `operation` function throws an exception the
teardown does not take place. Traditional solutions to this often rely
on allocating a lexical variable and storing an instance of some object
type with a `DESTROY` method, so this runs the required code as a result
of object destruction caused by variable cleanup. There are no in-core
modules to automate this process, but the CPAN module `Scope::Guard` is
among the more popular choices.

It would be nice to offer a native core syntax for this common
behaviour. A simple `defer { BLOCK }` syntax removes from the user any
requirement to think about storing the guard object in a lexical
variable, or worry about making sure it really does get released at the
right time.

As well as being more convenient for users, it would help decouple Perl
core's object behaviours, like `DESTROY`, from this task. If less code
relies on DESTROY, it may one day be possible to change the way that
part of the code works, without worrying so much about code that relies
on it breaking.

This syntax has already been implemented as a CPAN module,
[`Syntax::Keyword::Defer`](https://metacpan.org/pod/Syntax::Keyword::Defer).
This RFC formalizes an attempt implement the same in core.

## Rationale

((TODO - I admit I'm not very clear on how to split my wording between
the Motivation section, and this one))

The name "defer" comes from a collection of other languages.
Near-identical syntax is provided by Swift, Zig, Jai, Nim and Odin. Go
does define a `defer` keyword that operates on a single statement,
though its version defers until the end of the containing function, not
just a single lexical block. I did consider this difference, but ended
up deciding that a purely-lexical scoped nature is cleaner and more
"Perlish", overriding the concerns that it differs from Go.

## Specification

A new lexically-scoped feature bit, requested as

  use feature 'defer';

enables new syntax, spelled as

  defer { BLOCK }

This syntax stands alone as a full statement; much as e.g. a `while`
loop does. The deferred block may contain one or multiple statements.

When the `defer` statement is encountered during regular code
execution, nothing immediately happens. The contents of the block are
stored by the perl interpreter, enqueued to be invoked at some later
time, when control exits the block this `defer` statement is contained
within.

If multiple `defer` statements appear within the same block, the are
eventually executed in LIFO order; that is, the most recently
encountered is the first one to run:

  {
    setup1();
    defer { say "This runs second"; teardown1(); }

    setup2();
    defer { say "This runs first"; teardown2(); }
  }

`defer` statements are only "activated" if the flow of control actually
encounters the line they appear on (as compared to `END` phaser blocks
which are activated the moment they have been parsed by the compiler).
If the `defer` statement is never reached, then its deferred code will
not be invoked:

  while(1) {
    defer { say "This will happen"; }
    last;
    defer { say "This will *NOT* happen"; }
  }

((TODO: It is currently not explicitly documented, but naturally falls
  out of the current implementation of both the CPAN and the in-core
  versions, that the same LIFO stack that implements `defer` also
  implements other things such as `local` modifications; for example:

  our $var = 1;
  {
    defer { say "This prints 1: $var" }

    local $var = 2;
    defer { say "This prints 2: $var" }
  }

  I have no strong thoughts yet on whether to specify and document -
  and thus guarantee - this coïncidence, or leave it unsaid.))

## Backwards Compatibility

The new syntax `defer { BLOCK }` is guarded by a lexical feature guard.
A static analyser that is not aware of that feature guard would get
confused into thinking this is indirect call syntax; though this is no
worse than basically any other feature-guarded keyword that controls a
block (e.g. `try/catch`).

For easy compatibility on older Perl versions, the CPAN implementation
already mentioned above can be used. If this RFC succeeds at adding it
as core syntax, a `Feature::Compat::Defer` module can be created for it
in the same style as
[`Feature::Compat::Try`](https://metacpan.org/pod/Feature::Compat::Try).

## Security Implications

None foreseen.

## Examples

A couple of small code examples are quoted above. Further, from the
docs of `Syntax::Keyword::Defer`:

  use feature 'defer';
 
  {
    my $dbh = DBI->connect( ... ) or die "Cannot connect";
    defer { $dbh->disconnect; }
 
    my $sth = $dbh->prepare( ... ) or die "Cannot prepare";
    defer { $sth->finish; }
 
    ...
  }

## Prototype Implementation

CPAN module `Syntax::Keyword::Defer` as already mentioned.

In addition, I have a mostly-complete branch of bleadperl (somewhat
behind since I haven't updated it for the 5.34 release yet) at

  https://github.com/leonerd/perl5/tree/defer

I'm not happy with the way I implemented it yet (don't look at how I
abused an SVOP to store the deferred optree) - it needs much tidying up
and fixing for the various things I learned while writing the CPAN
module version.

## Future Scope

If this RFC becomes implemented, it naturally follows to enquire
whether the same mechanism that powers it could be used to add a
`finally` clause to the `try/catch` syntax added in Perl 5.34. This
remains an open question: while it doesn't any new ability, is the
added expressive power and familiarity some users will have enough to
justify there now being two ways to write the same thing?

## Rejected Ideas

On the subject of naming, this was originally called `LEAVE {}`, which
was rejected because its semantics don't match the Raku language
feature of the same name. It was then renamed to `FINALLY {}` where it
was implemented on CPAN. This has been rejected too on grounds that
it's too similar to the proposed `try/catch/finally`, and it shouldn't
be a SHOUTY PHASER BLOCK. All the SHOUTY PHASER BLOCKS are declarations,
activated by their mere presence at compiletime, whereas `defer {}` is
a statement which only takes effect if dynamic execution actually
encounters it. The p5p mailing list and the CPAN module's RT queue both
contain various other rejected naming ideas, such as UNWIND, UNSTACK,
CLEANUP, to name three. 

Another rejected idea is that of conditional enqueue:

  defer if (EXPR) { BLOCK }

as this adds quite a bit of complication to the grammar, for little
benefit. As currently the grammar requires a brace-delimited block to
immediately follow the `defer` keyword, it is possible that other ideas
- such as this one - could be considered at a later date however.

## Open Issues

Design-wise I don't feel there are any remaining unresolved questions.

Implementation-wise the code still requires some work to finish it off;
it is not yet in a merge-ready state.

## Copyright

Copyright (C) 2021, Paul Evans.
This document and code and documentation within it may be used,
redistributed and/or modified under the same terms as Perl itself.

-- 
Paul "LeoNerd" Evans

leonerd@leonerd.org.uk      |  https://metacpan.org/author/PEVANS
http://www.leonerd.org.uk/  |  https://www.tindie.com/stores/leonerd/

Thread Next


nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About