Ce chapitre vous présente les concepts sous-jacents aux références aux modules, packages et classes Perl. Il vous montre également comment créer quelques exemples de modules. Un module Perl est un ensemble de code Perl qui agit comme une bibliothèque d'appels de fonction. Le module terme en Perl est synonyme du paquet mot. Les packages sont une fonctionnalité de Perl 4, tandis que les modules sont répandus dans Perl 5. Vous pouvez conserver tout votre code Perl réutilisable spécifique à un ensemble de tâches dans un module Perl. Par conséquent, toutes les fonctionnalités relatives à un type de tâche sont contenues dans un fichier. Il est plus facile de construire une application sur ces blocs modulaires. Par conséquent, le mot module s'applique un peu plus que le paquet. Voici une introduction rapide aux modules. Certains sujets de cette section seront traités en détail dans le reste du livre. Lisez attentivement les paragraphes suivants pour obtenir un aperçu de ce qui vous attend à mesure que vous écrivez et utilisez vos propres modules. Ce qui est déroutant, c'est que les termes module et package sont utilisés de manière interchangeable dans toute la documentation Perl, et ces deux termes signifient la même chose. Donc, lorsque vous lisez des documents Perl, pensez quotpackagequot quand vous voyez quotmodulequot et vice versa. Donc, quelle est la prémisse pour l'utilisation des modules Eh bien, les modules sont là pour conditionner (pardonnez le calembour) les variables, les symboles et les éléments de données interconnectés ensemble. Par exemple, en utilisant des variables globales avec des noms très courants comme k. J. Ou i dans un programme n'est généralement pas une bonne idée. En outre, un compteur de boucle, i. Devraient être autorisés à travailler de façon indépendante dans deux parties différentes du code. Déclarer i en tant que variable globale, puis l'incrémenter à partir d'un sous-programme créera des problèmes ingérable avec votre code d'application car le sous-programme peut avoir été appelé à partir d'une boucle qui utilise également une variable appelée i. L'utilisation de modules en Perl permet de créer des variables de même nom à différents endroits distincts dans le même programme. Les symboles définis pour vos variables sont stockés dans un tableau associatif, appelé tableau de symboles. Ces tableaux de symboles sont propres à un package. Par conséquent, les variables du même nom dans deux packages différents peuvent avoir des valeurs différentes. Chaque module a sa propre table de symboles de tous les symboles qui y sont déclarés. Le tableau de symboles isole essentiellement les noms synonymes dans un module d'un autre. La table de symboles définit un espace de noms. C'est-à-dire un espace pour que des noms de variables indépendants existent. Ainsi, l'utilisation de modules, chacun avec sa propre table de symboles, empêche une variable déclarée dans une section d'écraser les valeurs d'autres variables avec le même nom déclaré ailleurs dans la même programme. En fait, toutes les variables de Perl appartiennent à un paquetage. Les variables d'un programme Perl appartiennent au paquetage principal. Tous les autres paquets dans un programme Perl sont soit imbriqués dans ce paquet principal, soit existent au même niveau. Il existe des variables vraiment globales, telles que le tableau de gestion des signaux SIG. Qui sont disponibles pour tous les autres modules dans un programme d'application et ne peuvent pas être isolés via des espaces de noms. Seuls les identificateurs de variables commençant par des lettres ou un trait de soulignement sont conservés dans une table de symboles de modules. Tous les autres symboles, tels que les noms STDIN. STDOUT. STDERR. ARGV. ARGOVUT. ENV. Inc. Et SIG sont obligés d'être dans le paquet principale. La commutation entre les packages n'affecte que les espaces de noms. Tout ce que vous faites lorsque vous utilisez un paquet ou un autre est de déclarer la table de symboles à utiliser comme table de symboles par défaut pour la recherche des noms de variable. Seules les variables dynamiques sont affectées par l'utilisation de tables de symboles. Les variables déclarées par l'utilisation du mot-clé my sont toujours résolues avec le bloc de code dans lequel elles se trouvent et ne sont pas référencées dans les tables de symboles. En fait, la portée d'une déclaration de paquet reste active uniquement dans le bloc de code dans lequel elle est déclarée. Par conséquent, si vous changez de tables de symboles à l'aide d'un paquet dans une sous-routine, la table de symboles d'origine en vigueur lors de l'appel sera restaurée Lorsque le sous-programme retourne. La modification des tables de symboles n'affecte que la recherche par défaut des noms de variables dynamiques. Vous pouvez toujours faire explicitement référence à des variables, des descripteurs de fichiers, etc. dans un package spécifique en ajoutant un packageName. Au nom de la variable. Vous avez vu le contexte d'un package lors de l'utilisation des références au chapitre 3. Un contexte de package implique simplement l'utilisation de la table de symboles par l'interpréteur Perl pour résoudre les noms de variable dans un programme. En changeant de tableaux de symboles, vous changez le contexte du package. Les modules peuvent être imbriqués dans d'autres modules. Le module imbriqué peut utiliser les variables et les fonctions du module dans lequel il est imbriqué. Pour les modules imbriqués, vous devez utiliser moduleName. NestedModuleName et ainsi de suite. L'utilisation du double côlon (::) est synonyme d'utilisation d'un retour de devis (). Toutefois, le double côlon est le moyen préféré de traiter les variables au sein des modules. L'adressage explicite des variables du module se fait toujours avec une référence complète. Par exemple, supposons que vous avez un module, Investissement. Qui est le paquet par défaut en cours d'utilisation, et que vous souhaitez adresser un autre module, Bonds. Qui est imbriqué dans le module d'investissement. Dans ce cas, vous ne pouvez pas utiliser Bond ::. Au lieu de cela, vous devriez utiliser Investment :: Bond :: pour adresser des variables et des fonctions dans le module Bond. L'utilisation de Bond :: impliquerait l'utilisation d'un paquet Bond qui est imbriqué dans le module principal et non dans le module Investment. La table de symboles d'un module est en fait stockée dans un tableau associatif des noms de modules apposés avec deux colonnes. La table des symboles d'un module appelé Bond sera appelée le tableau associatif Bond ::. Le nom de la table de symboles du module principal est main ::. Et peut même être raccourci à ::. De même, tous les paquetages imbriqués ont leurs symboles stockés dans des tableaux associatifs avec deux colonnes séparant chaque niveau d'imbrication. Par exemple, dans le module Bond qui est imbriqué dans le module Investment, le tableau associatif pour les symboles dans le module Bond sera nommé Investment :: Bond ::. Un typeglob est vraiment un type global pour un nom de symbole. Vous pouvez effectuer des opérations d'aliasing en les affectant à un typeglob. Une ou plusieurs entrées dans un tableau associatif pour les symboles seront utilisées lorsqu'une affectation via un typeglob est utilisée. La valeur réelle dans chaque entrée du tableau associatif est ce que vous faites référence lorsque vous utilisez la notation variableName. Ainsi, il existe deux façons de se référer aux noms de variable dans un package: Investissement :: argent Investissement :: factures Dans la première méthode, vous faites référence aux variables via une référence typeglob. L'utilisation de la table de symboles, Investissement ::. Est impliqué ici, et Perl permettra d'optimiser la recherche pour les symboles de l'argent et les factures. Il s'agit de la manière la plus rapide et préférée d'adresser un symbole. La deuxième méthode utilise une recherche pour la valeur d'une variable adressée par l'argent et les factures dans le tableau associatif utilisé pour les symboles, Investment :: explicitement. Cette recherche sera effectuée dynamiquement et ne sera pas optimisée par Perl. Par conséquent, la recherche est forcée de vérifier le tableau associatif chaque fois que l'instruction est exécutée. En conséquence, la deuxième méthode n'est pas efficace et doit être utilisée uniquement pour la démonstration de la façon dont la table de symboles est mise en œuvre en interne. Un autre exemple dans cette déclaration kamran husain provoque des variables, des sous-routines, et des poignées de fichier qui sont nommées via le symbole kamran pour être également adressées via le symbole husain. Autrement dit, toutes les entrées de symbole dans la table de symboles actuelle avec la clé kamran contiendront maintenant des références aux symboles adressés par la clé husain. Pour éviter une telle affectation globale, vous pouvez utiliser des références explicites. Par exemple, l'instruction suivante vous permettra d'aborder le contenu de husain via la variable kamran. Kamran husain Cependant, tous les tableaux tels kamran et husain ne seront pas les mêmes. Seules les références spécifiées seront modifiées. Pour résumer, lorsque vous affectez un typeglob à un autre, vous affectez toutes les entrées dans une table de symboles, quel que soit le type de variable auquel vous faites référence. Lorsque vous affectez une référence d'un type de variable à un autre, vous affectez une seule entrée dans la table des symboles. Un fichier de module Perl a le format suivant: package ModuleName. Insérer le code du module. 1 Le nom de fichier doit s'appeler ModuleName. pm. Le nom d'un module doit se terminer dans la chaîne. pm par convention. L'instruction package est la première ligne du fichier. La dernière ligne du fichier doit contenir la ligne avec l'instruction 1. Cela retourne en effet une valeur vraie au programme d'application à l'aide du module. Si vous n'utilisez pas l'instruction 1, le module ne sera pas chargé correctement. L'instruction package indique à l'interpréteur Perl de commencer par un nouveau domaine d'espace de noms. Fondamentalement, toutes vos variables dans un script Perl appartiennent à un paquet appelé main. Chaque variable dans le paquet principal peut être appelée mainvariable. Heres la syntaxe de ces références: packageNamevariableName La citation simple () est synonyme de l'opérateur deux points (::). Je vois plus d'utilisations de l'opérateur :: dans le prochain chapitre. Pour l'instant, vous devez vous rappeler que les deux instructions suivantes sont équivalentes: packageNamevariableName packageName :: variableName La syntaxe double-colon est considérée comme standard dans le monde Perl. Par conséquent, pour préserver la lisibilité, j'utilise la syntaxe double-colon dans le reste de ce livre à moins qu'il soit absolument nécessaire de faire des exceptions pour prouver un point. L'utilisation par défaut d'un nom de variable renvoie au paquetage actif actif au moment de la compilation. Ainsi, si vous êtes dans le paquet Finance. pm et spécifiez une variable pv. La variable est en fait égale à Finance :: pv. Utilisation des modules Perl: utilisation vs requise Vous incluez des modules Perl dans votre programme en utilisant l'utilisation ou l'instruction require. Heres la façon d'utiliser l'une de ces instructions: utilisez ModuleName require ModuleName Notez que l'extension. pm n'est pas utilisée dans le code ci-dessus. Notez également qu'aucune instruction ne permet d'inclure un fichier plus d'une fois dans un programme. La valeur renvoyée de true (1) en tant que dernière instruction est requise pour permettre à Perl de savoir qu'un module require d ou use d est chargé correctement et permet à l'interpréteur Perl d'ignorer les recharges. En général, il vaut mieux utiliser l'instruction Use Module que l'instruction require Module dans un programme Perl pour rester compatible avec les versions futures de Perl. Pour les modules, vous pouvez envisager de continuer à utiliser l'instruction require. Heres pourquoi: L'instruction use fait un peu plus de travail que l'instruction require car elle modifie l'espace de noms du module qui inclut un autre module. Vous voulez que cette mise à jour supplémentaire de l'espace de noms soit effectuée dans un programme. Toutefois, lorsque vous écrivez du code pour un module, vous pouvez ne pas vouloir modifier l'espace de noms à moins que cela ne soit explicitement requis. Dans ce cas, vous utiliserez l'instruction require. L'instruction require inclut le chemin d'accès complet d'un fichier dans le tableau Inc afin que les fonctions et les variables dans le fichier de modules soient dans un emplacement connu pendant le temps d'exécution. Par conséquent, les fonctions importées à partir d'un module sont importées via une référence de module explicite à l'exécution avec l'instruction require. L'instruction use fait la même chose que l'instruction require car elle met à jour le tableau Inc avec les chemins d'accès complets des modules chargés. Le code de la fonction d'utilisation va également plus loin et appelle une fonction d'importation dans le module en cours d'utilisation d pour charger explicitement la liste des fonctions exportées au moment de la compilation, ce qui permet d'économiser le temps nécessaire à une résolution explicite d'un nom de fonction pendant l'exécution. En principe, l'instruction use est équivalente à require ModuleName import ModuleName liste des fonctions importées L'utilisation de l'instruction use change l'espace de noms de vos programmes car les noms des fonctions importées sont insérés dans la table des symboles. L'instruction require ne modifie pas l'espace de noms de vos programmes. Par conséquent, l'utilisation de l'instruction suivante ModuleName () est équivalente à cette instruction: require ModuleName Les fonctions sont importées à partir d'un module via un appel vers une fonction appelée import. Vous pouvez écrire votre propre fonction d'importation dans un module, ou vous pouvez utiliser le module Exportateur et utiliser sa fonction d'importation. Dans presque tous les cas, vous utiliserez le module Exportateur pour fournir une fonction d'importation au lieu de réinventer la roue. (Vous en apprendrez plus à ce sujet dans la section suivante.) Si vous décidez de ne pas utiliser le module Exportateur, vous devrez écrire votre propre fonction d'importation dans chaque module que vous écrivez. Il est beaucoup plus facile d'utiliser simplement le module Exporter et laisser Perl faire le travail pour vous. Le module Sample Letter. pm La meilleure façon d'illustrer la sémantique de la façon dont un module est utilisé dans Perl est d'écrire un module simple et de montrer comment l'utiliser. Prenons l'exemple d'un requin de prêt local, Rudious Maximus, qui est tout simplement fatigué de taper le même quotrequest pour les lettres de paiement. Être un passionné d'ordinateurs et de Perl, Rudious prend l'approche des programmeurs paresseux et écrit un module Perl pour l'aider à générer ses mémos et ses lettres. Maintenant, au lieu de taper dans les champs dans un fichier de modèle de mémo, tout ce qu'il a à faire est de taper quelques lignes pour produire sa note agréable, menaçant. La liste 4.1 vous montre ce qu'il doit taper. Liste 4.1. Utilisation du module Lettre. 1 usrbinperl - w 2 3 Décompressez la ligne ci-dessous pour inclure le répertoire courant dans Inc. 4 push (Inc, pwd) 5 6 utilisez Letter 7 8 Letter :: To (quotMr. Gambling Manquot, l'argent pour Lucky Dog, Race 2quot) 9 Lettre :: ClaimMoneyNice () 10 Lettre :: ThankDem () 11 Letter :: Finish () L'instruction letter est présente pour forcer l'interpréteur Perl à inclure le code du module dans le programme d'application. Le module doit être situé dans le répertoire usrlibperl5, ou vous pouvez le placer dans n'importe quel répertoire répertorié dans le tableau Inc. Le tableau Inc est la liste des répertoires que l'interpréteur Perl recherchera lors de la tentative de chargement du code pour le module nommé. La ligne commentée (numéro 4) indique comment ajouter le répertoire de travail courant pour inclure le chemin d'accès. Les quatre lignes suivantes du fichier génèrent l'objet de la lettre. Heres la sortie de l'utilisation du module Lettre: À: M. Gambling Man Fm: Rudious Maximus, Prêt Shark Dt: Wed Feb 7 10:35:51 CST 1996 Re: L'argent pour Lucky Dog, Race 2 Il est venu à mon attention Que votre compte est bien fini. Vous allez nous payer bientôt Ou voudriez-vous que je vienne ovah Merci pour votre soutien. Le fichier du module Lettre est indiqué dans le Listing 4.2. Le nom du package est déclaré dans la première ligne. Parce que les fonctions de ce module seront exportées, j'utilise le module Exportateur. Par conséquent, l'utilisation de l'instruction Exporter est nécessaire pour hériter des fonctionnalités du module Exportateur. Une autre étape requise consiste à placer le mot exporté dans le tableau ISA pour permettre la recherche de Exported. pm. Le tableau ISA est un tableau spécial au sein de chaque package. Chaque élément du tableau indique où chercher une méthode si elle ne peut pas être trouvée dans le package actuel. L'ordre dans lequel les packages sont répertoriés dans le tableau ISA correspond à l'ordre dans lequel Perl recherche les symboles non résolus. Une classe qui est répertoriée dans le tableau ISA est appelée la classe de base de cette classe particulière. Perl cache les méthodes manquantes trouvées dans les classes de base pour les futures références. La modification du tableau ISA va vider le cache et faire en sorte que Perl recherche de nouveau toutes les méthodes. Voyons maintenant le code de Letter. pm dans le Listing 4.2. Listing 4.2. Le module Letter. pm. 1 paquet Lettre 2 3 requiert Exportateur 4 ISA (Exportateur) 5 6 head1 NOM 7 8 Lettre - Exemple de module pour générer un en-tête pour vous 9 10 head1 SYNOPSIS 11 12 utilisation Letter 13 14 Letter :: Date () , La société, l'adresse) 16 17 L'un des éléments suivants: 18 Lettre :: ClaimMoneyNice () 19 Lettre :: ClaimMoney () 20 Lettre :: ThreatBreakLeg () 21 22 Lettre :: ThankDem () 23 Lettre :: Finish () 24 25 head1 DESCRIPTION 26 27 Ce module fournit un bref exemple de génération d'une lettre pour un requin prêt à être voisin. 29 30 Le code commence après l'instruction quotcutquot. 31 coupe 32 33 EXPORT qw (date, 34 à, 35 ClaimMoney, 36 ClaimMoneyNice, 37 ThankDem, 38 fin) 39 40 41 Imprimer la date d'aujourd'hui 42 43 sous Lettre :: Date 44 date date 45 impression quotn Aujourd'hui est datequot 46 47 48 sous Lettre :: Vers 49 changement local (nom) 50 changement local (sujet) 51 impression quotn Pour: nomquot 52 impression quotn Fm: Rudious Maximus, Prêt Sharkquot 53 impression quotn Dt: quot, date 54 impression quotn Re: sujetquot 55 impression quotnnquot 56 Print quotnnquot 57 58 sub Lettre :: ClaimMoney () 59 print quotn Vous me devez de l'argent. Demandez-moi d'envoyer Bruno au quot 61 imprimer quotn de le collecter. Ou allez-vous payer 62 63 64 sous Lettre :: ClaimMoneyNice () 65 impression Quotn Il est porté à mon attention que votre compte est quotn 66 impression quotn chemin plus de due. quot 67 imprimer quotn Vous allez nous payer bientôt..quot 68 Imprimer quotn ou voulez-vous que je viens ovahquot 69 70 71 sous Lettre :: ThreatBreakLeg () 72 imprimé quotn apparemment des lettres comme celles-ci n'assistent pas 73 imprimer quotn Je vais devoir faire un exemple de vous 74 imprimer quotn n Vous voir à l'hôpital , Palquot 75 76 77 sous Lettre :: ThankDem () 78 print quotnn Merci pour votre soutien 79 80 81 sous Lettre :: Finish () 82 printf quotnnnn Sincerelyquot 83 printf quotn Rudious n quot 84 85 86 1 Les lignes contenant le signe égal sont utilisées Pour la documentation. Vous devez documenter chaque module pour votre propre référence Les modules Perl n'ont pas besoin d'être documentés, mais c'est une bonne idée d'écrire quelques lignes sur ce que votre code fait. Dans quelques années, vous pouvez oublier ce qu'est un module. Une bonne documentation est toujours un must si vous voulez vous rappeler ce que vous avez fait dans le passé je couvrir les styles de documentation utilisés pour Perl dans le chapitre 8. QuotDocumenting Perl Scripts. quot Pour cet exemple de module, l'instruction head1 commence la documentation. Tout jusqu'à l'instruction de coupe est ignoré par l'interpréteur Perl. Ensuite, le module répertorie toutes les fonctions exportées par ce module dans le tableau EXPORT. Le tableau EXPORT définit tous les noms de fonctions qui peuvent être appelés par un code externe. Si vous ne listez pas une fonction dans ce tableau EXPORT, elle ne sera pas vue par des modules de code externes. Après le tableau EXPORT est le corps du code, un sous-programme à la fois. Une fois tous les sous-programmes définis, l'instruction finale 1 termine le fichier de module. 1 doit être la dernière ligne exécutable dans le fichier. Examinons quelques-unes des fonctions définies dans ce module. La première fonction à examiner est la simple fonction Date, lignes 43 à 46, qui imprime la date et l'heure UNIX actuelles. Il n'y a aucun paramètre à cette fonction, et il ne retourne rien de significatif à l'appelant. Notez l'utilisation de ma variable avant la date à la ligne 44. Le mot-clé mon est utilisé pour limiter la portée de la variable à l'intérieur des fonctions date accolades. Le code entre les accolades est désigné sous le nom de bloc. Les variables déclarées dans un bloc sont limitées dans la portée à l'intérieur des accolades. En 49 et 50, le nom des variables locales et le sujet sont visibles pour toutes les fonctions. Vous pouvez également déclarer des variables avec le qualificatif local. L'utilisation de local permet à une variable d'être en portée pour le bloc courant ainsi que pour d'autres blocs de code appelés à partir de ce bloc. Ainsi, un x local déclaré dans un bloc est visible pour tous les blocs suivants appelés à partir de ce bloc et peut être référencé. Dans l'exemple de code suivant, la variable de nom des fonctions ToTitled peut être consultée, mais pas les données dans iphone. 1 sous Letter :: ToTitled 2 local (nom) shift 3 my (phone) shift L'exemple de code Letter. pm a montré comment extraire un paramètre à la fois. Le sous-programme To () prend deux paramètres pour configurer l'en-tête du mémo. L'utilisation de fonctions dans un module n'est pas différente de l'utilisation et la définition de modules Perl dans le même fichier de code. Sauf indication contraire, les paramètres sont transmis par référence. Plusieurs matrices passées dans une sous-routine, si elles ne sont pas explicitement déréférencées à l'aide de la barre oblique inverse, sont concaténées. Le tableau d'entrée dans une fonction est toujours un tableau de valeurs scalaires. Passer des valeurs par référence est la manière préférée dans Perl de transmettre une grande quantité de données dans un sous-programme. (Voir chapitre 3. quotReferences. quot) Un autre exemple de module: Finances Le module Finances, illustré dans la liste 4.3, est utilisé pour fournir des calculs simples pour les valeurs de prêt. L'utilisation du module Finance est simple. Toutes les fonctions sont écrites avec les mêmes paramètres, comme indiqué dans la formule pour les fonctions. Voyons comment la valeur future d'un investissement peut être calculée. Par exemple, si vous investissez quelques dollars, pv. Dans une obligation qui offre un taux de pourcentage fixe, r. Appliquée à des intervalles connus pour n périodes, quelle est la valeur de la liaison au moment de son expiration Dans ce cas, vous utiliserez la formule suivante: fv pv (1r) n La fonction pour obtenir la valeur future est déclarée comme FutureValue . Reportez-vous à la liste 4.3 pour savoir comment l'utiliser. Liste 4.3. Utilisation du module Finance. 1 usrbinperl - w 2 3 push (Inc, pwd) 4 utilisation Financement 5 6 prêt 5000.00 7 apr 3.5 APR 8 ans 10 en années. 9 10 ------------------------------------------------ ---------------- 11 Calculez la valeur à la fin du prêt si les intérêts 12 sont appliqués chaque année. 13 ------------------------------------------------- --------------- 14 fois année 15 fv1 Finance :: FutureValue (prêt, avril, heure) 16 imprimer quotn Si l'intérêt est appliqué à la fin de l'année 17 imprimer quotn La valeur future pour un Prêt de quot. prêt. Quotnquot 18 print quot à un APR de quot, apr. Quot pour quot, time, quot yearsquot 19 printf quot is 8.2f nquot. Fv1 20 21 ----------------------------------------------- ----------------- 22 Calculer la valeur à la fin du prêt si les intérêts 23 sont appliqués chaque mois. 24 ------------------------------------------------- --------------- 25 taux avril 12 avril 26 fois année 12 en mois 27 fv2 Finance :: FutureValue (prêt, taux, temps) 28 29 imprimer quotn Si l'intérêt est appliqué à la fin de Chaque moisquot 30 print quotn La valeur future d'un prêt de quot. prêt. Quotnquot 31 print quot à un APR de quot, apr. Quot pour quot, time, quot monthsquot 32 printf quot is 8.2f nquot. Fv2 33 34 printf quotn La différence de valeur est 8.2fquot, fv2 - fv1 35 printf quotn Par conséquent, en appliquant des intérêts à des périodes de temps plus courtes de 36 printf quotn nous obtenons réellement plus d'argent dans interest. nquot Voici l'échantillon d'entrée et de sortie du Listing 4.3. Testme Si l'intérêt est appliqué à la fin de l'année La valeur future d'un prêt de 5000 à un APR de 3,5 pour 10 ans est 7052.99 Si l'intérêt est appliqué à la fin de chaque mois La valeur future d'un prêt de 5000 à un APR de 3,5 pour 120 mois est 7091.72 La différence de valeur est 38.73 Par conséquent, en appliquant l'intérêt à des périodes plus courtes nous sommes réellement obtenir plus d'argent dans l'intérêt. La révélation dans la sortie est le résultat de la comparaison des valeurs entre fv1 et fv2. La valeur fv1 est calculée avec l'application d'intérêts une fois par an sur la durée de vie de l'obligation. Fv2 est la valeur si l'intérêt est appliqué chaque mois au taux d'intérêt mensuel équivalent. Le paquet Finance. pm est présenté dans le Listing 4.4 dans ses premiers stades de développement. Listing 4.4. Le package Finance. pm. 1 paquet Finances 2 3 Exiger Exportateur 4 ISA (Exportateur) 5 6 head1 Finance. pm 7 8 Calculateur financier - Calculs financiers faciles avec Perl 9 10 Tête 2 11 Utilisation Finances 12 13 Pv 10000.0 14 15 Taux 12.5 12 APR par mois. 16 17 temps 360 mois pour le prêt à maturité 18 19 fv FutureValue () 20 21 print fv 22 23 coupé 24 25 EXPORT qw (FutureValue, 26 PresentValue, 27 FVofAnnuity, 28 AnnuityOfFV, 29 getLastAverage, 30 getMovingAverage, 31 SetInterest) 32 33 34 Globals, le cas échéant 35 36 37 local defaultInterest 5.0 38 39 sub Financement :: SetInterest () 40 mon taux de changement de vitesse () 41 défautTaux d'intérêt 42 printf quotn defaultInterest ratequot 43 44 45 -------------- -------------------------------------------------- ---- 46 Notes: 47 1. Le taux d'intérêt r est donné en une valeur de 0-100. 48 2. Le n donné dans les termes est le taux auquel l'intérêt 49 est appliqué. 50 51 ------------------------------------------------ -------------------- 52 53 ---------------------------- ---------------------------------------- 54 Valeur actuelle d'un investissement donné 55 fv - Une valeur future 56 r - taux par période 57 n - nombre de période 58 ---------------------------------- ---------------------------------- 59 sub Finance :: FutureValue () 60 mon (pv, r, n ) 61 mon fv pv ((1 (r100)) n) 62 retour fv 63 64 65 ------------------------------ -------------------------------------- 66 Valeur actuelle d'un investissement donné 67 fv - un avenir Valeur 68 r - taux par période 69 n - nombre de périodes 70 ------------------------------------ -------------------------------- 71 sub Finances :: PresentValue () 72 mon pv 73 mon (fv, r, N) 74 pv fv ((1 (r100)) n) 75 retour pv 76 77 78 79 ----------------------------- --------------------------------------- 80 Obtenir la valeur future d'une rente donnée 81 mp - Paiement mensuel de la rente 82 r - taux par période 83 n - nombre de périodes 84 -------------------------------- ------------------------------------ 85 86 sous FVofAnnuity () 87 my fv 88 mon oneR 89 mon (Mp, r, n) 90 91 oneR (1 r) n 92 fv mp ((1R-1) r) 93 retour fv 94 95 96 ------------------ -------------------------------------------------- 97 Obtenir la rente à partir des informations suivantes 98 r - taux par période 99 n - nombre de période 100 fv - Valeur future 101 ---------------------- ---------------------------------------------- 102 103 sous AnnuityOfFV () 104 mp mp - Paiement mensuel de rente 105 mon oneR 106 mon (fv, r, n) 107 108 unR (1 r) n 109 mp fv (r (1R - 1)) 110 retour mp 111 112 113 - -------------------------------------------------- ---------------- 114 Obtenir la moyenne des dernières valeurs quotnquot dans un tableau. 115 ------------------------------------------------- ------------------- 116 Le dernier nombre de comptage d'éléments du tableau en valeurs 117 Le nombre total d'éléments en valeurs est en nombre 118 119 sub getLastAverage () 120 my (Comptage, nombre, valeurs) 121 my i 122 123 my a 0 124 retour 0 si (compte 0) 125 pour (i 0 compte i) 126 a valeursnumber - i - 1 127 128 renvoie un compte 129 130 131 --- -------------------------------------------------- --------------- 132 Obtenez une moyenne mobile des valeurs. 133 ------------------------------------------------- ------------------- 134 La taille de la fenêtre est le premier paramètre, le nombre d'éléments dans le tableau passé 135 est le suivant. (Cela peut facilement être calculé au sein de la fonction 136 à l'aide de la fonction scalar (), mais le sous-programme montré ici 137 est également utilisé pour illustrer comment passer des pointeurs.) La référence au tableau 138 de valeurs est passée ensuite, Référence à la place 139 les valeurs de retour doivent être stockées. 140 141 sub getMovingAve () 142 mon (compte, nombre, valeurs, movingAve) 143 mon i 144 mon a 0 145 mon v 0 146 147 retour 0 si (compte 0) 148 retour -1 si (compter le nombre) 149 retour - 2 if (count lt 2) 150 151 movingAve0 0 152 mobileAvastin - 1 0 153 pour (i0 iltcounti) 154 v valuesi 155 av count 156 movingAvei 0 157 158 pour icountnuméro 159 v valuesi 160 av count 161 v valuesi - count - 1 162 a - v compte 163 movingAvei a 164 165 return 0 166 167 168 1 Regardez la déclaration de la fonction FutureValue avec (). Les trois signes de dollar ensemble signifient trois nombres scalaires étant passé dans la fonction. Cette portée supplémentaire est présente pour valider le type des paramètres passés dans la fonction. Si vous deviez passer une chaîne au lieu d'un nombre dans la fonction, vous obtiendrez un message très semblable à celui-ci: Trop d'arguments pour Finance :: FutureValue à. f4.pl ligne 15, près quottime) quot Exécution de. f4.pl abandonné en raison d'erreurs de compilation. L'utilisation de prototypes lors de la définition des fonctions vous empêche d'envoyer des valeurs autres que ce que la fonction attend. Utilisez ou pour passer dans un tableau de valeurs. Si vous passez par référence, utilisez ou pour afficher une référence scalaire à un tableau ou un hachage, respectivement. Si vous n'utilisez pas la barre oblique inverse, tous les autres types dans le prototype de liste d'arguments sont ignorés. D'autres types de disqualifiers incluent une esperluette pour une référence à une fonction, un astérisque pour n'importe quel type et un point-virgule pour indiquer que tous les autres paramètres sont facultatifs. Maintenant, regardons la déclaration de la fonction lastMovingAverage, qui spécifie deux entiers à l'avant suivis d'un tableau. La façon dont les arguments sont utilisés dans la fonction est d'assigner une valeur à chacun des deux scalaires, count et number. Tandis que tout le reste est envoyé au tableau. Regardez la fonction getMovingAverage () pour voir comment deux tableaux sont passés afin d'obtenir la moyenne mobile sur une liste de valeurs. La manière d'appeler la fonction getMovingAverage est illustrée dans le Listing 4.5. Liste 4.5. Utilisation de la fonction de moyenne mobile. 1 usrbinperl - w 2 3 push (Inc, pwd) 4 utilisation Finance 5 6 valeurs (12,22,23,24,21,23,24,23,23,21,29,27,26,28) 7 mv ( 0) 8 size scalar (valeurs) 9 print quotn Valeurs à utiliser avec nquot 10 print quot Nombre de valeurs size nquot 11 12 ------------------------ ---------------------------------------- 13 Calculer la moyenne de la fonction ci-dessus 14 - -------------------------------------------------- ------------- 15 ave Finance :: getLastAverage (5, taille, valeurs) 16 print quotn Moyenne des 5 derniers jours ave nquot 17 18 Finance :: getMovingAve (5, taille, valeurs, mv ) 19 print quotn Moyenne mobile avec fenêtre 5 jours n nquot Voici la sortie du Listing 4.5: Valeurs à travailler avec Nombre de valeurs 14 Moyenne des 5 derniers jours 26.2 La fonction getMovingAverage () prend deux scalaires puis deux références aux tableaux en tant que scalaires. Dans la fonction, les deux scalaires des tableaux sont déréférencés pour une utilisation en tant que tableaux numériques. L'ensemble de valeurs renvoyées est inséré dans la zone passée comme seconde référence. Si les paramètres d'entrée n'avaient pas été spécifiés pour chaque tableau référencé, la référence de tableau movingAve aurait été vide et aurait provoqué des erreurs au moment de l'exécution. En d'autres termes, la déclaration suivante est incorrecte: sub getMovingAve () La résultante des messages d'erreur d'un prototype de mauvaise fonction est la suivante: Utilisation de la valeur non initialisée à Finance. pm ligne 128. Utilisation de la valeur non initialisée à la ligne Finance. pm 128. Utilisation de la valeur non initialisée à Finance. pm ligne 128. Utilisation de la valeur non initialisée à Finance. pm ligne 128. Utilisation de la valeur non initialisée à Finance. pm ligne 128. Utilisation de la valeur non initialisée à Finance. pm ligne 133. Utilisation de la valeur non initialisée À Finance. pm ligne 135. Utilisation de la valeur non initialisée à Finance. pm ligne 133. Utilisation de la valeur non initialisée à Finance. pm ligne 135. Utilisation de la valeur non initialisée à Finance. pm ligne 133. Utilisation de la valeur non initialisée à Finance. pm ligne 135 Utilisation de la valeur non initialisée à Finance. pm ligne 133. Utilisation de la valeur non initialisée à Finance. pm ligne 135. Utilisation de la valeur non initialisée à Finance. pm ligne 133. Utilisation de la valeur non initialisée à Finance. pm ligne 135. Utilisation de la valeur non initialisée à Finance. pm line 133. Use of uninitialized value at Finance. pm line 135. Use of uninitialized value at Finance. pm line 133. Use of uninitialized value at Finance. pm line 135. Use of uninitialized value at Finance. pm line 133. Use of uninitialized value at Finance. pm line 135. Use of uninitialized value at Finance. pm line 133. Use of uninitialized value at Finance. pm line 135. Average of last 5 days 26.2 Moving Average with 5 days window This is obviously not the correct output. Therefore, its critical that you pass by reference when sending more than one array. Global variables for use within the package can also be declared. Look at the following segment of code from the Finance. pm module to see what the default value of the Interest variable would be if nothing was specified in the input. (The current module requires the interest to be passed in, but you can change this.) Heres a little snippet of code that can be added to the end of the program shown in Listing 4.5 to add the ability to set interest rates. 20 local defaultInterest 5.0 21 sub Finance::SetInterest() 22 my rate shift() 23 rate -1 if (rate lt 0) 24 defaultInterest rate 25 printf quotn defaultInterest ratequot 26 The local variable defaultInterest is declared in line 20. The subroutine SetInterest to modify the rate is declared in lines 21 through 26. The rate variable uses the values passed into the subroutine and simply assigns a positive value for it. You can always add more error checking if necessary. To access the defaultInterest variables value, you could define either a subroutine that returns the value or refer to the value directly with a call to the following in your application program: Finance::defaultInterest The variable holding the return value from the module function is declared as my variable . The scope of this variable is within the curly braces of the function only. When the called subroutine returns, the reference to my variable is returned. If the calling program uses this returned reference somewhere, the link counter on the variable is not zero therefore, the storage area containing the returned values is not freed to the memory pool. Thus, the function that declares my pv and then later returns the value of pv returns a reference to the value stored at that location. If the calling routine performs a call like this one: Finance::FVofAnnuity(monthly, rate, time) there is no variable specified here into which Perl stores the returned reference therefore, any returned value (or a list of values) is destroyed. Instead, the call with the returned value assigned to a local variable, such as this one: fv Finance::FVofAnnuity(monthly, rate, time) maintains the variable with the value. Consider the example shown in Listing 4.6, which manipulates values returned by functions. Listing 4.6. Sample usage of the my function. 1 usrbinperl - w 2 3 push(Inc, pwd) 4 use Finance 5 6 monthly 400 7 rate 0.2 i. e. 6 APR 8 time 36 in months 9 10 print quotn ------------------------------------------------quot 11 fv Finance::FVofAnnuity(monthly, rate, time) 12 printf quotn For a monthly 8.2f at a rate of 6.2f for d periodsquot, 13 monthly, rate, time 14 printf quotn you get a future value of 8.2f quot, fv 15 16 fv 1.1 allow 10 gain in the house value. 17 18 mo Finance::AnnuityOfFV(fv, rate, time) 19 20 printf quotn To get 10 percent more at the end, i. e. 8.2fquot, fv 21 printf quotn you need a monthly payment value of 8.2fquot, mo, fv 22 23 print quotn ------------------------------------------------ nquot Here is sample input and output for this function: testme ------------------------------------------------ For a monthly 400.00 at a rate of 0.20 for 36 periods you get a future value of 1415603.75 To get 10 percent more at the end, i. e. 1557164.12 you need a monthly payment value of 440.00 ------------------------------------------------ Modules implement classes in a Perl program that uses the object-oriented features of Perl. Included in object-oriented features is the concept of inheritance . (Youll learn more on the object-oriented features of Perl in Chapter 5. quotObject-Oriented Programming in Perl. quot) Inheritance means the process with which a module inherits the functions from its base classes. A module that is nested within another module inherits its parent modules functions. So inheritance in Perl is accomplished with the :: construct. Heres the basic syntax: SuperClass::NextSubClass. ThisClass. The file for these is stored in. SuperClassNextSubClass133 . Each double colon indicates a lower-level directory in which to look for the module. Each module, in turn, declares itself as a package with statements like the following: package SuperClass::NextSubClass package SuperClass::NextSubClass::EvenLower For example, say that you really want to create a Money class with two subclasses, Stocks and Finance . Heres how to structure the hierarchy, assuming you are in the usrlibperl5 directory: Create a Money directory under the usrlibperl5 directory. Copy the existing Finance. pm file into the Money subdirectory. Create the new Stocks. pm file in the Money subdirectory. Edit the Finance. pm file to use the line package Money::Finance instead of package Finance . Edit scripts to use Money::Finance as the subroutine prefix instead of Finance:: . Create a Money. pm file in the usrlibperl5 directory. The Perl script that gets the moving average for a series of numbers is presented in Listing 4.7. Listing 4.7. Using inheriting modules. 1 usrbinperl - w 2 aa pwd 3 aa . quotMoneyquot 4 push(Inc, aa) 5 use Money::Finance 6 values ( 12,22,23,24,21,23,24,23,23,21,29,27,26,28 ) 7 mv (0) 8 size scalar(values) 9 print quotn Values to work with nquot 10 print quot Number of values size nquot 11 ---------------------------------------------------------------- 12 Calculate the average of the above function 13 ---------------------------------------------------------------- 14 ave Money::Finance::getLastAverage(5,size, values) 15 print quotn Average of last 5 days ave nquot 16 Money::Finance::getMovingAve(5,size, values, mv) 17 foreach i (values) 18 print quotn Moving with 5 days window mvi nquot 19 20 print quotn Moving Average with 5 days window n nquot Lines 2 through 4 add the path to the Money subdirectory. The use statement in line 5 now addresses the Finance. pm file in the. Money subdirectory. The calls to the functions within Finance. pm are now called with the prefix Money::Finance:: instead of Finance:: . Therefore, a new subdirectory is shown via the :: symbol when Perl is searching for modules to load. The Money. pm file is not required. Even so, you should create a template for future use. Actually, the file would be required to put any special requirements for initialization that the entire hierarchy of modules uses. The code for initialization is placed in the BEGIN() function. The sample Money. pm file is shown in Listing 4.8. Listing 4.8. The superclass module for Finance. pm . 1 package Money 2 require Exporter 3 4 BEGIN 5 printf quotn Hello Zipping into existence for younquot 6 7 1 To see the line of output from the printf statement in line 5, you have to insert the following commands at the beginning of your Perl script: use Money use Money::Finance To use the functions in the Stocks. pm module, you use this line: use Money::Stocks The Stocks. pm file appears in the Money subdirectory and is defined in the same format as the Finance. pm file, with the exceptions that use Stocks is used instead of use Finance and the set of functions to export is different. A number of modules are included in the Perl distribution. Check the usrlibperl5lib directory for a complete listing after you install Perl. There are two kinds of modules you should know about and look for in your Perl 5 release, Pragmatic and Standard modules. Pragmatic modules, which are also like pragmas in C compiler directives, tend to affect the compilation of your program. They are similar in operation to the preprocessor elements of a C program. Pragmas are locally scoped so that they can be turned off with the no command. Thus, the command no POSIX turns off the POSIX features in the script. These features can be turned back on with the use statement. Standard modules bundled with the Perl package include several functioning packages of code for you to use. Refer to appendix B, quotPerl Module Archives, quot for a complete list of these standard modules. To find out all the. pm modules installed on your system, issue the following command. (If you get an error, add the usrlibperl5 directory to your path.) find usrlibperl5 - name perl quot. pmquot - print Extension modules are written in C (or a mixture of Perl and C) and are dynamically loaded into Perl if and when you need them. These types of modules for dynamic loading require support in the kernel. Solaris lets you use these modules. For a Linux machine, check the installation pages on how to upgrade to the ELF format binaries for your Linux kernel. The term CPAN (Comprehensive Perl Archive Network) refers to all the hosts containing copies of sets of data, documents, and Perl modules on the Net. To find out about the CPAN site nearest you, search on the keyword CPAN in search engines such as Yahoo. AltaVista, or Magellan. A good place to start is the metronet site . This chapter introduced you to Perl 5 modules and described what they have to offer. A more comprehensive list is found on the Internet via the addresses shown in the Web sites metronet and perl . A Perl package is a set of Perl code that looks like a library file. A Perl module is a package that is defined in a library file of the same name. A module is designed to be reusable. You can do some type checking with Perl function prototypes to see whether parameters are being passed correctly. A module has to export its functions with the EXPORT array and therefore requires the Exporter module. Modules are searched for in the directories listed in the Inc array. Obviously, there is a lot more to writing modules for Perl than what is shown in this chapter. The simple examples in this chapter show you how to get started with Perl modules. In the rest of the book I cover the modules and their features, so hang in there. I cover Perl objects, classes, and related concepts in Chapter 5.With weight vector I mean the vector with weights that you have to multiply the observations in the window that slides over your data with so if you add those products together it returns the value of the EMA on the right side of the window. For a linear weighted moving average the formula for finding the weight vector is: (1:n)sum(1:n) (in R code). This series of length n adds up to 1. For n10 it will be 0.01818182 0.03636364 0.05454545 0.07272727 0.09090909 0.10909091 0.12727273 0.14545455 0.16363636 0.18181818 the numbers 1 to 10 55, with 55 the sum of the numbers 1 to 10. How do you calculate the weight vector for an exponential moving average (EMA) of length n if n is the length of the window, then alphalt-2(n1) and ilt-1:n so EmaWeightVectorlt-((alpha(1-alpha)(1-i))) Is this correct Even though the EMA is not really confined to a window with a start and an end, shouldnt the weights add up to 1 just like with the LWMA Thanks Jason, any pointers of how to approximate the EMA filter to any desired precision by approximating it with a long-enough FIR filter There39s a perl script on en. wikipedia. orgwikihellip that made the image of the EMA weight vector, but I don39t understand it: if they set the number of weights to 15 why are there 20 red bars instead of 15 ndash MisterH Dec 19 12 at 22:40The belief that a change will be easy to do correctly makes it less likely that the change will be done correctly. An XP programmer writes a unit test to clarify his intentions before he makes a change. We call this test-driven design (TDD) or test-first programming . because an API39s design and implementation are guided by its test cases. The programmer writes the test the way he wants the API to work, and he implements the API to fulfill the expectations set out by the test. Test-driven design helps us invent testable and usable interfaces. In many ways, testability and usability are one in the same. If you can39t write a test for an API, it39ll probably be difficult to use, and vice-versa. Test-driven design gives feedback on usability before time is wasted on the implementation of an awkward API. As a bonus, the test documents how the API works, by example. All of the above are good things, and few would argue with them. One obvious concern is that test-driven design might slow down development. It does take time to write tests, but by writing the tests first, you gain insight into the implementation, which speeds development. Debugging the implementation is faster, too, thanks to immediate and reproducible feedback that only an automated test can provide. Perhaps the greatest time savings from unit testing comes a few months or years after you write the test, when you need to extend the API. The unit test not only provides you with reliable documentation for how the API works, but it also validates the assumptions that went into the design of the API. You can be fairly sure a change didn39t break anything if the change passes all the unit tests written before it. Changes that fiddle with fundamental API assumptions cause the costliest defects to debug. A comprehensive unit test suite is probably the most effective defense against such unwanted changes. This chapter introduces test-driven design through the implementation of an exponential moving average (EMA), a simple but useful mathematical function. This chapter also explains how to use the CPAN modules Test::More and Test::Exception . Unit Tests A unit test validates the programmer39s view of the application. This is quite different from an acceptance test, which is written from the customer39s perspective and tests end-user functionality, usually through the same interface that an ordinary user uses. In constrast, a unit test exercises an API, formally known as a unit. Usually, we test an entire Perl package with a single unit test. Perl has a strong tradition of unit testing, and virtually every CPAN module comes with one or more unit tests. There are also many test frameworks available from CPAN. This and subsequent chapters use Test::More . a popular and well documented test module.2 I also use Test::Exception to test deviance cases that result in calls to die .3 Test First, By Intention Test-driven design takes unit testing to the extreme. Before you write the code, you write a unit test. For example, here39s the first test case for the EMA (exponential moving average) module: This is the minimal Test::More test. You tell Test::More how many tests to expect, and you import the module with useok as the first test case. The BEGIN ensures the module39s prototypes and functions are available during compilation of the rest of the unit test. The next step is to run this test to make sure that it fails: At this stage, you might be thinking, Duh Of course, it fails. Test-driven design does involve lots of duhs in the beginning. The baby steps are important, because they help to put you in the mindset of writing a small test followed by just enough code to satisfy the test. If you have maintenance programming experience, you may already be familiar with this procedure. Maintenance programmers know they need a test to be sure that their change fixes what they think is broken. They write the test and run it before fixing anything to make sure they understand a failure and that their fix works. Test-driven design takes this practice to the extreme by clarifying your understanding of all changes before you make them. Now that we have clarified the need for a module called EMA (duh), we implement it: And, duh, the test passes: Yeeha Time to celebrate with a double cappuccino so we don39t fall asleep. That39s all there is to the test-driven design loop: write a test, see it fail, satisfy the test, and watch it pass. For brevity, the rest of the examples leave out the test execution steps and the concomitant duhs and yeehas. However, it39s important to remember to include these simple steps when test-first programming. If you don39t remember, your programming partner probably will.4 Exponential Moving Average Our hypothetical customer for this example would like to maintain a running average of closing stock prices for her website. An EMA is commonly used for this purpose, because it is an efficient way to compute a running average. You can see why if you look at the basic computation for an EMA: today39s price x weight yesterday39s average x (1 - weight) This algorithm produces a weighted average that favors recent history. The effect of a price on the average decays exponentially over time. It39s a simple function that only needs to maintain two values: yesterday39s average and the weight. Most other types of moving averages, require more data storage and more complex computations. The weight, commonly called alpha . is computed in terms of uniform time periods (days, in this example): 2 (number of days 1) For efficiency, alpha is usually computed once, and stored along with the current value of the average. I chose to use an object to hold these data and a single method to compute the average. Test Things That Might Break Since the first cut design calls for a stateful object, we need to instantiate it to use it. The next case tests object creation: I sometimes forget to return the instance ( self ) so the test calls ok to check that new returns some non-zero value. This case tests what I think might break. An alternative, more extensive test is: This case checks that new returns a blessed reference of class EMA . To me, this test is unnecessarily complex. If new returns something, it39s probably an instance. It39s reasonable to rely on the simpler case on that basis alone. Additionally, there will be other test cases that will use the instance, and those tests will fail if new doesn39t return an instance of class EMA . This point is subtle but important, because the size of a unit test suite matters. The larger and slower the suite, the less useful it will be. A slow unit test suite means programmers will hesitate before running all the tests, and there will be more checkins which break unit andor acceptance tests. Remember, programmers are lazy and impatient, and they don39t like being held back by their programming environment. When you test only what might break, your unit test suite will remain a lightweight and effective development tool. Please note that if you and your partner are new to test-driven design, it39s probably better to err on the side of caution and to test too much. With experience, you39ll learn which tests are redundant and which are especially helpful. There are no magic formulas here. Testing is an art that takes time to master. Satisfy The Test, Don39t Trick It Returning to our example, the implementation of new that satisfies this case is: This is the minimal code which satisfies the above test. length doesn39t need to be stored, and we don39t need to compute alpha. We39ll get to them when we need to. But wait, you say, wouldn39t the following code satisfy the test, too Yes, you can trick any test. However, it39s nice to treat programmers like grown-ups (even though we don39t always act that way). No one is going to watch over your shoulder to make sure you aren39t cheating your own test. The first implementation of new is the right amount of code, and the test is sufficient to help guide that implementation. The design calls for an object to hold state, and an object creation is what needed to be coded. Test Base Cases First What we39ve tested thus far are the base cases . that is, tests that validate the basic assumptions of the API. When we test basic assumptions first, we work our way towards the full complexity of the complete implementation, and it also makes the test more readable. Test-first design works best when the implementation grows along with the test cases. There are two base cases for the compute function. The first base case is that the initial value of the average is just the number itself. There39s also the case of inputting a value equal to the average, which should leave the average unchanged. These cases are coded as follows: The is function from Test::More lets us compare scalar values. Note the change to the instantiation test case that allows us to use the instance ( ema ) for subsequent cases. Reusing results of previous tests shortens the test, and makes it easier to understand. The implementation that satisfies these cases is: The initialization of alpha was added to new . because compute needs the value. new initializes the state of the object, and compute implements the EMA algorithm. self-gt is initially undef so that case can be detected. Even though the implementation looks finished, we aren39t done testing. The above code might be defective. Both compute test cases use the same value, and the test would pass even if, for example, self-gt and value were accidentally switched. We also need to test that the average changes when given different values. The test as it stands is too static, and it doesn39t serve as a good example of how an EMA works. Choose Self-Evident Data In a test-driven environment, programmers use the tests to learn how the API works. You may hear that XPers don39t like documentation. That39s not quite true. What we prefer is self-validating documentation in the form of tests. We take care to write tests that are readable and demonstrate how to use the API. One way to create readable tests is to pick good test data. However, we have a little bootstrapping problem: To pick good test data, we need valid values from the results of an EMA computation, but we need an EMA implementation to give us those values. One solution is to calculate the EMA values by hand. Or, we could use another EMA implementation to come up with the values. While either of these choices would work, a programmer reading the test cases would have to trust them or to recompute them to verify they are correct. Not to mention that we39d have to get the precision exactly right for our target platform. Use The Algorithm, Luke A better alternative is to work backwards through the algorithm to figure out some self-evident test data.5 To accomplish this, we treat the EMA algorithm as two equations by fixing some values. Our goal is to have integer values for the results so we avoid floating point precision issues. In addition, integer values make it easier for the programmer to follow what is going on. When we look at the equations, we see alpha is the most constrained value: today39s average today39s price x alpha yesterday39s average x (1 - alpha) alpha 2 (length 1) Therefore it makes sense to try and figure out a value of alpha that can produce integer results given integer prices. Starting with length 1, the values of alpha decrease as follows: 1, 23, 12, 25, 13, 27, and 14. The values 1, 12, and 25 are good candidates, because they can be represented exactly in binary floating point. 1 is a degenerate case, the average of a single value is always itself. 12 is not ideal, because alpha and 1 - alpha are identical, which creates a symmetry in the first equation: today39s average today39s price x 0.5 yesterday39s average x 0.5 We want asymmetric weights so that defects, such as swapping today39s price and yesterday39s average, will be detected. A length of 4 yields an alpha of 25 (0.4), and makes the equation asymmetric: today39s average today39s price x 0.4 yesterday39s average x 0.6 With alpha fixed at 0.4, we can pick prices that make today39s average an integer. Specifically, multiples of 5 work nicely. I like prices to go up, so I chose 10 for today39s price and 5 for yesterday39s average. (the initial price). This makes today39s average equal to 7, and our test becomes: Again, I revised the base cases to keep the test short. Any value in the base cases will work so we might as well save testing time through reuse. Our test and implementation are essentially complete. All paths through the code are tested, and EMA could be used in production if it is used properly. That is, EMA is complete if all we care about is conformant behavior. The implementation currently ignores what happens when new is given an invalid value for length . Although EMA is a small part of the application, it can have a great impact on quality. For example, if new is passed a length of -1, Perl throws a divide-by-zero exception when alpha is computed. For other invalid values for length . such as -2, new silently accepts the errant value, and compute faithfully produces non-sensical values (negative averages for positive prices). We can39t simply ignore these cases. We need to make a decision about what to do when length is invalid. One approach would be to assume garbage-in garbage-out. If a caller supplies -2 for length . it39s the caller39s problem. Yet this isn39t what Perl39s divide function does, and it isn39t what happens, say, when you try to de-reference a scalar which is not a reference. The Perl interpreter calls die . and I39ve already mentioned in the Coding Style chapter that I prefer failing fast rather than waiting until the program can do some real damage. In our example, the customer39s web site would display an invalid moving average, and one her customers might make an incorrect investment decision based on this information. That would be bad. It is better for the web site to return a server error page than to display misleading and incorrect information. Nobody likes program crashes or server errors. Yet calling die is an efficient way to communicate semantic limits (couplings) within the application. The UI programmer, in our example, may not know that an EMA39s length must be a positive integer. He39ll find out when the application dies. He can then change the design of his code and the EMA class to make this limit visible to the end user. Fail fast is an important feedback mechanism. If we encounter an unexpected die . it tells us the application design needs to be improved. Deviance Testing In order to test for an API that fails fast, we need to be able to catch calls to die and then call ok to validate the call did indeed end in an exception. The function diesok in the module Test::Exception does this for us. Since this is our last group of test cases in this chapter, here39s the entire unit test with the changeds for the new deviance cases highlighted: There are now 9 cases in the unit test. The first deviance case validates that length can39t be negative. We already know -1 will die with a divide-by-zero exception so -2 is a better choice. The zero case checks the boundary condition. The first valid length is 1. Lengths must be integers, and 2.5 or any other floating point number is not allowed. length has no explicit upper limit. Perl automatically converts integers to floating point numbers if they are too large. The test already checks that floating point numbers are not allowed so no explicit upper limit check is required. The implementation that satisfies this test follows: The only change is the addition of a call to die with an unless clause. This simple fail fast clause doesn39t complicate the code or slow down the API, and yet it prevents subtle errors by converting an assumption into an assertion. Only Test The New API One of the most difficult parts of testing is to know when to stop. Once you have been test-infected, you may want to keep on adding cases to be sure that the API is perfect. For example, a interesting test case would be to pass a NaN (Not a Number) to compute . but that39s not a test of EMA . The floating point implementation of Perl behaves in a particular way with respect to NaNs6. and Bivio::Math::EMA will conform to that behavior. Testing that NaNs are handled properly is a job for the Perl interpreter39s test suite. Every API relies on a tremendous amount of existing code. There isn39t enough time to test all the existing APIs and your new API as well. Just as an API should separate concerns so must a test. When testing a new API, your concern should be that API and no others. Solid Foundation In XP, we do the simplest thing that could possibly work so we can deliver business value as quickly as possible. Even as we write the test and implementation, we39re sure the code will change. When we encounter a new customer requirement, we refactor the code, if need be, to facilitate the additional function. This iterative process is called continuous design . which is the subject of the next chapter. It39s like renovating your house whenever your needs change. 7 A system or house needs a solid foundation in order to support continuous renovation. Unit tests are the foundation of an XP project. When designing continuously, we make sure the house doesn39t fall down by running unit tests to validate all the assumptions about an implementation. We also grow the foundation before adding new functions. Our test suite gives us the confidence to embrace change. Quality Software Management: Vol. 1 Systems Thinking . Gerald Weinberg, Dorset House, 1991, p. 236. Part of the Test-Simple distribution, available at search. cpan. orgsearchqueryTest-Simple I used version 0.47 for this book. Just a friendly reminder to program in pairs, especially when trying something new. Thanks to Ion Yadigaroglu for teaching me this technique. In some implementations, use of NaNs will cause a run-time error. In others, they will cause all subsequent results to be a NaN. Don39t let the thought of continuous house renovation scare you off. Programmers are much quieter and less messy than construction workers.
No comments:
Post a Comment