Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine
 
PerlMonks  

What's the best way to use Any YAML (not YAML::Any)

by djerius (Beadle)
on Mar 14, 2014 at 21:30 UTC ( [id://1078397]=perlquestion: print w/replies, xml ) Need Help??

djerius has asked for the wisdom of the Perl Monks concerning the following question:

My module doesn't care which YAML is around, just that it can Dump and Load.

I'd like to make it YAML module agnostic, as there are several out there.

There's YAML::Any, but that ships with YAML, and what if that's not installed? This works; but it ain't pretty:

use List::Util qw[ first ]; my $class = first { eval "use $_; 1" } qw[ YAML::XS YAML::Syck YAML::T +iny YAML];
Is there a more refined approach?

Thanks!

Update 1: fixed code to stop after the first successful load.
Update 2: code is even better now

Update 3: The code should be polite and use an already loaded module, if possible.

use List::Util qw[ first ]; use Class::Load qw[ load_first_existing_class is_class_loaded ]; my @CLASSES = qw[ YAML::Tiny YAML::XS YAML::Syck YAML ]; my $class = ( first { is_class_loaded $_ } @CLASSES ) || ( load_first_existing_class @CLASSES );

Replies are listed 'Best First'.
Re: What's the best way to use Any YAML (not YAML::Any)
by kcott (Archbishop) on Mar 15, 2014 at 05:46 UTC

    G'day djerius,

    One of the main reasons for avoiding the use of 'string eval' is that it doesn't provide compile time warnings. If you use a BEGIN block, you can get around this.

    BEGIN { my @yaml_classes = qw{YAML::XS YAML::Syck YAML::Tiny YAML}; first { eval "use $_; 1" } @yaml_classes or die "Unable to load any of these YAML classes: @yaml_classe +s"; }

    That will give a compile time warning which, is not only very obvious with respect to the problem, but also will obviate the need for any specific testing to check if a YAML class was found and loaded, i.e. Test::More's use_ok('Your::Module::Name'), which I'd hope you already have, would suffice.

    Unable to load any of these YAML classes: YAML::XS YAML::Syck YAML::Ti +ny YAML at ... BEGIN failed--compilation aborted at ...

    If you need to know which YAML class was loaded and, as your code suggests, this is stored in a variable with package scope, I'd recommend something other than the all-too-common $class. You'd need to declare that variable outside the BEGIN block.

    my $yaml_class; BEGIN { my @yaml_classes = qw{YAML::XS YAML::Syck YAML::Tiny YAML}; $yaml_class = first { eval "use $_; 1" } @yaml_classes or die "Unable to load any of these YAML classes: @yaml_classe +s"; } print $yaml_class;
    "My module doesn't care which YAML is around, just that it can Dump and Load."

    I'd probably consider testing for that in the BEGIN block also. The following example clearly documents the intent and would only need a change, in a single and obvious place (i.e. near the top of the module code), if that requirement ever changed.

    BEGIN { # YAML classes to attempt to load (in order of preference) my @yaml_classes = qw{YAML::XS YAML::Syck YAML::Tiny YAML}; # Expecting to import these functons from whatever YAML class is l +oaded my @yaml_imports = qw{Dump Load}; my $yaml_class = first { eval "use $_ (\@yaml_imports); 1" } @yaml +_classes or die "Unable to load any of these YAML classes: @yaml_classe +s", " (using import list: @yaml_imports)"; }

    Update (improved solution): I've changed that final BEGIN block (i.e. the changes are in the block immediately above here).

    After posting, I realised that the original solution (see spoiler below) had some drawbacks: it loaded the YAML class before checking @EXPORT; it didn't allow imports from @EXPORT_OK; and, it would fail for all @yaml_classes if any particular class successfully loaded but then failed the @EXPORT test. The "improved solution" has none of these problems.

    As already stated (i.e. "original solution (see spoiler below)"), but apparently not clear to some, I repeat, the original solution is hidden in the spoiler below.

    BEGIN { my @yaml_classes = qw{YAML::XS YAML::Syck YAML::Tiny YAML}; my $yaml_class = first { eval "use $_; 1" } @yaml_classes or die "Unable to load any of these YAML classes: @yaml_classe +s"; # Only expecting whatever YAML class is loaded to export these fun +ctions my @yaml_funcs = qw{Dump Load}; no strict 'refs'; my %yaml_exports = map { $_ => 1 } @{$yaml_class . "::EXPORT"}; $yaml_exports{$_} or die "$yaml_class doesn't export '$_'" for @ya +ml_funcs; }

    [Minor Update: I've added some additional text, to the "Update (improved solution)" text, to make it clear that the original code is hidden in the spoiler.]

    -- Ken

      Thanks. A couple of comments:
      • Checking for Dump/Load is prudent, but all of the listed YAML modules implement the common Dump/Load API, so I don't think it's absolutely necessary.
      • The check for Dump/Load in your last version assumes the included module uses @EXPORT. I wouldn't rely on that, as there are alternates to Exporter which don't use that, and while these modules might currently use it, that behavior is not in the API. If they switch to something else, this'll break.

        A better way might be to perform an explicit import, e.g. something like

        eval { $yaml_class->import( 'Dump'); 1; } or croak( "Can't find Dump in $yaml_class\n" );
        or, if you can trust that the function isn't AUTOLOADED, look at the module's namespace for the function
        Package::Stash->new( $yaml_class )->has_symbol( '&Dump' ) or croak( "Can't find Dump in $yaml_class\n" );
        The only foolproof method is to just wait for a runtime error:
        my $Dump = $yaml_class . "::Dump"; [... later in the code ...] my $var = eval { &$Dump(...) } or croak( "bad return or Dump not imple +mented\n" );
      There's also another problem, which I didn't recognize until late last night (and didn't get around to posting about before your reply), namely that if there's a YAML module already loaded, it should use that. I've updated the OP above to illustrate my current code.
        "Checking for Dump/Load is prudent, but all of the listed YAML modules implement the common Dump/Load API, so I don't think it's absolutely necessary."

        Can you guarantee that you'll only ever use "the listed YAML modules"? Can you guarantee that you'll never want to use another function beyond Dump and Load in this, or a subclassed, module?

        Yes, it's prudent. No, it's not absolutely necessary. It's two lines of code that you can choose to add to your module or not.

        "The check for Dump/Load in your last version assumes the included module uses @EXPORT."

        No, that check occurred in the original version, not the update. The original solution was hidden in a spoiler (for anyone interested in seeing the changes that were made). I thought that "original solution (see spoiler below)" made this clear but apparently not: I've added additional text making this clearer.

        -- Ken

Re: What's the best way to use Any YAML (not YAML::Any)
by CountZero (Bishop) on Mar 15, 2014 at 12:52 UTC
    You will have to have some YAML-modules installed, so why don't you install YAML::Any?

    CountZero

    A program should be light and agile, its subroutines connected like a string of pearls. The spirit and intent of the program should be retained throughout. There should be neither too little or too much, neither needless loops nor useless variables, neither lack of structure nor overwhelming rigidity." - The Tao of Programming, 4.1 - Geoffrey James

    My blog: Imperial Deltronics
      That's a valid question. I'm not comfortable doing so for a few reasons:
      • YAML::Any is distributed as part of YAML. This is a module, not an application, so it might run in an environment where YAML is not available. I could certainly specify YAML as a requirement, but requiring installation of all of YAML just for YAML::Any seems like overkill. If YAML::Any were standalone, then I would have no reticence in requiring it.
      • As I read its code, YAML::Any won't use an already loaded module. I've decided that's an important thing to do.
        Does your application only rely on CORE modules? If not, you will have to assume that those non-CORE modules are installed with the users anyhow or they know how to install them. Little extra effort to install *some* YAML module too.

        CountZero

        A program should be light and agile, its subroutines connected like a string of pearls. The spirit and intent of the program should be retained throughout. There should be neither too little or too much, neither needless loops nor useless variables, neither lack of structure nor overwhelming rigidity." - The Tao of Programming, 4.1 - Geoffrey James

        My blog: Imperial Deltronics

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://1078397]
Approved by davies
Front-paged by ww
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others exploiting the Monastery: (3)
As of 2024-03-28 17:17 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found