Q: What to do when an annoying customer insists that he wants a CPU from a different brand on his Linux dedicated server ?
A: This: http://bitbucket.org/maugier/cpufake
lundi 26 septembre 2011
Dégoûter les démarcheurs téléphoniques avec Asterisk
Les démarcheurs téléphoniques, c'est chiant. Débarassons-nous en avec Aterisk. Au menu: un menu vocal horripilant, et un raccrochage dans la face au bout de quelques minutes.
D'abord, le matériel: un petit serveur de récup', déja allumé en permanence pour les diverses tâches de la maison. J'y ajoute une interface téléphonique analogique, une A400P de chez OpenVox, avec un module FXO (pour y brancher la ligne téléphonique) et un module FXS (pour y brancher mon ancien téléphone). J'intercale donc mon PBX entre mon téléphone et ma ligne.
Le serveur est sous Gentoo, j'y installe Asterisk 1.8 ainsi que le driver DAHDI. J'ajoute le module wctdm dans /etc/modules.autoload.d/kernel-2.6.
Il faut ensuite générer la config du driver avec dahdi_genconf, vérifier que les modules FXO et FXS sont bien détectés. Voici le contenu auto-généré de /etc/asterisk/dahdi.channels.conf:
;;; line="1 WCTDM/4/0 FXSKS"
signalling=fxs_ks
callerid=asreceived
context=sunrise
channel => 1
;;; line="2 WCTDM/4/1 FXOKS"
signalling=fxo_ks
callerid="Maison" <1>
context=maison
channel => 2
Notez que la terminologie est inversée pour le signaling: le module FXO, qui se comporte comme un téléphone, utilise le signaling FXS (car il cause avec la ligne FXS de mon fournisseur) alors que le module FXS, qui cause à mon téléphone, utilise le signaling FXO pour parler a mon téléphone FXO)
A ce stade on peut lancer Asterisk avec le script système, et attacher la console avec
Connected to Asterisk 1.8.5.0 currently running on glokrik (pid = 31554)
glokrik*CLI>
On teste en passant un appel bidon, d'abord vers le téléphone (la ligne 2)
glokrik*CLI> originate DAHDI/2 application sayphonetic ABCDEF
Le téléphone sonne, parle quand on décroche, puis raccroche, youpie.
On teste un appel sur la ligne sortante:
Cette fois-ci j'ai un petit bug, Asterisk ne détecte pas correctement le décrochage de la ligne et envoie le message immédiatement. A investiguer, mais sans conséquence pour ce que nous voulons faire.
Premièrement, rendons le PBX transparent en lui permettant de passer des appels entrants et sortants vers et depuis le combiné de la ligne 2. J'ajoute dans /etc/asterisk/extensions.conf
Au passage, je fais un tour dans sip.conf et y ajoute un compte pour mon PC, que je nomme monpc, dans le contexte maison; mon PC peut maintenant passer des appels sortants. Notez la définition dans le contexte sunrise qui fait également sonner mon pc quand un appel entrant arrive (le premier qui décroche prend l'appel.)
Maintenant, la partie rigolote: enregistrons un menu vocal idiot. J'utilise Asterisk pour me lancer des appels sur le pc et m'enregistrer:
Je décroche, et récite trèèèèès lentement, en me pincant le nez, et de manière générale avec une voix stupide: "Bonjour, vous êtes bien chez XXXXXX, meeeerci de choisir une des options suivantes: si vous appelez dans le contexte de blablabla, appuuuuuyez sur 1,..."
Asterisk enregistre ceci dans le fichier torture-main.gsm. Je procède de la même manière pour enregistrer chaque message, avec torture-submenu1.gsm, torture-submenu2.gsm, ..
J'ajoute maintenant dans extensions.conf le contexte [torture] dans lequel je perds les indésirables:
etc etc...
Note le Goto(torture,s,1) à la fin de chaque s de chaque contexte; si l'utilisateur attend la fin du message, il retourne a la racine du menu. Oui, il faut être rapide !
A la fin de la farce, en divers endroits, un Hangup() se débarasse purement et simplement de l'individu.
Notre contexte torture défini, reste à y envoyer les télémarketeurs. Commençons par une blacklist statique. On modifie le contexte de la ligne entrante:
[sunrise]
Ce qui a pour effet d'envoyer tous les numéros commençant par le préfixe mentionné (qui appartient, je crois, à un institut de sondage) vers le menu insupportable.
Ajoutons maintenant une redirection dynamique, pour ceci je vais utiliser une featuremap.
Je modifie encore le contexte entrant:
Et j'ajoute dans /etc/asterisk/features.conf, dans la section [applicationmap], la ligne suivante:
Qui a pour effet de rediriger l'appel de la ligne extérieure, la ligne 1, vers le contexte cible. "callee" indique que seul le destinataire (donc mon téléphone local) peut utiliser la fonction.
Et voila, il me suffit donc d'appuyer sur * pendant un appel, pour me débarasser du télémarketeur intempestif.
D'abord, le matériel: un petit serveur de récup', déja allumé en permanence pour les diverses tâches de la maison. J'y ajoute une interface téléphonique analogique, une A400P de chez OpenVox, avec un module FXO (pour y brancher la ligne téléphonique) et un module FXS (pour y brancher mon ancien téléphone). J'intercale donc mon PBX entre mon téléphone et ma ligne.
Le serveur est sous Gentoo, j'y installe Asterisk 1.8 ainsi que le driver DAHDI. J'ajoute le module wctdm dans /etc/modules.autoload.d/kernel-2.6.
Il faut ensuite générer la config du driver avec dahdi_genconf, vérifier que les modules FXO et FXS sont bien détectés. Voici le contenu auto-généré de /etc/asterisk/dahdi.channels.conf:
;;; line="1 WCTDM/4/0 FXSKS"
signalling=fxs_ks
callerid=asreceived
context=sunrise
channel => 1
;;; line="2 WCTDM/4/1 FXOKS"
signalling=fxo_ks
callerid="Maison" <1>
context=maison
channel => 2
Notez que la terminologie est inversée pour le signaling: le module FXO, qui se comporte comme un téléphone, utilise le signaling FXS (car il cause avec la ligne FXS de mon fournisseur) alors que le module FXS, qui cause à mon téléphone, utilise le signaling FXO pour parler a mon téléphone FXO)
A ce stade on peut lancer Asterisk avec le script système, et attacher la console avec
glokrik # asterisk -r
...
=========================================================================Connected to Asterisk 1.8.5.0 currently running on glokrik (pid = 31554)
glokrik*CLI>
On teste en passant un appel bidon, d'abord vers le téléphone (la ligne 2)
glokrik*CLI> originate DAHDI/2 application sayphonetic ABCDEF
Le téléphone sonne, parle quand on décroche, puis raccroche, youpie.
On teste un appel sur la ligne sortante:
glokrik*CLI> originate DAHDI/1/076xxxxxxx application sayphonetic ABCDEF
Cette fois-ci j'ai un petit bug, Asterisk ne détecte pas correctement le décrochage de la ligne et envoie le message immédiatement. A investiguer, mais sans conséquence pour ce que nous voulons faire.
Premièrement, rendons le PBX transparent en lui permettant de passer des appels entrants et sortants vers et depuis le combiné de la ligne 2. J'ajoute dans /etc/asterisk/extensions.conf
[maison]
exten => _X.,1,Dial(DAHDI/1/${EXTEN})
exten => _X.,2,Hangup()
[sunrise]
exten => s,1,Dial(DAHDI/2&SIP/monpc)
exten => s,2,Hangup()
Au passage, je fais un tour dans sip.conf et y ajoute un compte pour mon PC, que je nomme monpc, dans le contexte maison; mon PC peut maintenant passer des appels sortants. Notez la définition dans le contexte sunrise qui fait également sonner mon pc quand un appel entrant arrive (le premier qui décroche prend l'appel.)
Maintenant, la partie rigolote: enregistrons un menu vocal idiot. J'utilise Asterisk pour me lancer des appels sur le pc et m'enregistrer:
glokrik*CLI> originate SIP/monpc application record torture-main.gsm
Je décroche, et récite trèèèèès lentement, en me pincant le nez, et de manière générale avec une voix stupide: "Bonjour, vous êtes bien chez XXXXXX, meeeerci de choisir une des options suivantes: si vous appelez dans le contexte de blablabla, appuuuuuyez sur 1,..."
Asterisk enregistre ceci dans le fichier torture-main.gsm. Je procède de la même manière pour enregistrer chaque message, avec torture-submenu1.gsm, torture-submenu2.gsm, ..
J'ajoute maintenant dans extensions.conf le contexte [torture] dans lequel je perds les indésirables:
[torture]
exten => s,1,Background(torture-main)
exten => s,2,Goto(torture,s,1)
exten => 1,1,Goto(torture-submenu1)
exten => 2,1,Goto(torture-submenu2)
[torture-submenu1]
exten => s,1,Background(torture-submenu1)
exten => s,2,Goto(torture,s,1)
exten => 1,1,Goto(torture-sub-sub1)
exten => 2,1,Goto(torture-sub-sub2)
etc etc...
Note le Goto(torture,s,1) à la fin de chaque s de chaque contexte; si l'utilisateur attend la fin du message, il retourne a la racine du menu. Oui, il faut être rapide !
A la fin de la farce, en divers endroits, un Hangup() se débarasse purement et simplement de l'individu.
Notre contexte torture défini, reste à y envoyer les télémarketeurs. Commençons par une blacklist statique. On modifie le contexte de la ligne entrante:
[sunrise]
exten => s/_00494542801.,1,Goto(torture,s,1)
exten => s,1,Dial(DAHDI/2&SIP/monpc)
exten => s,2,Hangup()
Ce qui a pour effet d'envoyer tous les numéros commençant par le préfixe mentionné (qui appartient, je crois, à un institut de sondage) vers le menu insupportable.
Ajoutons maintenant une redirection dynamique, pour ceci je vais utiliser une featuremap.
Je modifie encore le contexte entrant:
[sunrise]
exten => s,1,Set(DYNAMIC_FEATURES=torture)
exten => s/_00494542801.,2,Goto(torture,s,1)
exten => s,2,Dial(DAHDI/2&SIP/monpc)
exten => s,3,Hangup()
Et j'ajoute dans /etc/asterisk/features.conf, dans la section [applicationmap], la ligne suivante:
torture => *,self/callee,ChannelRedirect(DAHDI/1-1,torture,s,1)
Qui a pour effet de rediriger l'appel de la ligne extérieure, la ligne 1, vers le contexte cible. "callee" indique que seul le destinataire (donc mon téléphone local) peut utiliser la fonction.
Et voila, il me suffit donc d'appuyer sur * pendant un appel, pour me débarasser du télémarketeur intempestif.
lundi 15 novembre 2010
Things learned while writing an erlang driver
I've spent a couple of days writing an erlang binding for zfec. The binding takes the form of a driver, that is, a shared library written in C that communicates with erlang according to the defined interface.
The reference doc was ambiguous for some points, which gets summarized here:
Q: How is the Command parameter of open_port() passed to the ErlDriverEntry .start callback ?
A: The Command is a string, and the first word (whitespace-separated) must be the name of the driver. The whole string, including the driver name, is passed to the callback.
Q: If I send a list of binaries to the port, can I expect to get every one of them separately with the outputv callback ? How does the SysIOVec struct work ?
A: NO. Small binaries, and presumably sub binaries spliced from a common one, may get merged in the resulting ErlIOVec. I've written this to iterate over an ErlIOVec. The doc defines:
and does not define (because it is platform-dependant, but in this case we want Unix):
size is the total byte size of the whole ErlIOVec. vsize is the number of chunks (which may not match the number of binaries in the original iolist). iov and binv both are arrays of size vsize.
binv references the parent refc binaries (so the same binary can appear twice, and you may not be interested in all its data). Use this to manipulate the refcounter of the binary if you need to keep the data around.
iov is the chunks of data you are actually interested in. The two fields should be self-explanatory :)
Q: Is it OK to define the outputv callback and not output one ?
A: yes, you will receive an IOVec with a single chunk
Q: Why does open_port seemingly randomly fail with "bad argument" ?
A: It will if you try to open a driver that is not loaded. Now, erl_ddll is smart and remembers who loaded which driver, and will unload a driver when it is not needed anymore. If you are testing from the shell, getting an uncaught exception will terminate the shell process and start a new one, which may cause your driver to get unloaded.
Q: Why is erl_ddll:load/2 asking for a path ? What is a reasonable value to provide ?
A: According to the OTP doc, the standard way is to use code:priv_dir(app_name)
The reference doc was ambiguous for some points, which gets summarized here:
Q: How is the Command parameter of open_port() passed to the ErlDriverEntry .start callback ?
A: The Command is a string, and the first word (whitespace-separated) must be the name of the driver. The whole string, including the driver name, is passed to the callback.
Q: If I send a list of binaries to the port, can I expect to get every one of them separately with the outputv callback ? How does the SysIOVec struct work ?
A: NO. Small binaries, and presumably sub binaries spliced from a common one, may get merged in the resulting ErlIOVec. I've written this to iterate over an ErlIOVec. The doc defines:
typedef struct {
int vsize;
int size;
SysIOVec* iov;
ErlDrvBinary** binv;
and does not define (because it is platform-dependant, but in this case we want Unix):
typedef struct {
char *iov_base;
size_t iov_len;
}
size is the total byte size of the whole ErlIOVec. vsize is the number of chunks (which may not match the number of binaries in the original iolist). iov and binv both are arrays of size vsize.
binv references the parent refc binaries (so the same binary can appear twice, and you may not be interested in all its data). Use this to manipulate the refcounter of the binary if you need to keep the data around.
Q: Is it OK to define the outputv callback and not output one ?
A: yes, you will receive an IOVec with a single chunk
Q: Why does open_port seemingly randomly fail with "bad argument" ?
A: It will if you try to open a driver that is not loaded. Now, erl_ddll is smart and remembers who loaded which driver, and will unload a driver when it is not needed anymore. If you are testing from the shell, getting an uncaught exception will terminate the shell process and start a new one, which may cause your driver to get unloaded.
Q: Why is erl_ddll:load/2 asking for a path ? What is a reasonable value to provide ?
A: According to the OTP doc, the standard way is to use code:priv_dir(app_name)
jeudi 4 novembre 2010
Changement a chaud de la configuration d'ejabberd
Depuis le 1er novembre, mon serveur jabber (qui dessert aussi swisslinux.org) dispose du client web Jappix, qui nécessite un point de service BOSH. BOSH est supporté nativement par ejabberd, mais il n'était pas activé dans la configuration .
J'aurais pu redémarrer ejabberd, mais cela aurait provoqué quelques désagréments: déconnexion temporaire de tous les membres utilisant une addresse @swisslinux.org, et déconnexion de la chat-room officielle. Heureusement, il existe un moyen très simple d'ajouter le service à chaud.
Dans la configuration de ejabberd, il existe un tuple appelé "listen", qui liste un certain nombre de "listeners". Chaque listener est responsable d'un port tcp. Par exemple, par défaut, on trouve le listener suivant:
Cela signifie qu'une instance du module ejabberd_c2s (le listener pour les connexions client) écoute sur le port 5222, avec une liste particulière d'options.
Le listener requis pour BOSH, que j'avais désactivé parce que je n'utilise pas l'interface d'administration web, est le suivant:
Le listener web est un peu particulier, puisqu'il peut écouter sur un seul port et y fournir plusieurs services sur des chemins différents. L'option http_bind active le service BOSH sur l'addresse /http-bind.
Pour ajouter ce listener à chaud, il suffit de se connecter au noeud erlang; sur ma distribution favorite, il suffit d'un simple
hostname # ejabberdctl debug
Qui nous donne un prompt erlang:
On démarre le listener supplémentaire:
(ejabberd@hostname)1> ejabberd_listener:start_listener(5280, ejabberd_http, [http_bind]).
Et hop, ça fonctionne... plus qu'a quitter le shell erlang avec un double Ctrl-C !
J'aurais pu redémarrer ejabberd, mais cela aurait provoqué quelques désagréments: déconnexion temporaire de tous les membres utilisant une addresse @swisslinux.org, et déconnexion de la chat-room officielle. Heureusement, il existe un moyen très simple d'ajouter le service à chaud.
Dans la configuration de ejabberd, il existe un tuple appelé "listen", qui liste un certain nombre de "listeners". Chaque listener est responsable d'un port tcp. Par exemple, par défaut, on trouve le listener suivant:
{5222, ejabberd_c2s, [{access, c2s}, {shaper, cs2_shaper}, starttls, {certfile, "/etc/ssl/jabber.pem"}]}
Cela signifie qu'une instance du module ejabberd_c2s (le listener pour les connexions client) écoute sur le port 5222, avec une liste particulière d'options.
Le listener requis pour BOSH, que j'avais désactivé parce que je n'utilise pas l'interface d'administration web, est le suivant:
{5280, ejabberd_http, [http_bind]}
Le listener web est un peu particulier, puisqu'il peut écouter sur un seul port et y fournir plusieurs services sur des chemins différents. L'option http_bind active le service BOSH sur l'addresse /http-bind.
Pour ajouter ce listener à chaud, il suffit de se connecter au noeud erlang; sur ma distribution favorite, il suffit d'un simple
hostname # ejabberdctl debug
Qui nous donne un prompt erlang:
(ejabberd@hostname)1>
On démarre le listener supplémentaire:
(ejabberd@hostname)1> ejabberd_listener:start_listener(5280, ejabberd_http, [http_bind]).
Et hop, ça fonctionne... plus qu'a quitter le shell erlang avec un double Ctrl-C !
mercredi 25 août 2010
shatag: un détecteur de doublons
Si vous êtes comme moi, il vous arrive fréquemment de déplacer à la main des fichiers volumineux d'un serveur à un autre. Vous n'utilisez pas encore partout un fs distribué, parce que la bande passante disponible ne le permet pas; vous avez des vieilles copies qui trainent dans les coins, et vous ne savez jamais quoi effacer lorsque vous faites le ménage, incapables de vous rappeler si vous avez déja téléchargé le fichier x ou y sur le serveur domestique.
Voici shatag, un petit script python pour inventorier des fichiers efficacement, et détecter les doublons ou la présence de fichiers identiques sur un stockage distant:
http://bitbucket.org/maugier/shatag/
Le programme calcule une somme de contrôle SHA-256 pour chaque fichier, et la met en cache dans un attribut étendu POSIX. La somme reste ainsi en cache même si le fichier est déplacé ou renommé. Une modification du fichier invalide la somme, qui peut être recalculée de manière transparente.
Un autre script permet ensuite de collecter ces sommes via ssh, et de les accumuler dans une base sqlite. Cette base peut ensuite être utilisée pour examiner le fs local, signaler les fichiers qui existent sur un serveur distant, a double ou pas !
Voici shatag, un petit script python pour inventorier des fichiers efficacement, et détecter les doublons ou la présence de fichiers identiques sur un stockage distant:
http://bitbucket.org/maugier/shatag/
Le programme calcule une somme de contrôle SHA-256 pour chaque fichier, et la met en cache dans un attribut étendu POSIX. La somme reste ainsi en cache même si le fichier est déplacé ou renommé. Une modification du fichier invalide la somme, qui peut être recalculée de manière transparente.
Un autre script permet ensuite de collecter ces sommes via ssh, et de les accumuler dans une base sqlite. Cette base peut ensuite être utilisée pour examiner le fs local, signaler les fichiers qui existent sur un serveur distant, a double ou pas !
jeudi 10 juin 2010
Monoïdes, Flèches, et Pliables: un cas particulier
Voici un joli exemple de la puissance du polymorphisme en Haskell. Le contexte est similaire au problème dit de "stream fusion": il s'agit d'effectuer en une seule passe plusieurs calculs qui demandent d'itérer sur une structure de données. L'exemple classique consiste à calculer la moyenne d'une liste:
Cette expression est inefficace, parce que le programme va traverser la liste deux fois: une fois pour calculer le sum, une fois pour le length. Bien sur, on peut éviter le double passage en implémentant la récursion à la main:
Ce qui a le désavantage d'être plutôt moche; exprimons le plutôt avec un fold:
Mieux, sans être extraordinaire. Observons qu'encore une fois, les deux opérations qui nous intéressent (la longeur et la somme) son associatives et admettent un neutre: nous sommes donc en présence d'un Monoid.
Coup de chance, ces deux notions qui nous intéressent sont déja dans la lib standard sous la forme du wrapper Sum, qui transfome n'importe quel Num en Monoid sous l'addition. Mieux: il existe une instance
Qui fait ce que vous pensez: appliquer deux mappend ou mconcat en parallèle.
Pour calculer notre moyenne, commençons par mapper chaque élément de la liste vers une paire de Monoids additifs, effectuons le concat, et il restera la division finale:
Encore un peu trop verbeux à mon goût: les lambdas peuvent être éliminés grâce aux combinateurs de flèches:
Cette fois-ci, nous sommes PointFree, un progrès apréciable. Mais nous pouvons encore améliorer les choses. Par exemple, et si nous généralisions la fonction à d'autres types de données (arbres, arrays, ...)
Nous avons utilisé mconcat, qui n'est autre qu'un fold de mappend. Si nous disposons d'un fold générique pour une structure de données, nous pouvons généraliser. Il existe une classe à ce dessein, la classe Foldable. La définition minimale d'un type Foldable consiste en une seule fonction:
instance Foldable t where
foldMap :: (Monoid m) => (a -> m) -> t a -> m
Qui applique une fonction (a->m) au contenu de la structure pour obtenir un Monoid, sur lequel on appelle mappend. Passer par un Monoid garantit que l'opération est associative, ce qui permet dans certains cas d'explorer la structure très efficacement (sujet d'un prochain article).
Evidemment, par défaut les Lists sont Foldable, et nous pouvons donc réécrire average en utilisant Foldable:
Pour la petite histoire: Il existe deux wrappers Bool, les Monoids And et Or, qui utilisent respectivement (&&) et (||) comme opération. Ceci permet d'écrire très simplement une fonction pour controler en une passe si un ensemble de Booléens ne contient que des True, que des False, ou un mix des deux:
Cette expression est équivalente à (or &&& and), avec l'avantage de calculer les deux folds en une seule passe....
... ou pas. Cette fonction est en réalité complètement stupide pour deux raisons:
average xs = sum xs / length xs
Cette expression est inefficace, parce que le programme va traverser la liste deux fois: une fois pour calculer le sum, une fois pour le length. Bien sur, on peut éviter le double passage en implémentant la récursion à la main:
average = uncurry (/) . foldAverage (0,0) where
foldAverage (sum,length) [] = (sum,length)
foldAverage (sum,length) (x:xs) = foldAverage (sum+x,length+1) xs
foldAverage (sum,length) [] = (sum,length)
foldAverage (sum,length) (x:xs) = foldAverage (sum+x,length+1) xs
Ce qui a le désavantage d'être plutôt moche; exprimons le plutôt avec un fold:
average = uncurry (/) . foldl' (\(s,l) x -> (s+x,l+1)) (0,0)
Mieux, sans être extraordinaire. Observons qu'encore une fois, les deux opérations qui nous intéressent (la longeur et la somme) son associatives et admettent un neutre: nous sommes donc en présence d'un Monoid.
Coup de chance, ces deux notions qui nous intéressent sont déja dans la lib standard sous la forme du wrapper Sum, qui transfome n'importe quel Num en Monoid sous l'addition. Mieux: il existe une instance
(Monoid a, Monoid b) => Monoid (a,b)
Qui fait ce que vous pensez: appliquer deux mappend ou mconcat en parallèle.
Pour calculer notre moyenne, commençons par mapper chaque élément de la liste vers une paire de Monoids additifs, effectuons le concat, et il restera la division finale:
average = (\(s,l) -> getSum s / getSum l) . mconcat . map (\x -> (Sum x, Sum 1))
Encore un peu trop verbeux à mon goût: les lambdas peuvent être éliminés grâce aux combinateurs de flèches:
import Control.Arrow
average = uncurry (/) . (getSum *** getSum)
. mconcat . map (Sum &&& (Sum.const 1))
average = uncurry (/) . (getSum *** getSum)
. mconcat . map (Sum &&& (Sum.const 1))
Cette fois-ci, nous sommes PointFree, un progrès apréciable. Mais nous pouvons encore améliorer les choses. Par exemple, et si nous généralisions la fonction à d'autres types de données (arbres, arrays, ...)
Nous avons utilisé mconcat, qui n'est autre qu'un fold de mappend. Si nous disposons d'un fold générique pour une structure de données, nous pouvons généraliser. Il existe une classe à ce dessein, la classe Foldable. La définition minimale d'un type Foldable consiste en une seule fonction:
foldMap :: (Monoid m) => (a -> m) -> t a -> m
Qui applique une fonction (a->m) au contenu de la structure pour obtenir un Monoid, sur lequel on appelle mappend. Passer par un Monoid garantit que l'opération est associative, ce qui permet dans certains cas d'explorer la structure très efficacement (sujet d'un prochain article).
Evidemment, par défaut les Lists sont Foldable, et nous pouvons donc réécrire average en utilisant Foldable:
average = uncurry (/) . (getSum *** getSum) . foldMap (Sum &&& (Sum.const 1)
Pour la petite histoire: Il existe deux wrappers Bool, les Monoids And et Or, qui utilisent respectivement (&&) et (||) comme opération. Ceci permet d'écrire très simplement une fonction pour controler en une passe si un ensemble de Booléens ne contient que des True, que des False, ou un mix des deux:
alltruefalse = (getAny *** getAll) . foldMap (Any &&& All)
Cette expression est équivalente à (or &&& and), avec l'avantage de calculer les deux folds en une seule passe....
... ou pas. Cette fonction est en réalité complètement stupide pour deux raisons:
- La définition du Monoid sur les tuples est trop stricte, et force l'itération de la liste jusqu'a la fin, même en présence de deux folds paresseux et court-circuités. Ce qui nous amène au point suivant,
- or et and sont court-circuités par défaut, et au moins un de ces deux terminera son évaluation immédiatement après le 1er élément (Si le premier élément est True, or retourne True, sinon and retourne False). La fusion des folds n'a donc aucun intérêt dans ce cas particulier !
Haskell, c'est concis !
Considérez le problème de la rotation d'une liste: On dispose d'une liste xs de longueur inconnue, on souhaite effectuer une rotation de n positions (déplacer n éléments du début à la fin.) Faisons une tentative d'implémentation en Haskell, tout a fait normale, en utilisant la récursion sur n:
C'est bien connu, le ++[x] est très loin d'être efficace (Les listes sont simplement chainées, et immutables, ce qui implique une réallocation complète de l'argument à gauche, donc ici O(l*n) allocations).
Sacrifions un peu de généralité pour une meilleure implémentation (mais qui n'accepte pas de n supérieur à la taille de la liste):
take est une fonction de la librairie standard qui retourne les n premiers éléments d'une liste, tandis que drop retire ces mêmes n premiers éléments.
Concis ? pas encore assez. Intéressons-nous à une classe souvent sous-estimée, le Monoid, dont voici la définition:
Rien d'extraordinaire ici, la classe Monoid regroupe les types pour lesquels il existe une opération naturelle associative, mappend, et un élément neutre des deux côtés, mzero. Quant à mconcat, c'est une fonction de convenance dont la définition par défaut est donnée.
Il existe beaucoup d'exemples de Monoids; on peut citer les nombres entiers dans deux contextes différents (addition et multiplication), le type Ordering (pour décrire un ordre hiérarchique), mais plus simplement: les listes !
Dans le cas des listes, on à simplement
Comme les Monoids sont fort utiles, on peut généraliser le sens de (++) et de concat à tous les Monoids (c'est d'ailleurs le cas en Caleskell):
Ceci nous permet donc d'utiliser (++) sur n'importe quel Monoid, et pas seulement sur les listes.
Voyez-vous ou tout ceci nous mène ? Probablement pas encore, sauf si vous
connaissez l'existence de l'instance suivante (dans la librairie standard):
dont l'implémentation est laissée comme un exercice au lecteur ;)
Traduction: si les listes [a] forment un Monoid, les fonctions (t -> [a]) en forment également un ! ce qui me permet par exemple de remplacer la définition
par une plus concise:
f = g ++ h
A condition que x soit d'un type appartenant à la classe Monoid.
Encore plus fort, notre instance est récursive ! en effet, on à
(on a juste renommé les variables de type).
Posons b = (t -> a), on a alors Monoid a => Monoid b, qui se dérive en
instance Monoid a => Monoid (t' -> (t -> a))
C'est exact, le truc fonctionne également sur les fonctions à plus d'un argument ! Ce qui nous permet de simplifier immédiatement
en
Si ça c'est pas de la concision...
rotate 0 xs = xs
rotate n (x:xs) = rotate (n-1) (xs ++ [x])
C'est bien connu, le ++[x] est très loin d'être efficace (Les listes sont simplement chainées, et immutables, ce qui implique une réallocation complète de l'argument à gauche, donc ici O(l*n) allocations).
Sacrifions un peu de généralité pour une meilleure implémentation (mais qui n'accepte pas de n supérieur à la taille de la liste):
rotate n xs = (drop n xs) ++ (take n xs)
take est une fonction de la librairie standard qui retourne les n premiers éléments d'une liste, tandis que drop retire ces mêmes n premiers éléments.
Concis ? pas encore assez. Intéressons-nous à une classe souvent sous-estimée, le Monoid, dont voici la définition:
class Monoid m where
mzero :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mzero
Rien d'extraordinaire ici, la classe Monoid regroupe les types pour lesquels il existe une opération naturelle associative, mappend, et un élément neutre des deux côtés, mzero. Quant à mconcat, c'est une fonction de convenance dont la définition par défaut est donnée.
Il existe beaucoup d'exemples de Monoids; on peut citer les nombres entiers dans deux contextes différents (addition et multiplication), le type Ordering (pour décrire un ordre hiérarchique), mais plus simplement: les listes !
Dans le cas des listes, on à simplement
instance Monoid [a] where
mzero = []
mappend = (++)
mconcat = concat
Comme les Monoids sont fort utiles, on peut généraliser le sens de (++) et de concat à tous les Monoids (c'est d'ailleurs le cas en Caleskell):
import Prelude hiding ((++), concat)
import Data.Monoid
(++) = mappend
concat = mconcatCeci nous permet donc d'utiliser (++) sur n'importe quel Monoid, et pas seulement sur les listes.
Voyez-vous ou tout ceci nous mène ? Probablement pas encore, sauf si vous
connaissez l'existence de l'instance suivante (dans la librairie standard):
instance Monoid a => Monoid (t -> a)
dont l'implémentation est laissée comme un exercice au lecteur ;)
Traduction: si les listes [a] forment un Monoid, les fonctions (t -> [a]) en forment également un ! ce qui me permet par exemple de remplacer la définition
f x = g x ++ h x
par une plus concise:
f = g ++ h
A condition que x soit d'un type appartenant à la classe Monoid.
Encore plus fort, notre instance est récursive ! en effet, on à
instance Monoid b => Monoid (t' -> b)
(on a juste renommé les variables de type).
Posons b = (t -> a), on a alors Monoid a => Monoid b, qui se dérive en
instance Monoid a => Monoid (t' -> (t -> a))
C'est exact, le truc fonctionne également sur les fonctions à plus d'un argument ! Ce qui nous permet de simplifier immédiatement
rotate n xs = (drop n xs) ++ (take n xs)
en
rotate = drop ++ take
Si ça c'est pas de la concision...
Inscription à :
Articles (Atom)
