Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW
 
PerlMonks  

Re^3: [DSL] disabling packages in Perl? (meditation)

by haukex (Archbishop)
on Apr 23, 2016 at 20:28 UTC ( [id://1161332]=note: print w/replies, xml ) Need Help??


in reply to Re^2: [DSL] disabling packages in Perl? (meditation)
in thread [DSL] disabling packages in Perl?

Hi Rolf,

I've finally had some time to think about this some more. I don't think there's a clear answer here, but I've included some sample code in which I've experimented with a few things.

First, the basic library, SQL_DSL.pm:

#!perl use warnings; use strict; package SQL_DSL; use Exporter 'import'; our @EXPORT_OK = qw/SQL WHERE/; use Carp; sub SQL (&;@) { # $SQL_DSL::IN_SQL is *not* required when using # SQL_DSL_Transform or SQL_DSL_Keyword croak "Already in SQL block" if $SQL_DSL::IN_SQL; local $SQL_DSL::IN_SQL = 1; print "Exec SQL\n"; # Debug shift->(@_) } sub WHERE (&;@) { croak "WHERE not in SQL block" unless $SQL_DSL::IN_SQL; print "Exec WHERE\n"; # Debug shift->(@_) } package LIKE; use Carp; sub AUTOLOAD { our $AUTOLOAD; croak "$AUTOLOAD not in SQL block" unless $SQL_DSL::IN_SQL; print "Exec $AUTOLOAD (@_)\n"; # Debug } 1;

Next, my test script, in which one of the three sets of use statements at the top needs to be uncommented:

#!/usr/bin/env perl use warnings; use strict; # Version "one": #use SQL_DSL qw/SQL WHERE/; # Version "two": #use SQL_DSL_Keyword; # Version "three": #use SQL_DSL 'SQL'; #use Filter::PPI::Transform 'SQL_DSL_Transform'; my $x = '%abc%'; SQL { WHERE { print "Where: ",__PACKAGE__, "\n"; b LIKE $x }; }; WHERE {}; # dies b/c not in SQL { ... }

The output of version "one" is:

Exec SQL Exec WHERE Where: main Exec LIKE::b (LIKE %abc%) WHERE not in SQL block at ...

The output of versions "two" and "three" are:

Exec SQL Exec WHERE Where: SQL_DSL Exec LIKE::b (LIKE %abc%) Can't call method "WHERE" without a package or object reference at ...

As you can see, version "one" doesn't really add any magic except en-/disabling the SQL_DSL::WHERE and LIKE::AUTOLOAD functions in dynamic scope.

Versions "two" and "three" get a little more interesting. They both work by inserting the declaration "package SQL_DSL;" inside the SQL { ... } block.

Version "two" uses the experimental keyword plugin feature that is available as of v5.12. It seems to be a better solution than source filters, but it's been in experimental state for quite a while and back then the discussion sounded like it might not stay. Anyway, I implemented it using Keyword::Simple, this is SQL_DSL_Keyword.pm:

#!perl package SQL_DSL_Keyword; use warnings; use 5.012; # for experimental pluggable keyword API use parent 'SQL_DSL'; use Carp; use Keyword::Simple; sub import { Keyword::Simple::define SQL => sub { ${$_[0]}=~s/^\s*\{/SQL_DSL::SQL { package SQL_DSL; / or croak "SQL keyword must be followed by a block"; }; } sub unimport { Keyword::Simple::undefine 'SQL'; } 1;

Lastly, version "three" is implemented with a source filter - yes, I know they're widely regarded as bad, and for good reason, but at least this one uses PPI for a hopefully decent parse, this is SQL_DSL_Transform.pm (based on PPI::Transform::UpdateCopyright and Programmatically Updating Code with PPI):

#!perl package SQL_DSL_Transform; use warnings; use strict; use Params::Util qw/_INSTANCE/; use parent 'PPI::Transform'; sub new { shift->SUPER::new(@_) } sub document { my $self = shift; my $document = _INSTANCE(shift, 'PPI::Document') or return undef; my $elements = $document->find( sub { $_[1]->isa('PPI::Token::Word') or return ''; $_[1]->content eq 'SQL' or return ''; $_[1]->snext_sibling->isa('PPI::Structure::Block') or return ' +'; return 1; } ); return undef unless defined $elements; return 0 unless $elements; foreach my $e (@$elements) { my $n = PPI::Document->new(\"package SQL_DSL;"); for my $t (reverse $n->tokens) { $e->snext_sibling->first_token->insert_after($t->remove) o +r die; } } return scalar @$elements; } 1;

I realized that automatically switching packages inside an SQL { ... } block might not be such a good idea, because it breaks something as simple as sub get_value { ... }; SQL { xyz LIKE get_value() };. So instead, it might make sense to use a module or a pragma that is only effective in its lexical scope, i.e. SQL { use SQL_DSL; ...; no SQL_DSL; }.

I agree that %^H does not seem to be useful to have something "unimported" or "undefined" at the end of a scope, it could only disable the functions like in my version "one" above. However, there is B::Hooks::EndOfScope, which is used by all three namespace::clean, namespace::autoclean and namespace::sweep! Or, one of the two methods above (pluggable keywords or source filters) could theoretically be used to insert a no statement at the end of the block. However, the problem that does remain is that these two methods are not particularly robust.

I haven't yet come up with any better ways to enable and disable syntactical features in a certain lexical scope at compile time without reaching into the internals (then again I'm not that much of an expert, so I could be missing something - I did run across Devel::Declare but haven't had a chance to look at it more closely*). It might be possible to define your DSL in a way that a SQL { ... } block complies cleanly, and then the features used within it are only enabled at run time (sub SQL (&) { enable_features(); shift->(); disable_features(); }), but that might be too limited. On the other hand, if you're trying to add syntactic sugar that goes too far beyond what Perl can do natively, you're essentially defining a mini-language that extends Perl. And if you don't want to slap a source filter around your whole program, the simplest solution might be: sub SQL ($) { ... };  SQL q{ WHERE b LIKE %abc% };

A lot of the above thoughts are more about the technical "how" than the "why" or "what for". Of course a lot depends on what you want your DSL to look like and what you want it to do. For SQL, there's already SQL::Abstract and SQL::Statement, or perhaps, as you mentioned elsewhere in this thread, you want something more like CGI's HTML generation statements, select(from(...),where(like('b','%abc%'))...

Anyways, that's just some more thoughts on the matter, maybe it helps!

Regards,
-- Hauke D

* Update: From the documentation of MooseX::Declare:

Warning: MooseX::Declare is based on Devel::Declare, a giant bag of crack originally implemented by mst with the goal of upsetting the perl core developers so much by its very existence that they implemented proper keyword handling in the core.

As of perl5 version 14, this goal has been achieved, and modules such as Devel::CallParser, Function::Parameters, and Keyword::Simple provide mechanisms to mangle perl syntax that don't require hallucinogenic drugs to interpret the error messages they produce.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://1161332]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (7)
As of 2024-04-23 12:24 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found