Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PPC0022] - naming convention for exceptional vs. undef-returning methods #45

Open
leonerd opened this issue Jan 4, 2024 · 6 comments

Comments

@leonerd
Copy link
Contributor

leonerd commented Jan 4, 2024

The original PPC doc suggested two sets of "fetch an element from a meta-package" methods, named "get_..." and "can_..." to reflect the difference between methods that throw exceptions and methods that return undef when the requested entity does not exist.

my $metasym = $metapkg->get_symbol($name);  # throws if missing
my $metasym = $metapkg->can_symbol($name);  # return undef if missing

My original inspiration for can_... came from Perl's own $pkg->can(...) which returns a coderef or undef. But perhaps it's not so great.

In addition, the API shape suggested by #44 leads to an alternative form of fetching metasymbols directly, by doing things like

my $metavar = meta::variable->get('$some::package::variable');
my $metavar = meta::variable->get($pkgname, $varname);

Under that style, using ->can would not work. Perahps instead take inspiration from https://metacpan.org/pod/Object::Pad::MOP::Class#try_for_class and use get_... vs try_get_...

my $metasym = $metapkg->get_symbol($name);
my $metasym = $metapkg->try_get_symbol($name);

That also works for the constructor-style ones:

my $metavar = meta::variable->get('$some::package::variable');
my $metavar = meta::variable->try_get('$some::package::variable');
@leonerd
Copy link
Contributor Author

leonerd commented Jan 4, 2024

Aside Thoughts

As a total aside, I sometimes feel this a far more general and wide-spread naming convention problem across all of Perl, and could be solveable by something like a specific exception type combined with a callsite-modifying keyword. If we had a "Failure" exception, and a may keyword that prefixes an expression, effectively acting as a shorthand for wrapping a try/catch around the expression to capture and ignore a "Failure" and turn it into an undef; then such code could be written

method get_symbol($name) {
   # some internal code to check if the method exists
   ... or die Failure("$self does not contain a symbol called '$name'");
   return # something about the symbol
}

my $metasym = $metapkg->get_symbol("thing");  # throws if missing
my $metasym = may $metapkg->get_symbol("thing");  # return undef if missing

Though one subtlety of this is that you don't want the "Failure" exceptions leaking out to outer may expressions, so maybe it'd have to have lexical rather than dynamic effect. For example

sub print_symbol_addr($name) {
   my $metasym = $metapkg->get_symbol($name);
   printf "Symbol $name is at %x" . $metasym->refaddr;
}

may print_symbol_addr("not-a-symbol");

You don't want the "Failure" to leak out to the outer may call.

It's all a messy half-baked thought I haven't really finished thinking about yet... but in any case it shouldn't hold up the design of meta. ;)

@Grinnz
Copy link

Grinnz commented Jan 4, 2024

I was just thinking, it is much simpler and less mistake-prone to append // die to a call returning undef than to wrap a call that may throw an exception in choice of exception catching mechanism, and a simple keyword like that could be practically very useful, though it will be complex to design.

@leonerd
Copy link
Contributor Author

leonerd commented Jan 4, 2024

@Grinnz

I was just thinking, it is much simpler and less mistake-prone to append // die to a call returning undef

Mmmm.. It can be in some cases, but I don't like it as a general solution because it has real DRY-failures at times. Consider if getsym returns undef on failure:

my $metasym = getsym('$'.join("::", $self->{caller_package}, $self->symname_for("thing")))
   // die "Cannot obtain symbol named ... err... do I have to repeat the entire expression above all over again?";

The great thing about having the invoked function yield its own failure name is that it lets you generate that human-readable message a lot simpler.

@leonerd
Copy link
Contributor Author

leonerd commented Jan 4, 2024

To explain more succinctly: Turning an entire class of exception into an undef (i.e. with the may keyword) is easy as it only throws away information. But turning an undef into an exception is much harder as it would have to know lots of extra information - namely the human-readable message string of that exception. Thus I like the idea that usually code will "fail" with exception names as a general rule, and throw it away into undef if requested.

@rabbiveesh
Copy link
Contributor

As a total aside, I sometimes feel this a far more general and wide-spread naming convention problem across all of Perl, and could be solveable by something like a specific exception type combined with a callsite-modifying keyword. If we had a "Failure" exception, and a may keyword that prefixes an expression, effectively acting as a shorthand for wrapping a try/catch around the expression to capture and ignore a "Failure" and turn it into an undef; then such code could be written

method get_symbol($name) {
   # some internal code to check if the method exists
   ... or die Failure("$self does not contain a symbol called '$name'");
   return # something about the symbol
}

my $metasym = $metapkg->get_symbol("thing");  # throws if missing
my $metasym = may $metapkg->get_symbol("thing");  # return undef if missing

A cooll thing we could do building on this idea (which i'm all in on) is that we can detect if a sub could fail, and then have a strict pragma that forces you to handle failures at the callsite. that would be pretty awesome

@leonerd
Copy link
Contributor Author

leonerd commented Jan 5, 2024

I'm currently converting some no strict 'refs' code to using meta. The old code went:

no strict 'refs';
push @{"${caller}::ISA"}, __PACKAGE__;

The new code currently looks like:

my $callermeta = meta::package->get( $caller );

push @{ $callermeta->get_symbol( '@ISA' )->reference }, __PACKAGE__;

Except this fails with:

Package has no symbol named "@ISA" at ...

I could write this as

push @{ ($callermeta->try_get_symbol( '@ISA' ) // $callermeta->add_symbol('@ISA'))->reference }, __PACKAGE__;

but clearly that sucks for length, DRY, readability and overall sanity. We can do better.

I feel now that we need three variants of the get-style methods, which all differ in what they do if the requested thing didn't exist:

  • Throw an exception - the get_... variants
  • Return undef - the try_get_... variants
  • Attempt to create the thing if appropriate - needs a new name.

Of course, the latter case isn't always appropriate. You can create a missing package, glob, scalar/array/hash variable if it didn't exist, but you can't create a missing subroutine. So this variant wouldn't always exist.

Alternative ideas: Create a little set of (exported?) flag constants and supply an optional second argument to the get method:

get($name);  # or throw
get($name, META_MISSING_OK);  # or return undef
get($name, META_CREATE);  # or create

Ugly As Sin though, and it gets in the way of the idea that maybe meta::glob->get and friends could join package + symbol names:

my $metaglob = meta::glob->get($pkgname, "DATA");  # obtain the package's \*DATA glob

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants