develooper Front page | perl.perl5.porters | Postings from November 2022

Corinna design - field $name = EXPR or field $name {BLOCK} ?

Thread Next
From:
Paul "LeoNerd" Evans
Date:
November 29, 2022 22:38
Subject:
Corinna design - field $name = EXPR or field $name {BLOCK} ?
Message ID:
20221129223812.121ed276@shy.leonerd.org.uk
TL;DR: Porters - please help me pick what some syntax should look like


For most of the past month I have been somewhat stuck on making more
progress with the `feature-class` branch, because of one fairly large
unresolved design question. The question is about how should defaulting
expressions for fields be written?

This has been discussed a lot over at
  https://github.com/Ovid/Cor/discussions/96

but here follows an overall summary of the current state.


There are two main candidates for this, written using an
assignment-like `=` operator, or using a method-like {} block:

  class Thing {                   |      class Thing {
    field $count = 20;            |        field $count { 20; }
  }                               |      }

In both cases we can put extra attributes of the field itself before
the defaulting expression. Most notably for the rest of this point, the
`:param` attribute says that the value for this field can be overridden
by passing a named parameter to the constructor:

  class Thing {                   |      class Thing {
    field $count :param = 20;     |        field $count :param { 20; }
  }                               |      }

  Thing->new( count => 30 );

The defaulting expression is a full expression, not just a constant. It
is evaluated within the constructor of *each* instance individually; it
is *not* evaluated just once at class compilation time. As such, it
behaves quite similarly to how you might see a defaulting expression in
a subroutine signature. This is intentional. "Similar things should
look similar" and all that.

So far, so good. Either notation feels right for this behaviour.

I just can't settle one a choice of one over the other, because
annoyingly each form has some things it makes much nicer (or even
possible at all), but yet makes certain other things impossible (or at
least, difficult).


## The case for `=`

In favour of the `=` notation we firstly have that it looks a bit
neater; it's less visually noisy. It's probably more what people might
expect to write, looking quite similar to what Raku has for example.

It also allows us to use the same `//=` operator that I recently added
to subroutine signatures, to ask for a defaulting expression that
additionally also applies even if the caller passed a value but that
value was undef:

  class Thing {                   |      class Thing {
    field $count :param //= 20;   |        ### No way to write this
                                  |        ### using {BLOCK} notation
  }                               |      }

  Thing->new( count => undef );  # 20 will still apply

This particular example looks silly, but it becomes a lot more useful
if you imagine code that forwards arguments from other places, or uses
Getopt::Long to parse commandline defaults, or any of a bunch of other
scenarios. Most of the reasons why `//=` is useful in subroutine
signatures equally apply here.


## The case for `{BLOCK}`

Conversely, block notation makes it much more obvious that the
expression doesn't happen just once at class-definition time, but
happens every time within the constructor of each instance. The
following looks more obvious in that respect, than if it had been
written with `=` and an uncontained expression:

  class Thing {                  |      class Thing {
    my $next_id = 1;             |        my $next_id = 1;
    field $id = $next_id++;      |        field $id { $next_id++ }
  }                              |      }

Similarly, because of the scoping braces, it doesn't look as visually
"odd" to find uses of the `$self` lexical just appearing as if out of
nowhere. It feels a bit more like a `method` block in this case, where
the same thing happens with a spontaneous `$self`.

  class Thing {                  |      class Thing {
    field $x = $self->_init_x;   |        field $x { $self->_init_x; }
  }                              |      }

The fact that there's a block here gives us a place to attach a
signature-like specification in parentheses, which explains what other
named-constructor-arguments ("NCAs") are used by the code block. [1]

The syntax works on blocks but doesn't really look right on `=`
expressions; either before or after the `=` symbol itself:

       class Thing {
         field $colour (:$red, :$green, :$blue) {
           Colour->new_rgb($red, $green, $blue);
         }

         ## would not nicely be expressible as either of
         ##
         ##   field $colour (:$red, :$green, :$blue) =
         ##       Colour->new_rgb($red, $green, $blue);
         ##
         ##   field $colour = (:$red, :$green, :$blue) ...
       }


In summary: It's annoying that of the two notations, neither one can
neatly handle all the things we'd like to throw at it.

  field $name = EXPR   can neatly do `//=` behaviours but has nowhere to
                       put NCA specifications, and feels weird to have
                       spontaneous lexical just appear and disappear
                       around it.

  field $name {BLOCK}  can neatly express NCAs and wraps a neat scope
                       around the expression, making spontaneous
                       lexicals and the deferred nature of its
                       evaluation feel much more natural, but cannot
                       express the "also overwrite undef please"
                       behaviour of `//=`.

It's not just a matter of which is nicer or neater. If you wanted to
use both `//=` and NCAs, there's literally no choice which makes them
both possible.


Can anyone see a way out of this quandry?

---

[1]: Aside on NCAs: We have these already in ADJUST blocks; the idea
  here is that they feel like named signature parameters but consume
  individual named arguments to a constructor; e.g.

    class Point {
      ADJUST (:$x, :$y) { say "Would make a point at ($x, $y)" }
    }
    Point->new( x => 10, y => 20 );

  This allows you to write code that can consume arbitrarily named
  arguments to the constructor, without needing to assign them all to
  same-named fields that are stored for all of the instance lifetime.

  This part of the design came out of the long-running discussion
    https://github.com/Ovid/Cor/issues/77

-- 
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