6
votes

Exiger des modules Perl utilisant un nom aliasé

*** Ce qui suit est un arrière-plan pour vous aider à expliquer ce que j'ai essayé jusqu'à présent. Si vous préférez lire la question principale en premier, passez au bas. ***


Pour commencer

Mon module Baz invoque un certain nombre d'autres modules, tous similaires, dont chacun est un niveau plus bas dans l'espace de noms. Ceux qui vous intéressent ici composent le rôle Thing . En plus des instructions individuelles require , la liste de constantes ALL_THINGS énumère les modules Thing pertinents pour une utilisation ultérieure. Mon code d'origine ressemble à ceci:

BEGIN { *Things:: = *Foo::Bar::Baz:: ; }

Élimination de la redondance

Comme je l'ai mentionné, il y a beaucoup de choses code> modules, et j'ajoute encore plus. Chaque fois que je crée une nouvelle classe Thing , je dois ajouter une nouvelle instruction require et également ajouter le même texte identique à la liste ALL_THINGS . Afin d'éviter cette duplication, j'ai voulu remplacer les lignes individuelles require par une boucle itérant sur ALL_THINGS . J'ai ajouté ceci, qui fonctionne bien par lui-même:

foreach my $module (ALL_THINGS) {
    eval "require $module";
}

Cependant, cette solution ne semble pas bien fonctionner avec mon prochain changement.


Amélioration de la lisibilité

Le nom complet du module pour chaque chose est long et peu maniable. Je voudrais alias le nom du package pour le rendre plus facile à taper / lire. J'ai regardé Package :: Alias ​​, mais il semble que cela les utilisera , ce que j'aimerais éviter si possible. La meilleure solution à laquelle je suis parvenu jusqu'à présent est le modèle suggéré dans cette question :

package Foo::Bar::Baz;

use constant ALL_THINGS => qw{ Foo::Bar::Baz::ThingA Foo::Bar::Baz::ThingB ... };

require Foo::Bar::Baz::ThingA;
require Foo::Bar::Baz::ThingB;
[...]

Cela fonctionne également, dans le sens où cela me permet d'utiliser Thing :: ThingA-> classMethod . Cependant, sans surprise, cela ne fonctionne pas dans la boucle require ci-dessus, car require Thing :: ThingA recherche @INC pour Thing /ThingA.pm plutôt que Foo/Bar/Baz/ThingA.pm.


Question principale: les assembler

Je voudrais réduire les longs noms de paquet (c'est-à-dire Foo :: Bar :: Baz :: ThingA ) dans ma liste ALL_THINGS en Things :: ThingA , mais je peux toujours utiliser cette même liste pour construire mes instructions require dans une boucle.

  • Existe-t-il une manière différente d'aliaser Foo :: Bar :: Baz :: comme Things :: de sorte que je puisse exiger Things :: ThingA ?
  • Ou, si je fais la partie alias correctement, existe-t-il un moyen de déréférencer Things :: ThingA à Foo :: Bar :: Baz :: ThingA dans (ou avant?) l'évaluation pour que require trouve le bon package?
  • Existe-t-il une autre méthode généralement acceptée pour lier des paquets à différents niveaux du même espace de noms pour éviter le besoin de tout cela?

Questions bonus (liées à eval "require $ x" ):

  • Dans le perldoc pour la constante , il est indiqué que les listes em> ne sont pas réellement en lecture seule. Cela crée-t-il un problème de sécurité avec l'utilisation de eval ?
  • Si tel est le cas, y a-t-il un moyen plus sûr de le faire sans avoir à charger des modules supplémentaires?
  • Étant un peu nouveau en Perl, y a-t-il des différences plus subtiles que j'aurais pu manquer entre cette approche et la précédente (instructions individuelles require pour chaque module)?

Remarque: j'ai accepté la réponse de Dave Sherohman, car elle répond le mieux à la question que j'ai posée. Cependant, j'ai finalement implémenté une solution basée sur la réponse de lordadmira.


0 commentaires

5 Réponses :


3
votes

Y a-t-il une manière différente d'alias Foo :: Bar :: Baz :: as Things :: de telle sorte que je puisse exiger Things :: ThingA?

Oui. Il y a deux conditions pour que cela fonctionne:

  1. Créez l'alias de package comme vous l'avez déjà fait.

    my $obj7 = Foo::Bar::Baz->newThing("Thing7",foo => 42);
    print ref($obj7);   # probably  Foo::Bar::Baz::Thing7
    
  2. Créez un lien symbolique vers mylibs / Things à partir de votre répertoire mylibs / Foo / Bar / Baz (où mylibs est le chemin vers vos modules Perl)

    (Créez également un lien du fichier Foo / Bar / Baz.pm vers Things.pm , si vous le souhaitez)

Une fois que vous avez fait cela, et appelez eval "require Things :: Quux" ou eval "use Things :: Quux" , Perl se chargera le fichier dans mylibs / Things / Quux.pm , qui est le même que le fichier mylibs / Foo / Bar / Baz / Quux.pm . Ce fichier contient une instruction package Foo :: Bar :: Baz :: Quux , mais comme ce package a déjà un alias sur l'espace de noms Things :: Quux , tous ses sous-programmes et variables de package seront accessibles dans l'un ou l'autre des espaces de noms.

Existe-t-il une autre méthode généralement acceptée pour lier des paquets à différents niveaux du même espace de noms pour éviter le besoin de tout cela?

On ne sait pas quel est votre modèle d'objet, mais si * :: Thing1 , * :: Thing2 , etc. sont toutes des implémentations de certains courants classe de base, vous pouvez envisager une méthode de fabrique dans la classe de base.

package Foo::Bar::Baz;
sub newThing {
    my ($class, $implementation, @options) = @_;
    eval "use $class\::$implementation; 1"
        or die "No $implementation subclass yet";
    no strict 'refs';
    my $obj = "$class\::$implementation"->new(@options);
    return $obj;
}

Maintenant Foo :: Bar :: Baz :: Thing7 (qui peut ou peut ne pas avoir d'alias sur Things :: Thing7 ) ne sera chargé que si cela est nécessaire, par exemple, à partir d'un appel comme

BEGIN { *Things:: = *Foo::Bar::Baz:: }

2 commentaires

Les classes représentent en fait des choses très différentes et sont implémentées très différemment. Le rôle Thing garantit certains attributs pour tous, mais chaque classe nécessite qu'un sous-ensemble différent de ces attributs soit fourni en tant qu'arguments du constructeur, dérivant les autres. Pour contourner ce problème, chacun a une méthode de classe commune Thing-> find_all qui trouve toutes les choses de ce type sur le système (en utilisant des moyens non spécifiés - et variables), crée un objet représentant chacun, puis renvoie une liste de références à ces objets.


J'ai voté pour votre réponse car elle offre un moyen de le faire, et la discussion sur un modèle d'usine est utile. Cependant, l'utilisation de liens ne répond pas vraiment à ma question telle que posée. Bien que cela fonctionne techniquement, cela nécessite de modifier l'environnement dans lequel le script s'exécute plutôt que le script lui-même. Cela rend également l'étape (1) redondante, car avec le lien que vous avez suggéré, je pourrais simplement exiger Thing :: ThingA , aucun alias de package requis.



0
votes

J'ai trouvé une solution basée sur un exemple de perlmod qui semble fonctionner, même si j'essaie toujours de m'enrouler dans la tête. Je le publie dans l'espoir que quelqu'un puisse l'améliorer, ou au moins l'expliquer / fournir des commentaires.

foreach my $package (ALL_THINGS) {
    no strict "refs";
    $package = *{$package}{PACKAGE}."::".*{$package}{NAME};
    eval "require $package";
}

Modifier: Après avoir suivi le lien vers perlref, j'ai trouvé cette présentation dans la section (7):

* foo {NAME} et * foo {PACKAGE} sont l'exception, car ils renvoient des chaînes plutôt que des références. Ceux-ci renvoient le package et le nom du typeglob lui-même, plutôt que celui qui lui a été assigné. Ainsi, après * foo = * Foo :: bar , * foo deviendra "* Foo :: bar" lorsqu'il est utilisé comme chaîne, mais * foo {PACKAGE} et * foo {NAME} continueront de produire respectivement "main" et "foo".

À partir de là, il est logique que * Things {PACKAGE} se résout toujours en Foo :: Bar :: Baz , puisque c'est le package que nous travaillent dans, et donc le paquet auquel le typeglob "appartient". Dans mon code ci-dessus, $ package se résout en Things :: ThingA , pas en Things , donc nous obtenons * Things :: ThingA { PACKAGE} . De même, la deuxième partie devient * Things :: ThingA {NAME} . Je pourrais imaginer pourquoi cela fonctionne, mais la vérité est que je ne suis pas sûr.


1 commentaires

Oui, veuillez préciser. Donc, si $ package est Thing :: ThingA , le * {$ package} {PACKAGE} se développera en Foo :: Bar :: Baz en quelque sorte? et * {$ package} {NAME} deviendra ThingA ? Aussi, pourquoi est-ce une solution à votre problème?



3
votes

À quel point votre magie est-elle noire?

Nous savons tous que, pour exiger des modules, Perl regarde à travers @INC pour trouver le fichier qu'il veut charger. L'un des aspects peu connus (et encore moins utilisés) de ce processus est que @INC ne se limite pas à contenir uniquement des chemins de système de fichiers. Vous pouvez également y placer des coderefs, vous permettant de détourner le processus de chargement du module et de le plier à votre guise.

Pour le cas d'utilisation que vous avez décrit, quelque chose comme ce qui suit (non testé) devrait faire l'affaire:

use Module::Pluggable require => 1, search_path => ['Foo::Bar::Baz'];
my @plugins = plugins;

En gros, ce que cela fait, c'est que la première entrée de @INC , il regarde le nom du module demandé et, s'il démarre avec Thing :: , il charge le module Foo :: Bar :: Baz :: correspondant à la place. Simple et efficace, mais vraiment facile à confondre avec les futurs programmeurs de maintenance (y compris vous-même!), Alors utilisez-le avec prudence.


Comme autre approche, vous avez également le possibilité de spécifier un nom de package dans le module qui ne correspond pas au chemin physique du fichier - les deux sont normalement les mêmes par convention, pour faciliter la lecture et la maintenance du code, mais il n'y a aucune exigence technique pour qu'ils rencontre. Si le fichier ./lib/Foo/Bar/Baz/Xyzzy.pm contient

require Foo::Bar::Baz::Xyzzy;
Thing::Xyzzy::frob();

alors vous l'utiliserez en faisant

package Thing::Xyzzy;

sub frob { ... };

et Perl en sera parfaitement satisfait (même si vos collègues peuvent ne pas l'être).


Enfin, si vous voulez vous débarrasser de ALL_THINGS , jetez un œil à Module :: Pluggable . Vous lui donnez un espace de noms, puis il trouve tous les modules disponibles dans cet espace de noms et vous en donne une liste. Il peut également être défini sur require chaque module tel qu'il est trouvé:

BEGIN { unshift @INC, \&require_things }

sub require_things {
  my (undef, $filename) = @_;

  # Don't go into an infinite loop when you hit a non-Thing:: module!
  return unless $filename =~ /^Thing::/;

  $filename =~ s/^Thing::/Foo::Bar::Baz::/;
  require $filename;  
}

@plugins contient maintenant une liste de tous les modules Foo :: Bar :: Baz :: * , et ces modules ont déjà été chargés avec require . Ou vous pouvez simplement appeler des plugins sans affecter le résultat à une variable si vous ne vous souciez que de charger les modules et n'avez pas besoin d'une liste d'entre eux.


3 commentaires

J'aime ma magie comme mon café. Aussi noir que possible. Lol, j'ai en fait remarqué cette note quelque part dans les perldocs, mais je pensais qu'accrocher de @INC de cette façon pourrait être un peu au-delà de mes compétences. Je suis ravi de voir un exemple de mise en œuvre de celui-ci (et à quel point c'est simple en fait), et je pense que cette réponse pourrait justifier cette question à elle seule.


Il se trouve que j'ai aussi Module :: Pluggable disponible dans notre répertoire lib pré-emballé, donc je vais y jeter un coup d'œil également. Chaque chose est assez petit, mais les charger tous inutilement peut être coûteux. J'ai un hachage qui contient des listes - des sous-ensembles plus petits de Things à charger et à rechercher en fonction du type de système sur lequel il est exécuté, donc normalement je ne voudrais exiger que ce qui est dans cette liste. Mais il existe une option de repli "Tous", d'où la nécessité d'une liste de tous les modules. Cela facilite également l'écriture d'un test pour vérifier les fautes de frappe dans les sous-listes. Quoi qu'il en soit, Pluggable a fière allure.


@BryKKan - Même si vous décidez que vous ne voulez pas charger automatiquement toutes les choses, vous pouvez simplement laisser de côté le require => 1 et en obtenir une liste complète sans en charger, ce qui enregistre le tracas de maintenir manuellement une liste principale.



2
votes

Jouer avec des typesglobs, c'est comme une exagération nucléaire ici. Module :: Runtime est le moyen standard de charger des modules lors de l'exécution en fonction des données de configuration. À ce stade, tout peut être des variables ordinaires. Il n'y a aucun avantage à utiliser une constante ici.

Voici ma suggestion à partir de notre chat IRC.

package Foo::Bar::Baz;

use strict;
use Module::Runtime "require_module";
use List::Util "uniq";

my $prefix = "Things::LetterThings";
my %prop_module_map = (
   foo => [ qw{ThingC ThingF ThingY} ],
   bar => [ qw{ThingL ThingB} ],
   baz => [ qw{ThingA ThingB ThingC ThingE ThingG ThingH ThingZ} ],
   # or
   # ALL => [ qw{ThingA .. ThingZ} ],
);
my @all_modules = uniq map { @$_ } values %prop_module_map;

sub load_modules {
  my $self = shift;

  # map module list if matching property found, otherwise use ALL_MODULES
  my $modules = $prop_module_map{$self->prop} ?
     $prop_module_map{$self->prop} :
     \@all_modules;

  #only do the map operation on the list we actually need to use
  my @modules = map { join "::", $prefix, $_  } @$modules;

  foreach my $module (@modules) {
    require_module($module);
  }
}

1;
__END__


1 commentaires

C'est en effet la solution que j'ai finalement retenue. Merci de l'ajouter ici pour que d'autres puissent en profiter.



0
votes

NAME et PACKAGE sont là principalement pour que vous puissiez découvrir de quel package et nom provient une référence. Autre que cela est un moyen de renvoyer le nom de la variable plutôt que la valeur de la variable pour par exemple débogage.

printf "%s\n", __PACKAGE__; my $y = \*ydp; pp *$y{PACKAGE}, *$y{NAME};
W
("W", "ydp")


0 commentaires