Dinoblog

Le blog d'un Dinosaure

Dans absence d’articles, il y a absence.

Je sais qu’on m’aime, que j’ai un site qui fait fureur et que des millions de personnes attendent un article super intéressant sur mon blog pour affûter leurs claviers et poster pleins de commentaires et vénérer le llame … C’est une vérité mais qui risque de ne pas durer. Au moment où j’écris ces lignes, j’entreprend de révisé mon Bac.

Pour ces vacances qui vont être difficile pour moi, je coupe tout contact avec l’humanité et avec vous chers lecteurs en vous signalant que je ne suis pas près à poster un article avant le Bac.

Logiciel libre et gratuité

Les puristes du libre vous l’ont peut être déjà dit et moi je vous le redis, le logiciel libre n’est pas forcément gratuit. Prenons le cas de la licence GNU GPL. Il est spécifier explicitement dans cette licence que l’on peut faire payer l’acte physique de transmission d’une copie et même proposer une garantie contre rémunération. Donc les phrases du type: c’est un logiciel libre donc gratuit, sont factices. Tout simplement parce que la licence qui fait qu’un logiciel soit considéré comme libre n’implique pas qu’il soit gratuit et c’est une vérité.

Mais alors, pourquoi la grande majorité des logiciels libres sont gratuits ? J’ai réfléchi à la question et c’est vrai qu’on pourrait croire que le logiciel libre implique d’être gratuit au vue de la majorité des logiciels libres gratuits. On pourrait ce permettre de tout mettre dans un même sac. Mais, oh mon dieu, que vous allez devenir bête quand vous serez face à un logiciel libre payant car ça existe et l’entreprise Red Hat (l’entreprise éditrice du système Red Hat et de Fedora aussi) ou encore Novell (celle qui a fait la pub pour GNU/Linux et éditrice de openSUSE) le montrent encore aujourd’hui. Alors, doit on blâmer ces entreprises qui éditent des programmes payants dont il existe des alternatives gratuites et très semblables ? Je n’ai personnellement jamais utilisé RHEL ou openSUSE pour vous parler de la qualité de ces systèmes. Néanmoins, je ne doute pas de leurs qualités (on travaille mieux en général quand on a un chèque à l’arrivée).

Mais ce qui fait qu’un logiciel libre devienne gratuit, c’est à dire la conséquence à cette gratuité, c’est internet ou les moyens de supra communication. Prenons un exemple, je suis un apprenti développeur et j’ai fait mon programme super utile. Je le mets sous licence GPL mais je compte bien le vendre. Il arrivera sûrement que M. Bob vous le payera. Mais M. Bob sait ce qu’il s’applique aux logiciels libres et il compte bien vous l’acheter mais ensuite distribuer le logiciel gratuitement (car il en a le droit selon la licence). Mais cette distribution (pas le système type GNU/Linux, je sous-entends la distribution du logiciel), pour qu’elle soit non négligeable (c’est à dire en dehors de papa et maman), il faut internet. Et c’est internet qui va proliférer rapidement le logiciel de façon gratuite et légal. Maintenant, vous, apprenti développeur, pourquoi les gens iront acheter votre logiciel alors que M. Bob le distribue gratuitement ?

C’est avec cette logique qu’on a fait le système de don. Ainsi, l’apprenti développeur gagne (aléatoirement quand même) de l’argent et le logiciel est distribué d’office gratuitement car créer un modèle économique sur la vente de logiciel libre est impensable à cause de internet. Alors comment font les entreprises comme Red Hat et Novell pour avoir le statut d’entreprise (ce qui implique de gagner de l’argent) et faire du logiciel libre ? Le service ! En plus de cela, le développement d’un système comme RHEL est moins cher (car une communauté de bénévole aide l’entreprise). C’est en vendant des abonnements d’assistance, de formations et des services d’intégration que l’entreprise ce fait un peu des sous-sous.

Mais l’apprenti développeur, il n’a pas l’étoffe de faire une entreprise. Alors, dans ces conditions, pour quand même avoir un statut juridique et gagner de l’argent, on ce mets comme étant une association (certains diront que c’est mauvais). Aujourd’hui, je pense que c’est ce qu’il ce passe et que la gratuité d’un logiciel libre n’est dû qu’à internet et des possibilités (P2P) de partage qu’on a. Alors, éviter de dire qu’un logiciel libre implique qu’il soit gratuit. Vous pouvez juste dire qu’un logiciel est libre et gratuit pour rendre ces deux caractéristiques disjointes (n’utilisez surtout pas le mot clé donc). Vous allez me dire, on en a rien à faire, c’est une juste mot … Eh bien non car aujourd’hui certains croient que un logiciel libre payant est illégale ! Et utiliser ce mot va pousser encore plus de gens à croire cela. Si vous répéter à un homme qu’il est rat, il va ce prendre pour un rat, c’est une analogie extrême mais elle correspond. Donc ne faîtes pas croire aux autres que le logiciel libre implique d’être gratuit. Dite qu’il est libre et avant tout libre !

Singleton – C++

Pour ceux qui ont regardé mon C++ Blog et qui ont certaines compétences, ils ont peut être remarquer un design pattern du nom de Singleton. Néanmoins, pas tout le monde à ce savoir et il peut être intéressant de s’intéresser là dessus pour les petits novices qui lisent mon blog. Il faut déjà savoir ce qu’est un design pattern ou un patron de conception. En gros, les informaticiens ont remarqué qu’il y avait des problèmes récurrents en programmation objet (je parle bien du paradigme orienté objet, hein). Ils ont donc défini plusieurs patrons (des exemples en gros) pour aider l’apprenti à résoudre certains de ses problèmes. Il y a plusieurs patrons et ici, nous allons en voir qu’un seul. Néanmoins, je vous invite à voir les autres design pattern pour voir si vous pouvez optimiser (c’est pas vraiment le mot, on devrait dire plutôt « améliorer la structure de votre logiciel », mais ça fait bien) votre logiciel. Notez que ici, on parle plus sur l’architecture de votre logiciel que sur des notions du langage ou des notions sur les algorithmes. En effet, le design pattern n’a pas pour ambition d’être appris par cœur mais de comprendre son fonctionnement pour ensuite l’appliquer à des problèmes de conception que vous pouvez rencontrer dans votre développement. Ensuite, il vous suffira d’une recherche pour connaître sa structure dans votre langage préféré (du moment que celui-ci ce base sur le paradigme orienté objet).

Dans notre cas, nous allons utilisé le C++ car j’ai envie. Nous allons apprendre le fonctionnement du design pattern Singleton, pourquoi on l’utilise, quand on l’utilise et comment on l’utilise. Toute ces questions seront décrites ici pour que vous puissiez appréhender vos problèmes d’architecture plus rapidement (et gagner ainsi en productivité). Cela permettra en outre de maintenir votre projet avec plus de flexibilité (ces design pattern ont été pensé pour).

Alors commençons … C’est quoi un Singleton. C’est un patron qui permet de restreindre l’instanciation d’une classe à un seul objet. Par exemple, vous avez créer une classe Database, mais vous voulez que dans tout votre programme, il n’y est qu’un seul et unique objet Database accessible partout (du moment que la classe est défini). L’intérêt, c’est de pouvoir centraliser les opérations sur le système en un seul objet (on peut notamment parler du problème du lecture/écriture d’un fichier par plusieurs threads dans lequel on doit mettre une politique d’accès restreint pour qu’il n’y est pas d’ambiguïté). La première solution, la plus obvie, c’est de créer l’objet dans la fonction main et de ce dire, bon maintenant, je n’instancie plus ma classe dans un objet. Un premier problème ce pose, c’est que vous devrez toujours spécifier cette objet dans les fonctions qui l’utilisent entant que argument pour quelles puissent y avoir accès sans pour autant faire une nouvelle instanciation. Pour résoudre ce problème, il suffit de définir votre objet comme variable globale, ainsi, elle sera accessible de n’importe où (attention tout de même à cette utilisation). Néanmoins, il y a encore un problème. En effet, il peut arriver qu’un des développeurs oubli la restriction de ne pas instancier une nouvelle fois votre classe … Et là boum, comment qu’on fait ? On lui gueule dessus. Vous savez très bien que cette solution n’est pas bonne. Non, il faut restreindre, dans le fonctionnement de votre classe, l’instanciation multiple.

Les cas où on l’utilise sont diverses. On peut par exemple imaginer un objet représentant un composant matériel de votre ordinateur qui doit être restreint à une unique instanciation pour éviter des conflits de gestions des ressources du composant. Après, les cas varient en fonction de votre projet, là où il faut comprendre, c’est qu’à l’aide du Singleton vous allez pouvoir (vous ?) limiter à n’avoir qu’un objet de votre classe dans tout votre programme et qu’il soit accessible partout !

De base, il faut comprendre que le constructeur de notre classe doit être privé sinon, on pourra instancier votre classe n’importe quand. Mais alors … Comment on pourra instancier notre classe si son constructeur est privé ? On va définir une fonction statique dans votre classe qui va permettre d’instancier votre classe si, et seulement si l’instanciation n’a jamais été fait ! Maintenant, comment savoir si l’instanciation n’a jamais été fait ? On va créer un pointeur statique qui va pointé sur NULL si votre classe n’a jamais été instancié (c’est la valeur par défaut de notre pointeur) et dès qu’on fera appelle à notre méthode statique d’instanciation, notre pointeur pointera alors sur le nouvelle objet qu’on aura créé. Cela correspond donc à une condition qui vérifie si notre pointeur est NULL, si c’est le cas, alors on créer l’objet et on assigne son adresse à notre pointeur, sinon on retourne notre pointeur (car il ne peut que contenir notre objet) ce qui assure l’unique instanciation de notre objet. Et puisqu’on a défini notre méthode d’instanciation (qui fait tout simplement appelle au constructeur, hein) et notre pointeur comme étant statique, ils seront accessible partout dans notre programme tant qu’on a inclue le *.hpp de notre classe.

L’implémentation d’un Singleton ce fait simplement en définissant le constructeur et le destructeur entant que private et on créer une méthode qu’on nomme en général getInstance qui vérifiera si on a déjà créé l’objet ou pas. Dans un des cas, on crée l’objet et on assigne son adresse à notre pointeur statique, dans l’autre on retourne tout simplement le pointeur:

class Singleton
{
    private:
        Singleton();
        ~Singleton();

        static Singleton    *instance;
    public:
        static Singleton* getInstance() {
            if(instance == NULL)
                instance = new Singleton();

            return instance;
        }
        static void kill() {
            if(instance != NULL) {
                delete instance;
                instance = NULL;
            }
        }
};

// Dans le *.cpp
Singleton *Singleton::instance = NULL;

Si vous avez compris les paragraphes ci-dessus, vous pouvez comprendre ce code. Dans ce cas, on opère à chaque fois à une condition dès qu’il s’agira d’instancier notre Singleton avec la méthode getInstance. Comme on prend soin de définir notre pointeur instance comme NULL dans notre *.cpp, on a donc dès le départ aucun objet du type Singleton. Ensuite, dès qu’on fera appelle pour la première fois à notre méthode statique (c’est à dire de cette manière: Singleton *mon_singleton = Singleton::getInstance();), on créera notre objet Singleton à l’aide de l’opérateur new. Ensuite, si on refait appelle à notre méthode getInstance, on nous retournera notre pointeur Singleton::instance. Ainsi, la possibilité de créer un nouvelle objet Singleton est restreint (mais pas impossible, nous allons voir pourquoi). Puisqu’on définit le destructeur de notre classe comme private, il nous faut aussi une fonction membre qui détruise notre objet. C’est à cela que sert la fonction kill(). N’oubliez pas que new permet d’allouer la mémoire, il est donc obligatoire de devoir utiliser delete par la suite, sinon, il y a une fuite de mémoire. Bon, maintenant regardons le code plus en profondeur. On sait pourquoi le constructeur est private: c’est pour empêcher que le développeur instancie une nouvelle fois notre classe. De ce fait, on est dans l’obligation de créer une méthode statique (donc indépendante de l’objet) qui puisse créer une instance de notre classe. Celle ci fait un contrôle sur l’état de notre pointeur et juge si il est nécessaire d’instancier (si le pointeur est égal à NULL) ou pas selon le cas. Mais nous avons aussi notre destructeur qui est private, mais pourquoi ? Pour bloquer la copie. En effet, le compilateur s’amuse parfois à créer des méthodes pour vous. Il y a entre autre le constructeur de copie et l’opérateur =. On peut faire le test (avec g++) en créant une propriété dans notre classe Singleton (ainsi qu’une fonction modifier pour modifier la valeur de notre propriété et une fonction friend pour l’afficher avec l’opérateur <<), en spécifiant le destructeur comme public, et en tapant ceci dans le main:

Singleton*  t = Singleton::getInstance("Dino");
// Le constructeur reçoit comme argument
// la futur valeur de notre propriété

Singleton   e = (*t);
// On fait appelle au constructeur de copie
// sans l'avoir explicitement spécifié dans le code

e.set("Jack");

display((*t));

Singleton::kill();
// On supprime la supposée unique instance de Singleton

display(e);

On peut voir apparaître Hello Dino (ce qui correspond à notre objet pointé par t) et Hello Jack (ce qui correspond à e). Ainsi, ceci prouve qu’il existe 2 objet Singleton (ceci ce prouve aussi en faisant appelle à Singleton::kill() et en affichant e juste après). Notre variable e est donc un objet indépendant de ce que peut pointé notre pointeur t. L’objectif de notre Singleton n’y est pas d’où la nécessité de devoir mettre le destructeur en private. Dans ce cas ci, en spécifiant le destructeur comme private, vous aurez une erreur de compilation car créer un objet suppose que le compilateur soit capable de le supprimer aussi. Or, si le destructeur est private, le compilateur ne peut être capable de détruire le nouvelle objet, donc il y a une erreur dans la compilation. En effet, pour ce qui ce passe avec e, c’est tout simplement l’utilisation de l’opérateur =. Notre compilateur va donc, de lui même, surcharger cette opérateur pour ensuite renvoyer un nouvelle objet Singleton. Néanmoins, la création d’un objet suppose aussi sa destruction, or dans notre première implémentation (en spécifiant le destructeur comme private), il est impossible pour le compilateur de détruire l’objet e, donc il nous retourne une erreur (dans le cas ci-dessus, il nous retourne pas une erreur car on a bien pris soin de spécifier le destructeur comme public). Définir le destructeur comme private est donc essentiel néanmoins cette approche n’est pas spontané. On pourrait très bien définir l’opérateur = et le constructeur de copie comme private (on peut aussi créer de l’objet e de cette façon: Singleton e((*t);), cela reviendrait au même et ce serait plus évident dans la lecture de votre code. Après, c’est à vous de voir. Et bien entendu, puisque le destructeur est private, on est dans l’obligation de spécifier une fonction membre statique kill() pour supprimer notre objet (ceci peut être notamment source d’erreur car on est dans l’obligation de faire appelle à cette fonction mais personne n’est à l’abri d’un oubli).

Mais allons plus loin et bien qu’on est vue les grandes lignes du Singleton, il y a certaines choses qu’on peut optimiser. Il est possible que l’utilité du mot clé new n’y soit pas dans vos applications et que vous voudriez optimiser votre code pour justement ne pas dépendre de cette dernière fonction membre kill() qui est obligatoire. La possibilité est donc de créer l’objet sans le mot clé new. Cela ce passe notamment dans le *.cpp où, au lieu de faire appelle au mot clé new, on définira instance tout simplement comme une variable:

Singleton Singleton::instance;
// Tout comme: int   number;

Bien entendu, le type de instance doit être static Singleton instance (ce n’est plus un pointeur). Néanmoins, cela engendre différent problème et en particulier dans son utilisation car, pour garder la même politique (notamment en définissant le destructeur comme private), la possibilité d’assigner notre objet Singleton à une variable nous est impossible (car l’utilisation du constructeur de copie ou de l’opérateur = nous est interdite). Si vous tapez ce code:

Singleton   t = Singleton::getInstance();

Le compilateur va vous retourner une erreur comme quoi le destructeur est en private. L’instanciation de l’objet nous est donc impossible. Il y aurait un moyen avec les pointeurs mais ce serait des lignes et du temps perdu pour pas grand chose. L’utilisation de notre objet ce fera alors à l’aide de la transparence référentielle. En effet, nous allons utiliser notre objet de cette façon Singleton::getInstance().set("Jack") (pour l’utilisation de la fonction membre set()). Comme vous pouvez le constater, ce n’est pas très pratique, mais l’objet ce fera bien supprimer à la fin sans pour autant utiliser une fonction tierce comme on l’a vue avec kill(). La définition de la classe change quelque peu aussi car on peut vite parvenir à des erreurs de compilation obscures que je ne pourrais détailler ici. Je vous donne donc la définition de la classe qui n’utilise pas les pointeurs:

class Singleton
{
    private:
        Singleton() { }
        ~Singleton() { }

        static Singleton    instance;
    public:
        static Singleton& getInstance() throw() { return instance; }
};

// Dans le *.cpp
Singleton Singleton::instance;

Comme on peut le remarquer, on définit explicitement le contenu du constructeur et du destructeur. Sinon, il y a lieu d’une erreur de compilation (après, je n’ai pas réellement chercher sur ce point si quelqu’un peut m’éclairer). Ensuite, on retourne une référence à notre variable instance d’où notre transparence référentielle. Ensuite, on définit instance dans le *.cpp tout simplement comme on l’aurait fait avec une variable. Donc ce genre d’utilisation, c’est à vous de voir et personnellement, ce genre d’implémentation est un peu bancal à mon goût même si dans notre ancienne implémentation, on était obligé d’utiliser la fonction kill() (peut être parce que l’instanciation ce passe dans le *.cpp et pas dans le main). Néanmoins, cela présente l’avantage de ne pas devoir faire état d’une condition dans la fonction getInstance ce qui peut être un gain d’optimisation. Enfin, c’est à vous de décider. Maintenant, on a toute les données pour créer un unique objet dans notre code. Seulement, même si théoriquement, tout ce passe bien, dans le cadre d’une application avec des threads (pour notre première implémentation), on peut être face à un problème si 2 threads demandent un accès à notre Singleton, donc un accès concurrent (en vérité, le problème ce relate aussi avec l’accès à une fichier par 2 threads, c’est un accès concurrent et il faut gérer cette accès selon une file d’attente).

La solution serait d’utiliser les mutex qui permettent justement de faire face à ce genre de situation. Je vous invite d’ailleurs à lire mon article sur les threads (et les fork) à cette adresse. Mais après cela dépend de votre application et il est conseiller de rester très concentrer quant il s’agit de faire une application multithreadée avec les Singletons/fichiers.


Bon, on a vue les grandes lignes et il n’est pas dans votre intérêt de savoir par cœur l’implémentation d’un Singleton mais juste de savoir son fonctionnement et, au pire, écrire les bouts de codes sur un papier ou un cahier de notes. Par contre, il est essentiel de savoir qu’une tel stratégie dans l’architecture logistique existe car vous serez alors plus apte à créer des programmes et résoudre des problèmes en rapport avec l’architecture de votre logiciel pour des questions de maintenant et de partage de votre code.

Nombre complexe et C++

Mes collègues de ma classe ont fait une blague pour le DM. En effet, il m’ont fait croire qu’il y avait un exercice en plus à rendre. Néanmoins, même si le travail m’a demander plus de temps, j’ai apprécier l’exercice. J’ai donc appliquer cette exercice dans un programme informatique. Il s’agit donc ici de faire quoi ? Eh bien d’implémenter la notion de complexe en C++ grâce aux classes et à la surcharge des opérateurs. Notez tout de même que la STL propose déjà une classe complex. Mais là n’est pas le but d’être laxiste, il faut bien maîtriser les notions de surcharge d’opérateurs et du mot clé friend. On doit être capable, non seulement, de faire des opérations arithmétiques sur les complexes mais permettre à l’utilisateur d’accéder aux propriétés de notre complexe (c’est là où intervient le mot clé friend).

Il faut bien comprendre que cette exercice n’a pas pour vocation de vous apprendre des maths (si c’est ce que vous chercher, vous prenez votre livre et vous faîtes les exercices). Il permet pas non plus d’apprendre des notions complexes d’algorithmes, il sert juste à bien utiliser les notions que nous propose le langage C++ et ce n’est pas à négliger car un bon programmeur, c’est quelqu’un qui maîtrise avant tout son langage. Donc les notions que je vais expliquer ici ne tiennent pas à être difficile et où il ne vous faudrait pas un papier et un crayon à côté pour comprendre, il suffit de lire et surtout (surtout !) d’appliquer le plus possible. De plus, il faut porter une réflexion non pas sur le fonctionnement de notre classe (par exemple, comment additionner 2 complexes) mais sur l’aperçu extérieur de notre classe pour que son utilisation soit spontanée. Comment faire ? Un complexe s’utilise dans les mathématiques, il est donc primordial de définir tout les opérateurs arithmétiques possibles pour notre classe Complexe. On pourrait aussi ce rapprocher de l’écriture mathématique notamment avec Im() et Re(). Il arrive aussi que l’utilisateur préfère utiliser les coordonnées polaires, il nous faut donc une fonction capable de passer d’une coordonnée polaire à une coordonnée cartésienne. Enfin, il peut être intéressant de proposer des outils de conversions d’un objet Complexe à une structure comme Coordinate (struct Coordinate { float x, y; }; par exemple).

Friend

Pour bien comprendre ce qui va suivre, il faut comprendre la notion de friend dans une classe. Ce mot clé permet d’offrir à une autre classe ou à une fonction des privilèges d’accès en rapport avec notre classe de base. Comme vous le savez bien, les propriétés d’une classe doivent être privées pour respecter l’encapsulation. Néanmoins, il est parfois nécessaire d’accéder aux propriétés d’une classe. Une première méthode serait les accesseurs (getReal) qui sont des fonctions membre de la classe. Néanmoins, dans notre cas des complexes, on voudrait ce rapprocher le plus possible de l’écriture mathématique:


Cette écriture convient à une fonction libre dont le prototype serait float Re(const Complexe &a) pour la partie réel et c’est la même chose pour Im(). Néanmoins, comment accéder à la partie réel ou à la partie imaginaire de notre complexe qui sont des propriétés privées ? C’est à l’aide du mot clé friend. Dans la définition de notre classe, nous allons spécifier que la fonction Re() et Im() sont des fonctions amies de notre classe Complexe pour qu’ils aient accès aux propriétés mon_complexe.real et mon_complexe.imaginary. Il faut donc définir notre classe Complexe comme ceci:

class Complexe
{
    friend float Re(const Complexe &a);
    friend float Im(const Complexe &a);

    private:
        float   real;
        float   imaginary;
};

L’accès aux propriétés de la classe Complexe sera ainsi toléré pour les fonctions Re() et Im(). Il faut tout de même préciser certaines spécificités sur le mot clé friend. Il faut savoir que les fonctions/classes friend ne brise pas l’encapsulation. Il s’avère parfois plus judicieux d’utiliser le mot clé friend que les accesseurs. Notre utilité ici ce trouve comme une alternative syntaxique. De plus, en utilisant le mot clé const dans notre prototype, on oblige la fonction de n’être que en lecture seul sur les propriétés de notre classe. La modification de la partie réel de notre complexe indépendamment de la partie imaginaire n’est pas possible dans notre cas. Ensuite, la particularité du mot clé friend, c’est que les privilèges qui y sont associés ne sont pas hérité, transitive et pas réciproque ce qui assure encore une fois l’encapsulation. Mais dans notre cas, cela ne nous concerne pas vraiment. Les fonctions Re() et Im() ont juste à retourner respectivement a.real et a.imaginary. Maintenant, on peut créer un objet Complexe et respecter la notation mathématique pour la partie réel ou imaginaire de notre complexe.

Opérateur

On vous a sûrement appris à définir les opérateurs d’une classe comme étant membre de cette dite-classe. C’est à dire, que vous avez surcharger les opérateurs comme + directement dans une fonction membre de la classe. C’est bien ça marche, mais pas tout le temps. Pour ce faire, il faut comprendre un peu comment ça fonctionne. Affecter l’opérateur + à une fonction membre de la classe en définissant explicitement (et c’est normal) le type à rentrer en paramètre vous limite dans ce que vous avez écrit. Si on écrit ce type de fonction: Complexe Complexe::operator+(const Complexe &a) cela vous limitera qu’à additionner des complexes avec des complexes. Et oui, car le seul type accepter pour ce qui est de l’addition dans votre classe est le type objet Complexe. Mais vous me direz, alors on surcharge une nouvelle fois l’opérateur + pour int, float et whatever ? Oui, c’est une solution, mais surcharger l’opérateur + plusieurs fois pour une question de type, c’est une baisse de votre productivité et il y a un moyen plus rapide pour le faire: le transtypage. Votre compilateur (du moins g++) est intelligent et il sait ce qu’il faut faire quand on lui donne les moyens de le faire. Si on regarde de plus près la surcharge de l’opérateur + avec notre classe Complexe, on obtient ça:

Complexe a = Complexe(1, 1) + Complexe(2, 2);
// On a défini le constructeur de Complexe
Complexe a = Complexe(1, 1).operator+(Complexe(2, 2));
// Cette ligne équivaut à la première

Pour l’instant, tout va bien, on ajoute un Complexe à un autre Complexe ce qui correspond à notre prototype Complexe Complexe::operator+(const Complexe &a), on aura créer un constructeur acceptant 2 paramètres du type float pour définir respectivement les propriétés real et imaginaru de notre objet. Puisqu’on a surcharger l’opérateur + dans une fonction membre de notre classe, il est logique de voir apparaitre Complexe(1, 1).operator+(Complexe(2, 2)). C’est comme une fonction membre normal qu’on appellerait de la même façon Mon_Objet.ma_methode(). Néanmoins, si nous voulions par exemple tout simplement ajouter 5 ? Comment nous pourrions faire ? C’est une valeur de type int et à aucun moment on a surcharger l’opérateur + pour une valeur de type int. Comme il est expliqué plus haut, on pourrait le faire, mais pour float, double, etc ? On va utiliser le transtypage est donner le moyen à notre compilateur de convertir une valeur int en un Complexe, ainsi l’opération pourra ce faire et on aura pas à surcharger encore une fois l’opérateur +:

Complexe::Complexe(const float &a) : real(a), imaginary(0) { }
Complexe a = Complexe(1, 1) + 5;
Complexe a = Complexe(1, 1).operator+(Complexe(5));

Ici, on a donné le moyen au compilateur de convertir un float en un complexe en définissant le constructeur Complexe(const float &a). Mais nous ne voulions pas déjà utiliser un int ? On peut très bien le faire, le problème, c’est que l’utilisation des complexes demande souvent des nombre avec des virgules. Nous pourrions faire un constructeur pour les int, le problème, c’est que là aussi le compilateur est intelligent et peut passer d’un int à un float implicitement. Sauf que passer d’un float à un int équivaut à une perte de donnée car imaginons que nous définissons qu’un constructeur pour les int pour notre classe Complexe, si on fait ce genre d’opération Complexe a = Complexe(1, 1) + 5.6. Au moment du transtypage (lors de l’appelle de notre constructeur), 5.6 va ce transformer 5 puisque je le rappelle que les int sont des nombres entiers. Le choix d’un float (ou d’un double) est donc justifié et la conversion implicite du int au float ce fera sans perte. Au final, notre calcul ressemblera à ça:

Complexe a = Complexe(1, 1).operator+(Complexe(float(5)));

On a donné le moyen à notre compilateur de traiter une valeur objet Complexe avec un int ou un float juste en définissant un nouveau constructeur. C’est un constructeur de transtypage. Il est important surtout dans notre cas pour une utilisation, qui, tournée vers l’utilisateur, soit spontanée. Mais cela ne résout pas totalement le problème car on ne peut pas mettre l’élément de gauche de notre opération entant que argument à notre fonction membre. C’est à dire que l’appelant de l’opérateur + ne peut pas être insérer comme argument à la méthode surchargeant notre opérateurs. Prenons l’exemple de notre 5 encore une fois mais cette fois, on va faire:

Complexe a = 5 + Complexe(1, 1);
Complexe a = 5.operator+(Complexe(1, 1);

Si on suit toujours la même logique, on obtiendra ce code. Néanmoins, cela va générer une erreur. Pourquoi ? Car il est aucunement défini dans le type int l’opérateur + acceptant comme argument un Complexe. Vous ne pouvez donc pas écrire ce code sous faute d’une erreur car personne n’a surcharger l’opérateur + pour int en spécifiant un argument de type objet Complexe. Il y a néanmoins, plusieurs moyens mis à notre disposition. L’un d’eux serait de créer un opérateur de transtypage (et pas un constructeur) dans notre classe Complexe pour que le compilateur fasse une conversion implicite de l’objet Complexe au type int. Mais vous vous en doutez, il y a une perte de données encore une fois, surtout qu’ici, on voudra un Complexe entant que résultat. Puis votre compilateur n’est pas super intelligent et il ne pense pas faire ceci: Complexe a = Complexe(5).operator+(Complexe(1, 1));. Ce qui est normal. Alors comment faut il faire ? Il y a une autre solution, c’est de surcharger l’opérateur + par exemple mais dans une fonction libre qui prendra en compte alors l’élément de gauche de l’addition (ce que nous voulions faire) et l’élément de droite. Cette fonction aura la particularité de renvoyer un Complexe ce qui assurera son unicité (et ce qui permettra au compilateur de choisir cette fonction quand nous voudrions avoir comme résultat un complexe). Ensuite, le compilateur grâce aux outils qu’on lui propose (notamment le constructeur de transtypage) pourra faire l’addition en faisant une conversion implicite de 5 en un complexe. Mais comment faire là aussi. Eh bien, il y a encore plusieurs méthodes. Une méthode général et une autre qui ce destine à notre implémentation. La méthode général serait de définir ces fonctions libres comme friend de notre classe pour quelles puissent avoir accès aux propriétés de notre classe (on vous conseillera toujours cette méthode dans ce cas). Cela ressemblerait à ça:

class Complexe
{
    friend Complexe operator+(const Complexe &a, const Complexe &b);

    private:
        float   real;
        float   imaginary;
};

Sauf que nous, nous avons déjà spécifier des fonctions qui ont accès aux propriétés de notre classe Complexe et en plus, ces fonctions les retournent. On a donc accès (seulement en lecture, hein) aux propriétés de notre classe par le biais des fonctions Re() et Im(). Il n’est donc pas nécessaire de surcharger nos opérateurs par le biais de fonctions friend à notre classe Complexe et de faire appelle à Re() et à Im() pour connaître les propriétés des objets qu’on manipulera. Le prototype de nos fonctions ce fera donc hors de notre classe (ces fonctions dépendront quand même de notre classe car les arguments et l’objet retour sont tous du type objet Complexe):

class Complexe
{
    // ...
};

Complexe operator+(const Complexe &a, const Complexe &b);

L’addition ne ce fera plus de la même manière maintenant. Puisqu’on a défini la surcharge de notre opérateur + dans une fonction libre, on a la possibilité de prendre en compte non seulement l’objet de droite mais aussi celui de gauche. Puisqu’on spécifie explicitement le type de retour étant Complexe, dès qu’il s’agira d’obtenir un résultat pour l’assigner à un complexe, le compilateur saura qu’il faut utiliser notre fonction (et oui, il est intelligent). En plus, on a créer un constructeur de transtypage ce qui nous donne la possibilité d’additionner un int (float, double, whatever) à nos complexes car, là aussi, notre compilateur est intelligent est va faire les conversions nécessaires pour que tout ce passe bien. Quand on ajoutera un complexe et un nombre par exemple et que nous voudrions obtenir en retour un nombre complexe, on fera appelle à cette fonction de cette façon:

Complexe a = 5 + Complexe(1, 1);
Complexe a = operator+(Complexe(5), Complexe(1, 1));

La particularité chez nous, c’est que puisqu’on a offert un accès en lecture seul aux propriétés de notre classe (avec les fonctions Re() et Im()), il n’est pas nécessaire de définir nos fonctions surchargeant les opérateurs comme amies de notre classe puisqu’il suffit de faire appelle aux dites-fonctions pour créer un nouveau complexe qui résultera de l’addition de 2 complexes (cf. votre cours de maths). On peut s’amuser à ajouter des nouvelles fonctions comme obtenir le conjugué d’un complexe, le module, etc … Mais ça, c’est dans vos corde (d’ailleurs vous pouvez utiliser les fonctions friend dans ce cas là aussi pour faire Module(z) par exemple même si, comme pour les opérateurs, ce n’est pas indéniable). Mais il s’agit de proposer d’autres outils à notre programmeur. L’un d’eux est de convertir une coordonnée polaire en une coordonné cartésienne. Pour ce faire, je vous renvoie à votre cours de maths. On pourrait penser créer un autre constructeur pour faire la conversion directement au lieu de passer par une fonction. Néanmoins, il y a ambiguïté entre notre constructeur de base Complexe(const float &r, const float &i) et le supposé constructeur pour les coordonnées polaires Complexe(const float &m, const float &t). Même si, sur l’un, il s’agit de donner un angle (en radian ou en degré) et sur l’autre un module, tout les deux (les arguments) sont considéré comme des float. Ainsi, l’utilisation d’une fonction libre et justifié:

Complexe Polar(const float &m, const float &t);

Pour une meilleur organisation, on peut très bien utilisé les namespace mais bon, ce serait vous expliquer une nouvelle chose qui n’a pas réellement son intérêt ici. Non, maintenant, il faut parler des opérateurs de transtypage. En effet, il peut être nécessaire que votre complexe devienne un autre objet ou une autre structure. C’est là où ils interviennent. Normalement, le constructeur (à ce que j’ai pu lire mais je n’ai pas de confirmation) de transtypage est plus rapide que l’opérateur de transtypage. Néanmoins, on peut pas ce permettre de dire à notre programmeur: voilà, on a une super classe sauf que pour la convertir en un autre objet, tu dois définir dans ton objet un constructeur de transtypage. Il faut toujours penser extérieur. Il nous faut donc créer un opérateur de transtypage mais comment ça marche ? C’est très simple, la définition d’une fonction pour l’opérateur de transtypage est similaire que pour la surcharge d’un opérateur. Mais c’est pas exactement ça:

Complexe::operator Coordinate() const;

Il y a le mot clé operator et le nom de notre fonction n’est autre que le type de retour (ici Coordinate pour struct Coordinate { float x, y; };). Comme vous le voyez, il n’y a pas besoin de spécifier le type de retour car il équivaut au nom de notre fonction (pratique l’ami). On notera aussi qu’il n’y a pas la présence d’arguments car, en effet, on va agir sur l’appelant de cette fonction, c’est à dire this. Bien entendu, ceci est dans le cas où cet opérateur de transtypage est défini par le biais d’une fonction membre de notre classe. Dans le cas d’une fonction libre, il faudra spécifier un argument du type Complexe. Pour ceux qui ne le savent pas, this est un pointeur par défaut qui désigne l’objet en lui-même. C’est un pointeur non modifiable, ce qui est logique (merci la FAQ C++). Dans cette fonction, il suffira de créer un objet type Coordinate dont x vaut Re((*this)) (notez le *, en effet, this est un pointeur) et y vaut Im((*this)). Notez encore une fois que ici, j’utilise les fonctions friend Re() et Im(). Cet opérateur de transtypage peut donc être défini par le biais d’une fonction libre.


Bon, vous savez les grandes lignes et vous pouvez déjà vous appliquer à faire une classe Complexe. Il faut savoir que les notions du type surcharge d’opérateur dans des fonctions libres est un cas à ne pas appliquer à toutes vos classes. En effet, ici, c’était nécessaire pour pouvoir utiliser des types natifs (comme int ou float) sans surcharger encore une fois selon le type. Néanmoins, vous ferez sûrement des classes qui n’auront pas nécessairement besoin qu’on leurs ajoute des nombres. On peut par exemple restreindre le programmeur à n’additionner que les objets d’une classe entre eux (sous faute d’une erreur de compilation sinon). J’aborde ce problème car, pour les sources que j’ai observé (dans les cours débutants), rien ne soulever le problème, et quant on pouvez (enfin !) voir des fonctions libres, on nous expliquer par pourquoi. En espérant que ce petit article à fait la lumière sur les opérateurs. Il y aura aussi peut être des update pour appliquer cette classe à un problème mathématique !

Epitech, me voilà !

Article rapide pour dire que je viens de recevoir mon dossier d’inscription à l’Epitech ! Bon je vous épargne mes motivations pour cette école, j’évite aussi de faire de la pub mais voilà où j’en suis, mon seul objectif maintenant, c’est de passer mon Bac. Et je compte bien le passer.

Optimisation et PHP

L’optimisation PHP par certains développeurs n’est pas justifié. En effet, l’approche qu’on certaines personnes sur l’optimisation et langage qu’ils utilisent n’est pas réellement approprié. J’ai été un peu choquer par la présence d’article complet et bien fait sur l’optimisation de vos scripts PHP mais que sur le plan du langage. C’est à dire que tout au long de cette article, on apprends surtout l’utilisation de notions obscur du langage PHP qui enlèverait 1 ou 2 nano-seconde pour l’exécution de votre script. Est ce là vraiment l’optimisation ? Surtout quand on observe une requête SQL dans une boucle … Ceci traire de l’ordre des micro-optimisations. Mais pour autant, faut il s’attarder à ce genre de détail pour la publication de nos scripts PHP ? Doit on passer un temps certain à remplace echo "Mon texte: $number"; par echo 'Mon texte: $number';. Ceci est il vraiment justifié ? L’optimisation de vos scripts PHP ne doit pas passer par là. Du moins, cela ne doit pas être priorité (car au final, on s’en contre fiche et cela dénote plus de la mauvaise qualité du langage que l’acquisition de quelques nano-secondes). Cette article va donc vous expliquer là il faut optimiser votre code.

La première optimisation que l’on doit faire, c’est au niveau algorithmique. Par exemple, il est parfois plus judicieux d’utiliser le tri par insertion ou par sélection que le tri rapide pour une liste courte. Il s’agit non seulement de savoir le complexité de l’algorithme que l’on utilise, mais aussi de connaître ou de rechercher d’autres algorithmes qui peuvent être plus rapide suivant le cas. Souvent, certains utiliseront un algorithme très rapide dans certains cas ce qui peut ne pas être adapter à leurs situations et dégrader la vitesse de leurs programmes. Mais cela ne concerne pas seulement vos algorithmes mais aussi dans vos instructions (comme en Python avec list.append() et list.insert()) où il faut ce renseigner sur leurs complexités (et leurs utilisations) et faire les opérations nécessaires pour savoir la complexité final de votre algorithme si elle intéressante ou pas. Après, il faut épuisé toutes les possibilités qui vous sont offertes. Rien ne doit vous échapper et vous devez être sûr qu’il n’existe aucuns autres moyens plus rapide pour résoudre votre problème. Il ne s’agit donc pas ici de simplement appliquer ce que nous X ou Y en disant que c’est plus rapide que de faire ceci, il s’agit de faire une réflexion non seulement dans ce qu’on utilise mais aussi dans ce qu’on devrait utilisé.

L’optimisation, notamment dans des langages qu’on compile, ce n’est pas aller au plus bas niveau. J’entends encore dire qu’un programme en Assembleur est plus rapide en C. C’est une erreur car aujourd’hui, les compilateurs créer un programme autant rapide qu’un autre programme écrit en assembleur (tant que le programmeur sait très bien programmer en assembleur). De plus, cela allonge votre temps de productivité (car il est plus long de faire un programme en assembleur qu’en C) et vous pénalise plus qu’autre chose. Le gain de rapidité n’y est pas dans ce genre de méthode. Néanmoins, il convient parfois d’utiliser localement un langage bas niveau pour des problèmes d’architectures (comme par exemple un logiciel qui travaillerait sur plusieurs machines en parallèles ou dans les systèmes embarqués). Mais ce genre de cas est plus exceptionnel, l’utilisation d’une telle technique ce restreint donc à certains cas limites.

Il s’agit ensuite de porter une véritable réflexion dans la structure de votre programme. Écrire du code astucieux qui vous fait gagner une nano-seconde n’est rien en comparaison d’un code très bien structuré, lisible et aisé à maintenir (c’est d’ailleurs là où on repère la qualité d’un programmeur). Le véritable problème, c’est que cette réflexion ce porte dès le départ du projet et certains programmeurs la sautent immédiatement ce qui ce termine par établir une nouvelle structure et tout recommencer (et là, c’est une véritable perte de temps). Il faut donc définir des structures de données et connaître des algorithmes qu’on appliquera après qu’on est justifié si ils nous sont appropriés.

Après, pour ce qui est de C++, on peut faire confiance à notre compilateur qui peut faire des optimisations dont, nous, nous ne penserions pas dès la première approche. Notamment en utilisant des mots clés comme const. Mais il faut le spécifier explicitement à votre compilateur dans votre code. Mais ça, c’est de l’ordre de votre langage dont vous devez en connaître tout les secrets (et il ne s’agit pas de connaître les endroits obscurs du dit-langage mais de voir tout ce qu’on peut en tirer en connaissance des mot clés).

Ce que je veux faire comprendre ici, c’est que l’optimisation du type echo cité au début de cette article ne sert à rien si vous ne procéder pas aux optimisations citées ci-dessus. Ensuite, un autre point mais qui est de l’ordre de la productivité (mais aussi de l’organisation), c’est d’optimiser après que vous ayez fait quelque chose de fonctionnel. L’optimisation pendant la création est plus un frein (car vous reviendrez sur votre code, vous recommencerez, vous repartirez pour ensuite revenir sur votre code, etc …) qu’un gain. Alors faîtes quelque chose qui fonctionne avant de vous posez la question si votre script et rapide ou non.

Liste chaînée – C

Voilà un bon article qui fait l’introduction à ce qu’on nomme les listes chaînées. Par exemple, si nous prenons PHP, il est aisé de faire un tableau, de lui ajouter/supprimer/modifer des éléments, néanmoins, ce sympathique type est bien plus complexe en C. Si vous avez tenter de faire du C, l’une des règles élémentaires du C est de définir la taille d’un tableau directement pendant l’initialisation et sa taille reste fixe (d’ailleurs, on peut encore voir dans certains code la taille d’un tableau définie dans une macro #define ce qui accentue la caractéristique de constante). Mais en sachant que le PHP et fait à partir du langage C, d’où nous vient ce type array() dans PHP dont la taille peut être variable ? Ce n’est pas de la magie mais de la logique et c’est ce qu’on appelle les listes chaînées (l’implémentation de Python en C utilise aussi la notion de liste chaînées).

Une liste chaînée peut ce définir comme ceci: Une liste chaînée désigne en informatique une structure de données représentant une collection ordonnée et de taille arbitraire d’éléments de même type. C’est un peu lourd mais le concept est là, il faut toujours ce positionner sur la notion de gestion de la mémoire qui est omniprésente en C. La notion de liste chaînée s’expliquer par plusieurs valeurs auxquelles on leurs associe un pointeur. Ce pointeur ne va pas pointer dans le vide mais sur la prochaine valeur de la liste, c’est ainsi qu’on peut créer une liste d’ordonnée:

[A] -> [B] -> [C] -> ... -> [H]

C’est là où intervient le mot « chaînée » car en effet, ce sont les pointeurs qui représentent les chaînes de notre liste. Mais pourquoi nous ne pouvons pas tout simplement appliquer cette logique pour les tableaux en C. Quant il s’agit d’allouer la mémoire, on vous retourne l’emplacement où vous pouvez mettre votre valeur. Néanmoins, cette emplacement n’est pas déterminer par le développeur. Ainsi, quant il s’agit d’allouer la mémoire pour un tableau, on vous donne un bloc (qui peut contenir 10 valeurs par exemple). La particularité de ce bloc, c’est qu’il est ordonné et continu mais de taille fixe dans la mémoire, c’est à dire que si on vous retourne la case 20154, la prochaine valeur de votre tableau sera dans la case 20155 et ainsi de suite. Néanmoins, il ce pose un problème car après votre tableau, il peut y avoir d’autres valeurs de d’autres programmes d’où le fait qu’il nous est impossible en C d’ajouter une nouvelle valeur à un tableau tel qu’on le connait en PHP. On a donc bien un bloc d’une taille limitée par des bornes qu’on ne peut dépasser. Et c’est la principale différence avec les listes chaînées. Quand on créer un éléments dans une liste chaîné, c’est comme si on créait une variable (associé à un pointeur). On vous retourne la case 5646 pour l’élément A. Par contre, quant il s’agira de mettre l’élément B dans la liste, on créer notre variable B (comme une allocation de la mémoire dynamique normale) et on nous retournera comme case 2468. Cette valeur devra être insérer entant que pointeur dans notre élément A. Mais ce qu’on dénote c’est que la liste chaînée n’est pas continu dans la mémoire (), néanmoins, elle est ordonnée par la présence d’un pointeur qui pointe sur l’élément suivant. D’une façon général, on pourrait dire que la liste chaînée (sa structure) ce compose de l’élément ainsi que d’un pointeur vers l’élément suivant de la liste. Il faut préciser aussi que le dernier élément d’une liste chaînée pointe vers la valeur NULL. Ce repère nous permettra de faire plusieurs actions sur la liste (comme connaître la taille d’une liste).

Il faut tout de même savoir qu’il est possible de modifier la taille d’un tableau en C, ajouter une valeur, supprimer. Néanmoins, les opérations nécessaires pour utiliser un tableau C comme en PHP sont fastidieuse car il s’agit de relouer la mémoire, y mettre le tableau avec/sans la nouvelle valeur et supprimer l’ancien tableau. C’est long, cela demande qu’on écrive dans la mémoire des blocs la liste chaînée propose un traitement rapide quant il s’agit de l’insertion ou de la suppression d’élément. Mais le point sur la rapidité sera vue un peu plus loin.

L’avantage est donc que la liste chaînée peut contenir autant de valeur que la mémoire peut contenir, la taille n’est plus fixe car concrètement chaque valeur de notre liste chaînée est à un endroit différent dans la mémoire de façon discontinue. Maintenant, on peut commencer à aborder le plan technique de la chose en C. On va donc créer une structure à partir du mot struct. Nous aborderons ici une représentation simple, il existe d’autres structures comme la liste doublement chaînée mais il n’est pas question ici de celle-ci et nous porterons notre réflexion sur la liste chaînée.

struct element
{
    int value;
    struct element *next;
};
typedef struct element element;
typedef struct element* list;

Voici donc la structure que nous utiliserons dans cet article. Le type de value n’est pas important, cela peut être n’importe quoi mais ici, j’utilise le type int pour une meilleur compréhension. Nous allons donc reprendre notre explication pour calculer la taille d’une liste chaînée (car sa taille est variable à la différence d’un tableau). Il faut savoir que calculer la taille d’un tableau en C est plus rapide puisque, comme il l’est dit, la taille d’un tableau est constante (avec le #define notamment) et toujours connue donc il n’y a pas d’opération pour le savoir. On peut opérer de façon récursive pour calculer la taille d’une liste chaînée. En effet, si notre liste équivaut à NULL, alors la taille est 0, sinon, sa taille équivaut à 1 + la taille de la liste pointée. Prenons une liste a qui ce compose de la valeur (en référence à la structure que l’on vient de faire en C) et , le pointeur. On déduit que la taille d’une liste équivaut à:

Au final, tant que l’on atteint pas le pointeur NULL (la fin de notre liste), on incrémente une variable t et si on atteint la valeur NULL, il nous suffit de retourner t. On remarque ici qu’on fait un parcours complet de la liste, on fait un comptage tout simplement mais celui-ci à le défaut d’avoir une complexité de avec n la taille de notre liste. On peut dire ici que le tableau est gagnant, néanmoins, il existe des méthodes pour être informer de la taille d’une liste sans pour autant devoir parcourir la liste en entier. Il s’agirait, selon vos objectifs, de créer une variable globale qu’on incrémenterait dès qu’il s’agira d’ajouter un nouvelle élément à notre liste par exemple. Il existe bien d’autres moyens pour obtenir la taille d’une liste mais celle-ci est générale et elle ne dépend que de notre structure de base (pour notre deuxième exemple, j’ai mentionner une variable globale ce qui peut rendre votre code mauvais). Après, c’est en fonction de vos objectifs que vous devrez adopter telle ou telle politique. Enfin, en code C, cela donne cette fonction:

int len(list a) {
    if(a == NULL)
        return 0;

    return len(a->next) + 1;
}

Ce code correspond à notre attente, à savoir une structure conditionnelle ainsi que l’application d’une méthode récursive. Ici, je ne mentionne pas la variable t car il n’est pas nécessaire de le faire. Par contre, j’incrémente bien la valeur de retour. Néanmoins, savoir la taille pour une liste vide n’est pas intéressant, c’est pour cela que nous allons voir comment ajouter un élément à notre liste. On peut avoir recours à 2 méthodes pour ajouter un élément à une liste. Nous pouvons ajouter un l’élément à la fin de notre liste (cela équivaut à un push ou, en Python, à list.append(ma_valeur)) ou bien ajouter un élément au début de notre liste. Pour ce qui est d’ajouter un élément en début de liste, c’est très simple. Il faut d’abord allouer la mémoire pour un nouvelle élément. Comme dans le cas d’une liste chaînée, on fait une abstraction de la position de notre nouvelle élément dans notre mémoire, il nous suffit d’attribuer à la valeur la position dans la mémoire de notre ancienne liste. Ensuite, notre fonction retournera l’adresse de notre nouvelle élément pour informer que c’est notre nouveau premier élément:

Comme on le remarque ici, nous avons une variable a qui est une liste, on alloue la mémoire pour créer e en lui assignant par exemple la valeur 5, la prochaine valeur de e va devenir le premier élément de notre liste a (pour ranger e comme étant notre premier élément de notre nouvelle liste) et ensuite, la variable a nous dirigera vers e (comme étant le premier élément de notre liste). Ceci donne en C:

list push_before(list a, int value) {
    element* e = malloc(sizeof(element));
    e->value = value;
    e->next = a;

    return e;
}

On notera ici que la complexité quant il s’agit d’ajouter un élément en début de notre liste est de , il ne s’agit donc pas de traverser entièrement notre liste mais de juste faire une modification de nos références pour ajouter un nouvelle élément et c’est là la force de la liste chaînée. Cependant, on peut chercher à vouloir ajouter un élément à la fin de notre liste. Cela est un peu plus complexe que le code précédent (mais tout à fait abordable, rien de bien méchant). Pour ce faire, il faut ce référer à une caractéristique du dernier élément d’une liste, à savoir que équivaut à NULL. Et dans ce cas ci, il faudra parcourir notre liste à la recherche de notre dernier élément, c’est à dire atteindre tant que celui ci n’est pas NULL (note l’utilisation du mot clé tant que présent dans le langage algorithmique qui nous laisse donc penser à une itération). On peut donc expliquer ces instructions par cela:


C’est un peu plus compliqué comme vous le voyez. Il s’agit d’abord de créer un nouvelle élément e à l’aide de malloc, ensuite, si la liste est vide, il suffit de retourner e, sinon on parcourt la liste jusqu’à atteindre la référence à NULL (caractéristique du dernier élément d’une liste chaînée), on assigne donc la référence à notre dernier élément de notre liste à la variable t pour lui assigner dans son attribue next la référence de notre nouvelle élément e. Il faut remarque qu’on ne change pas, dans ce cas, la référence a (puisque le premier élément de notre liste ne change pas). Il faut noter aussi le fonctionnement de la fonction browse() qui est une fonction qui peut être récursive (ce qui est le cas avec le genre de représentation ci-dessus) mais aussi itérative. Dans notre cas, nous utiliserons la méthode itérative (avec le tant que) pour ne pas trop alourdir notre code. Néanmoins, on peut tout de même implémenter cette fonction qui renvoie le dernier élément d’une liste. Si vous suivez toujours, on peut passer à l’implémentation en C qui est:

list push_back(list a, int value) {
    element* e = malloc(sizeof(element));
    e->value = value;
    e->next = NULL;

    if(a == NULL)
        return e;
    else {
        element* t = a;

        while(t->next != NULL)
            t = t->next;

        t->next = e;
        return a;
    }
}

Si on revient sur ce code, on repère quant il s’agit d’allouer la mémoire (comme pour notre fonction push_before), ensuite nous avons une structure conditionnel pour vérifier si a est NULL ou pas. Dans le cas où a est NULL, on retourne le nouvelle élément e, sinon on utilise une implémentation itérative de la fonction browse() pour parvenir au dernier élément qu’on sauvegarde dans la variable t, et enfin on assigne comme référence à notre nouvelle élément e, on termine donc par retourner a. On remarque que l’ajout d’une valeur en fin d’une liste contient un parcours de la dite-liste ce qui nous amène à une complexité de ce qui n’est pas négligeable. Néanmoins, on peut encore donner l’avantage aux listes chaînées par rapport aux tableaux en inversant la liste donc en considérant que le début est la fin et vise-versa, cette représentation est à faire dans votre tête et pas réellement dans le code. En effet, ainsi, pour ajouter un élément à la fin, vous utiliserez notre première fonction pour obtenir une complexité et concernant la taille, que la liste soit dans un sens ou dans l’autre, cela ne change rien. Mais qui dit allocation dynamique dit aussi qu’on l’on doit vider ce qu’on a alloué ! Il faut donc bien vérifier que notre liste devienne vide à la fin, pour ce faire, c’est très simple et cela rentre plus dans le domaine technique que algorithmique. Il nous suffira de libérer la mémoire à l’aide de free tant que notre liste n’équivaut pas à NULL. Noter l’utilisation du mot clé tant que ce qui nous donne déjà une idée de comment nous allons implémenter cela.

void free_list(list a) {
    while(a != NULL) {
        element* t = a;
        a = a->next;
        free(t);
    }
}

Ici, on parcours notre liste, et à chaque élément, on utilise la fonction free() pour libérer la mémoire. La variable t est une variable de temporisation car elle permet juste de garder une valeur pendant un temps déterminer pour ensuite faire une action sur celle-ci (la fonction free() notamment) quand toute les conditions nécessaires sont acquises (en outre, le passage à l’élément suivant de notre liste avec a = a->next). Vous pouvez désormais tester votre code en ajoutant des éléments et en notant la taille de votre liste pour ensuite la vider ! Concernant l’affichage de votre liste, ce n’est pas très compliqué, tant qu’une variable t n’équivaut pas à NULL, on affiche et on assigne à t la référence pour passer à l’élément suivant. Bien entendu, il faut initialiser t comme faisant référence à notre liste a ce qui donne en C:

void print(list a) {
    element* t = a;

    while(t != NULL) {
        printf("%d ", t->value);
        t = t->next;
    }

    printf("\n");
}

Il peut arriver qu’on veuille accéder à un élément distinct de notre liste. Dans le cas d’un tableau, il suffit de spécifier l’index, par exemple mon_tableau[4] renvoie la cinquième valeur de mon tableau (n’oubliez pas que l’index commence par 0). Mais dans une liste, comment on fait ? Eh bien, là aussi dans ce cas, le tableau à une complexité plus intéressante (puisqu’elle est de ) que celle de la liste qui est de avec n notre indice (c’est à dire, l’index + 1). Ainsi, si vous voulez accéder au premier élément de votre liste, vous aurez une complexité de , pour le deuxième , etc … Il s’agit donc de parcourir la liste jusqu’au n-ième élément. On peut faire l’analogie avec la fonction browse() vue plus haut. Sauf qu’on ne cherchera pas à atteindre le pointeur vers NULL (c’est à dire la fin de notre liste) mais à s’arrêter dès qu’on est fait n fois l’instruction t = t->next avec t la référence de notre liste à l’initialisation. Ce qui équivaut donc à:


Il faut d’abord savoir si on demande bien un élément qui est dans notre liste. C’est à dire l’indice d’un élément qui soit strictement inférieur à la taille de notre liste. On peut aussi arrêter les instructions quand , c’est à dire quand on a atteint la fin de la liste. Ensuite, si i équivaut à n (à l’indice que l’on recherche), alors on retourne a. Sinon on passe à l’élément suivant avec (noter le [n] qui refait appelle à notre structure conditionnelle, c’est la récursivité) après avoir incrémenter i de 1. Ici, nous choisirons d’écrire la fonction par la méthode itérative car l’intérêt de la récursivité n’y est pas et qu’on peut très bien s’en sortir avec une boucle for.

list element_i(list a, int n) {
    int i = 0;

    while(i < n && a != NULL) {
        a = a->next;
        ++i;
    }

    if(a == NULL)
        return NULL;
    else
        return a;
}

Cette fonction ne correspond pas en tout point à notre représentation plus haut. En effet, déjà, notre représentation plus haut suggère d’utiliser une méthode récursive (notamment en rappelant la même fonction) alors qu’ici, nous utilisons une méthode itérative (avec while). Ensuite, notre représentation suggère d’utiliser comme nom de fonction l’opérateur [] alors qu’ici, on fait appelle à la fonction element_i. À ma connaissance, il est impossible en C de modifier un opérateur d’une structure tel que [] (mais peut être que je me trompe). Dans notre représentation, nous utilisons [] pour faire l’analogie avec les tableaux (qui utilisent cet opérateur) et comprendre son objectif final (enfin je l’espère) donc ne soyez pas perturber par cela. On peut maintenant décrire le code si dessus. Il s’agit d’une valeur i qu’on incrémente tant que (while) celle ci est inférieur à l’indice recherché n et tant que notre liste a n’équivaut pas à NULL (ce qui reviendrait à arrêter l’instruction while quant on a atteint la fin de notre liste). Ensuite, si a équivaut à NULL (si on a atteint notre liste), on retourne NULL. Sinon on retourne a qui est l’élément qui correspond à notre indice n. Attention cette fonction doit être vérifier. En effet, l’écriture du type element_i(a, 5)->value est à proscrire si on n’est pas sûr de la taille de notre liste car la fonction peut retourner NULL et cela engendre un segment fault (puisque NULL ne contient pas value). Dans ce cas, on peut très bien choisir de retourner le dernier élément de notre liste dans le cas où i serait supérieur ou égal à n (après, tout dépend de votre programme et de vos objectifs) pour éviter à faire une nouvelle structure conditionnel qui devrait vérifier si ce que retourne cette fonction est bien différent de NULL. Si tel est notre objectif, on allège un peu notre code ce qui donne:

list element_i(list a, int n) {
    int i = 0;

    while(i < n && a->next != NULL) {
        a = a->next;
        ++i;
    }

    return a;
}

Ici, il suffit de s’arrêter au dernier élément de notre liste et retourner a dans tout cas, c’est à dire quand on à atteint l’indice n ou quand on a atteint la dernière valeur de notre liste. L’instruction element_i(a, 5)->value devient donc juste et retournera soit a[n], soit a[len(a) - 1] dans le cas ou on demande un indice supérieur ou égal à la taille de notre tableau. Ceci montre que mes exemples ne sont pas à prendre à la lettre et qu’on peut très bien s’amuser à les modifier pour qu’ils conviennent à nos objectifs, mais ça, c’est à vous de voir. Au final, on a une complexité de dans le pire des cas, on parle ici d’un accès arbitraire à un élément de la liste et le tableau a l’avantage sur ce coup car il propose un accès arbitraire avec une complexité de . Pourquoi ? Tout simplement parce que pour accéder au cinquième élément d’un tableau, on prend la référence du premier élément de notre tableau et on y ajoute 4 puisque, je le rappelle, la caractéristique d’un tableau est d’être représenter dans la mémoire de façon continue. On a maintenant des fonctions sur l’écriture, sur l’affichage mais aucune sur la suppression (on a quand même free_list mais cette fonction est plus de l’ordre technique … c’est une fonction tout de même importante). Ces fonctions de suppression sont pas très complexes et ressemblent aux fonctions d’ajouts sauf qu’au lieu d’utiliser malloc, on utilisera free. On va donc ici passer directement au code sans prendre le temps de bien expliquer ces fonctions que vous pouvez comprendre avec tout ce que vous avez vue:

list pop_before(list a) {
    if(a != NULL) {
        element* e = a->next;
        free(a);

        return e;
    } else
        return NULL;
}

list pop_back(list a) {
    if(a == NULL)
        return NULL;

    if(a->next == NULL) {
        free(a);
        return NULL;
    }

    element* e = a;
    element* t = a;

    while(e->next != NULL) {
        t = e;
        e = e->next;
    }

    t->next = NULL;
    free(e);

    return a;
}

La principale différence qu’on peut trouver par rapport aux autres fonctions, c’est l’utilisation non seulement de la fonction free pour libérer la mémoire mais aussi l’ajout d’une nouvelle structure conditionnel qui permet de vérifier si la liste est NULL. Ensuite, le fonctionnement est très basique. Les notions sont expliquées plus haut (comme pour la boucle while dans la fonction pop_back) et je n’y reviendrais pas. Nous allons maintenant parler des fonctions utiles pour les fonctions comme la concaténation. Pour ceux qui ne le savent pas, la concaténation permet de mettre bout à bout au moins deux ensemble (ici deux listes). On peut faire l’analogie avec les suffixes et les préfixes qui viennent s’ajouter à un mot pour changer notamment son sens. Ici, c’est pareil, on va ajouter les éléments d’une liste à une autre liste pour former une nouvelle liste. Pour le cas de deux tableaux, c’est simple, il suffit de savoir la taille de notre tableau a puis celle de notre tableau b pour ensuite créer un tableau c de taille . Il suffit ensuite d’insérer tout les éléments de a dans c et les éléments de b dans c à partir de . Pour le cas d’une liste, on peut très bien faire l’analogie avec push_before. En effet, ajouter une liste a au début d’une liste b équivaut à modifier la référence du dernier élément de a. Ainsi, on déduit que la complexité de cette algorithme est de . Il suffit d’utiliser par la méthode itérative notre fonction browse() pour atteindre le dernier élément de a et modifier comme étant égal à :

Et sous forme de code, cela donne:

list concat(list a, list b) {
    if(a != NULL) {
        element* t = a;

        while(t->next != NULL)
            t = t->next;

        t->next = b;

        return a;
    } else
        return b;
}

Comme on peut le voir, j’ai rajouter une structure conditionnel pour savoir si a n’équivaut pas à NULL, si c’est le cas, alors on peut parcourir a et atteindre son dernier élément pour changer la référence. Sinon, on retourne b (puisque a est NULL). Et dans le cas où b serait NULL cela n’influencerait en rien le code puisque si a et b sont NULL, on retourne NULL et si c’est seulement b qui est NULL, on retourne a (car on aura changer la référence du dernier élément de a en NULL ce qui ne change rien …). Ici, nous avons à faire à un effet de bord car en effet, on modifie la liste a en lui rajoutant la liste b. En d’autres termes, l’état de la variable a n’est plus le même après la fonction et dans le cas où vous voudriez utiliser la liste générer par cette fonction ainsi que la liste a (celle avant l’appel de la fonction concat) vous utiliseriez en faite la même liste. L’avantage, c’est qu’on alloue pas la mémoire, l’inconvénient c’est que dans une portée restreinte tel que la fonction main (c’est à dire en ne sachant pas le fonctionnent de concat car il est présent dans un autre fichier obscur de votre système) où nous ne pouvons pas avoir accès à la fonction concat, on peut avoir un comportement implicite (qui ne se voit pas) sur nos variables ce qui peut engendrer des erreurs (cela ce complique réellement dans l’utilisation de thread). Le moyen pour éviter cela est de cette fois, allouer la mémoire pour véritablement créer une nouvelle liste indépendante de A. Pour procéder, nous allons tester d’abord si A n’est pas NULL, dans ce cas, nous allons appliquer 2 instructions qui utilisent push_before et concat. Noter que nous avons à faire à de la récursivité. concat va s’occuper d’un ensemble de liste A qui ce trouve après le premier élément de A et push_before va ajouter le premier élément de la liste A au résultat de la fonction concat. On peut décrire le code de cette façon:

Comme on peut le voir, on a à faire à une fonction récursive. Dans le cas où a serait NULL, on retourne b (il n’y a pas de concaténation à faire). Sinon, on retourne e après qu’on est renseigner comme faisant référence à (bien entendu, il faut aussi renseigner aussi). Au final, cet appel de fonction va rajouter le deuxième élément à b qui va refaire appel à la fonction pour ajouter le troisième élément de a à b, ainsi de suite … Cette fonction récursive a la particularité de, non seulement faire un parcours de la liste a (en spécifiant toujours comme argument) mais aussi d’appliquer la fonction push_before. En effet, les instructions ressemble aux instructions qu’on retrouve dans la fonction push_before mais où value est a->value et notre liste est concat(a->next, b). Ainsi, on ne change pas l’état de la variable a (on y accède seulement en lecture) et seul b est modifié. L’effet de bord n’est plus. Et pour ce qui est du code, c’est très simple:

int concat(list a, list b) {
    if(a == NULL)
        return b;
    else
        return push_before(concat(a->next, b), a->value);
}

Donc ici, comme on peut le voir, on utilise la fonction puhs_before qui va rajouter le premier élément de a à concat(a->next, b). Cette fonction concat(a->next, b) va s’occuper d’ajouter récursivement tout les éléments situés après a, c’est à dire en commençant par a->next. On peut visualiser cette récursivité dans 2 sens. Le premier, c’est le parcours de a jusqu’à que a équivaut à NULL. Arriver à ce point, on peut maintenant, dans le sens inverse (du dernier élément de a au premier), ajouter les éléments de a avec push_before à b. On peut noter que, en comparaison avec les tableaux, la concaténation d’une liste a avec une liste b dépend que de la taille de a. Je vous rappelle que la concaténation d’un tableau à une complexité de , ici, c’est une complexité de seulement. Donc on peut dire que l’avantage de la concaténation d’une liste ce retrouve quant il s’agit d’ajouter une petite liste à une autre qui est grande. Par rapport à notre première fonction, la grande différence ce que la première reprend directement le fonctionnement de push_before avec comme valeur toute la liste a. Dans notre deuxième fonction, on utilise push_before pour chaque élément de a (on fait de la copie) ce qui assure que l’état de a reste inchangé. Nous allons maintenant faire une dernière fonction (enfin!) où cette fois, on va s’intéresser aux éléments par rapport à un critère. Par exemple, dans ma liste de nombres, je ne veux que les éléments dont leurs valeurs équivaut à 4. On va, ici, utiliser ce qu’on appelle un prédicat. C’est à dire une fonction mettant en œuvre un ou plusieurs arguments, utilisée pour déclarer quelque chose concernant notre élément courant, et dont le résultat est true ou false. C’est à dire que nous allons vérifier si notre élément courant (par exemple a[5]) contient comme valeur 4. Si c’est le cas, on retourne true, sinon false. L’utilisation des prédicats est courante et cette notion peut devenir très complexe quant il s’agit de connaître une relation entre 2 objets différents. Ici, ce n’est pas très complexe de trouver la relation qu’il peut y avoir entre le nombre 4 et notre liste a. Nous allons donc créer une nouvelle liste qui intègre la position de tout les éléments de notre liste a dont leurs valeurs équivaut à 4 (toujours dans notre exemple). Il suffit donc de parcourir notre liste a et de tester si l’élément courant de a (qu’on pourrait être noter a[i]) est égal à 4. Il s’agit ensuite d’ajouter (par le biais de push_before et non pas en modifiant la référence sinon on aura un effet de bord) i (la position courante) à notre nouvelle liste ce qui donne directement en code:

list search(list a, int value) {
    list    r = NULL;
    list    t = a;
    int     i = 0;

    while(t != NULL) {
        if(t->value == value)
            r = push_before(r, i);

        ++i;
        t = t->next;
    }

    return r;
}

Puisque la fonction push_before a une complexité de , la complexité de notre fonction search est de avec n la taille de notre liste a (puisqu’on fait un parcours dans cette liste). L’algorithme n’est pas très complexe et il ne s’agit que de parcourir notre liste et par une structure conditionnel, ajouter ou non la valeur courante de i à notre nouvelle liste, i étant l’indice courant de notre liste a. Ainsi, si on recherche tout les éléments dont leurs valeurs est 4, on obtient une liste r et cette instruction devrait renvoyer 4: a[r[0]] si len(r) est strictement supérieur à 0 (c’est à dire si notre fonction search trouve au moins une fois la valeur 4 dans notre liste a). Pour le cas d’un tableau, comme la modification de la taille d’un tableau est très couteuse, on ne peut ce permettre de modifier la taille du tableau r (ajouter un nouvelle élément) dès qu’on trouve un élément dans a dont la valeur est 4 (pour rester dans notre exemple). Il serait possible de compter le nombre d’éléments dont la valeur est 4 pour ensuite créer un tableau dont la taille conviendrait à notre recherche et refaire une boucle pour insérer les positions dans notre nouveau tableau. On fait donc deux boucles qui tourne n fois avec n la taille de notre tableau a. La complexité de cette algorithme reste puisque cela reste proportionnel à n mais il reste tout de même plus lent que celui avec les listes. Ensuite, si vous appliquer l’algorithme search pour les listes mais cette fois ci avec les tableaux, je vous ai dit qu’on ne pouvait ce permettre de le faire puisque la modification de la taille d’un tableau est trop couteuse. Néanmoins, il est intéressant de comprendre la complexité algorithmique dans ce cas car c’est là où on voit l’intérêt des structures de donner pour un même algorithme. En effet, comme la complexité algorithmique de push_before pour un tableau est de (puisqu’il s’agit de recréer un tableau b à partir d’un tableau a dont la taille serait et d’insérer tout les anciens éléments de a plus le nouvelle élément), et que notre boucle tourne n fois, on aura une complexité de donc une complexité quadratique.


Vous savez enfin manipuler les listes chaînées et les complexités de certains algorithmes basiques. Néanmoins, ne prenez pas mes exemples à la lettre car il faut tout d’abord prendre conscience de ce que vous avez besoin (car en effet, le tableau est parfois plus judicieux que les listes chaînées). Mais je n’ai pas développé tout ce qu’on pouvait faire avec les listes (il existe plein d’autres choses plus ou moins complexes, à vous de les trouver). Ce qu’il faut bien comprendre, c’est d’établir en fonction de ses besoins une structure de données qui ne dégrade pas la qualité de votre tableau. Mais attention, votre choix ne devrait pas être décisif et il sera peut être courant de jouer entre plusieurs structures de données. C’est là où intervient la notion de conversion (qui n’a jamais eu la problème de vouloir passer d’un int à un char* et vice-versa). Il s’agit donc de bien définir la structure de votre programme. Concernant mes affirmations sur PHP et Python concernant les listes, disons que ces tableaux on un comportement un peu bizarre par rapport à ce qu’on a appris. Le tout, c’est de renseigner, notamment avec la documentation, sur la complexité des opérations que l’on peut faire avec. Après, il s’agit de bien prendre conscience qu’une complexité de pour l’ajout d’un élément par exemple dans l’un de vos super algorithmes complexes peut entraîner des dégâts et dégrader votre programme. Il ne s’agit donc pas seulement de savoir la complexité de notre algorithme mais aussi de prendre en compte la complexité des autres algorithmes (plus simples que vous pouvez sûrement considérer comme des opérations naturels) que vous utilisez. Après, comme il est dit, on peut jouer entre les structures quant il s’agit de faire des opérations que, par exemple, les tableaux gèrent mieux que les listes.

C’est terminer pour cette article un peu trop long je pense ! Bonne programmation.

Thread et Fork

Comprendre ces deux notions systèmes demande à ce qu’on comprenne d’abord le Multitâche d’aujourd’hui: Le Multitâche Préemptif. Aujourd’hui, les OS comme Windows, GNU/Linux ou Mac OS X sont des systèmes préemptifs. Ils ont la capacité d’exécuter ou de stopper une tâche planifiée en cours, c’est grâce à un ordonnanceur (sheduler) préemptible qui présente l’avantage d’une meilleur réactivité du système et de son évolution. Il ne faut pas croire que vos programmes s’exécutent en même temps, c’est grâce à l’ordonnanceur qu’on génère ce qu’on appelle une simultanéité apparente qui est en faite, une alternance rapide d’exécution des processus. Et puisqu’elle est rapide, l’utilisateur lambda peut considérer que son système est multitâche.

Après, que l’ordonnanceur soit préemptible veut dire qu’il est capable de stopper un processus à tout moment pour laisser la priorité à un autre processus. Il faut aussi savoir qu’une tâche ne peut pas prendre un temps non-définie, c’est à dire qu’un temps limité est attribué au dit-processus et que si il n’accomplie pas sa tâche avant la limite fixée, la priorité revient à un autre processus. Il y a alors ce qu’on appelle la commutation de contexte où l’état de notre premier processus est enregistrer dans la mémoire. Bien sûr, cette commutation est transparente.

À l’inverse, on a ce qu’on appelle des systèmes d’exploitation collaboratifs dans lequel c’est au processus de choisir arbitrairement si il garde la main ou si il la rend. Un problème que l’on peut rencontrer dans ce genre de système et que si le processus ne redonne pas la main (parce qu’il est bugué), il peut faire arrêter le système en entier.

Fork

Le fork, c’est semblable au copier/coller mais cette fois d’un processus. Un processus A fait appelle à la fonction fork(). Il va ce copier (ainsi que toutes ces variables globales) en un processus B. Il faut cependant bien comprendre que le processus A et le processus B sont bien distinct. C’est à dire que si tout le deux contiennent la variable ma_variable, selon l’exécution du processus B, cette variable peut être différente entre les deux processus.

Néanmoins, il faut aussi signaler que les instructions du processus A sont les mêmes que celle du processus B. Alors comment on peut modifier l’état de notre variable ma_variable dans notre processus B (en sachant qu’il y a exactement les mêmes instructions) pour quelle soit différente de celle de notre processus A ? Eh bien ce qui distingue nos deux processus, c’est leurs PIDs. En effet, il nous est donc possible par condition mettant en jeu la valeur du PID du processus de faire tel ou tel instruction. Et là, il faut savoir que la fonction fork() retourne le PID du processus B dans le processus A et cette même fonction retourne 0 dans le processus B.

Au final, ce qui pourra différencier le processus A du processus B, c’est ce que retourne la fonction fork(). Néanmoins, il faut savoir que même si leurs variables à chacun sont distinctes (rappelez-vous, ma_variable est distincte dans les deux processus et elle peut être différente selon l’exécution du processus) après exécution (car à la naissance du processus B, elles sont équivoques), la position du curseur sur la sortie standard (stdout) et la même. Par contre, il n’y a pas la notion de partage et si l’un des processus ouvre un autre fichier, cela ne ce répercutera pas sur l’autre processus. Le processus B hérite des descripteurs de fichiers du processus A (dont stdout) seulement pendant le fork, c’est à dire au moment où on copie le processus A.

Ensuite, le processus A peut attendre que le processus B ait terminé pour lui aussi ce terminer, notamment grâce à la fonction wait(). Par contre, si le processus A ne contient pas l’instruction wait() (et ce serait de même pour le processus B car on copie le code), si il ce termine avant le processus B, le processus B continue ! Le processus B a donc très de dépendance au processus A et si l’un s’arrête, l’autre continue.

Thread

Un thread est similaire à un processus car il exécute aussi un ensemble d’instructions. Et comme il l’est dit en introduction, ces exécutions semble ce dérouler parallèlement (notamment grâce à la vitesse de commutation de contexte). Mais, contrairement au fork, le thread partage la même mémoire virtuelle (les variables globales ne sont plus distinctes entre processus). Par contre on créer une pile d’appel propre au thread. Ceci à pour qualité de ne pas rendre coûteux la commutation de contexte puisque la mémoire virtuelle reste la même. Par contre, si notre processus A ce termine, tout les threads de ce processus sont tués (référence à la commande kill des systèmes UNIX). Cependant, il faut signaler que le code qu’il y a dans le thread créer par son processus père n’est pas forcément le même. En effet, le thread va exécuter le code d’une fonction présente dans le processus père, c’est pour cela qu’on compare souvent le thread à un processus léger.

Le cas d’utilisation d’un thread ce fait ressentir quant il s’agit de mettre une tâche de fond à notre processus pour ne pas interrompre l’utilisation du programme par l’utilisateur. C’est le cas par exemple quant il s’agit de rechercher un élément dans le texte (ce qui peut être long selon le taille de votre texte) mais de pouvoir écrire en même temps dans le dit-texte. Le programme créer un thread qui exécutera une fonction de recherche et puisque nous utilisons des systèmes préemptifs, si l’utilisateur fait appelle au programme pour écrire dans le texte, le processus de recherche s’arrêtera. Mais vous me direz que tant qu’on écrit, la recherche ne trouvera rien, eh bien c’est là où il faut prendre en compte la rapidité de commutation de contexte et quant vous vous arrêter d’écrire pendant un petit laps de temps, le thread de recherche devient prioritaire et reprend la main. Eh oui, votre ordinateur est très rapide.

Mais venons un peu plus au plan technique. Il faut bien avoir l’idée que le thread partage la même mémoire virtuel que son père, le processus. Mais il peut y avoir collision si le processus voudrait modifier une variable globale ainsi que le thread. C’est là où intervient le notion de mutex. Le mutex doit être globale dans notre processus pour que le thread puisse aussi le voir et la capacité du mutex et de pouvoir attribuer un seuil de priorité supérieur aux autres threads et/ou au processus père. Ainsi, on peut modifier une variable sans qu’il n’y est de collision avec d’autres instructions.

On pourrait aussi parler dans le cadre de la synchronisation pour qu’il n’y est pas d’interblocages (deadlocks) sur la modification d’une variable, des sémaphores. Cette notion a été inventé par Dijkstra (un grand nom) pour résoudre le problème du dîner des philosophes qui est une représentation plus abordable pour quelqu’un qui ne fait pas de la programmation de notre problème de pouvoir modifier notre variable par le biais du processus ou d’un thread. Mais ceci est plus de l’ordre technique puisque son objectif est le même que le mutex.


Cette article essaye de bien montrer la différence entre le Fork et le Thread ce qui n’est pas très compliqué mais dont les ressources sur internet manquent. Cette article va aussi subir des updates pour rendre plus appréciable la distinction entre ces deux notions mais aussi pour faire une approche en C. En espérant que vous ayez compris cette notion !

Moteur de Template et PHP

L’article a déjà été fait mais cela n’a pas marqué les gens. En effet, le moteur de Template est inutile en PHP ! Il est vrai que c’est pratique, que c’est cool comme truc, pour l’intégrateur qui sait à peine utiliser le langage de balisage HTML, c’est pratique de pouvoir mettre en place un moteur de Template pour que lui est ses doigts douteux ne touche pas une seul ligne de code. Seulement, vous vous y prenez mal cher développeur et ce n’est pas en utilisant un moteur de Template que vous vous aiderez, que vous aiderez à la maintenance de votre site et que vous aiderez l’intégrateur HTML. Maintenant, expliquons pourquoi c’est inutile d’utiliser un moteur de Template.

Un moteur de Template permet de séparer la logique applicative de la présentation (typique de l’architecture MVC). Avant, on écrivait le code PHP directement dans la page HTML. Cependant, au fur et à mesure, on a découvert que cela devenait de plus en plus illisible. Il fallait trouver une solution et ce fût le moteur de Template. Une application PHP qui permet de faire le lien entre nos programmes et la présentation. C’est bien, ça marche ! Notre développement dépendait donc de ce moteur qui est à la limite intrusif. Cependant, comprendre la logique de cette application n’était pas dans nos objectifs et du moment que cela fonctionnait, c’était cool. Et pourtant, regarder de plus près son fonctionnement est très intéressant !

Un moteur de Template, c’est d’abord une application qui impose son langage qui permet de gérer l’affichage. On retrouve notamment ceci: {ma_variable}. Connaissez vous ce langage ? Moi pas … On constate que ce langage est propre au moteur de Template, c’est à dire que dès que vous voudriez changer de moteur de Template, vous devrez certainement changer vos fichier *.tpl (il y a même des extensions pour ça, tqvu). Il faut aussi comprendre que ce langage n’est pas reconnue par le serveur, alors comment le moteur affiche le contenu de ma_variable dans le fichier HTML ? C’est la magie des str_replace et preg_replace. Et oui, le moteur de Template utilise d’une façon prédominante ces fonctions. Alors, comme ça, c’est rien mais concrètement, le temps d’exécution de vos scripts peut être multiplier par 5. Autant revenir à notre code illisible dans le document HTML, c’est plus rapide …

Néanmoins, les moteurs l’ont bien compris, l’optimisation. Ils ont donc insérer nativement un système de cache pour que le temps d’exécution puisse devenir faible. Ça marche, c’est toujours cool.

Alors que doit t’on faire ? Ne plus utiliser les moteurs de Template, revenir à un code illisible ? Non, il suffit de dire une vérité qui va vous ouvrir les yeux: le PHP est déjà un moteur de Template. Vous utilisiez donc une imbrication de moteurs de Template quand vous utilisez Smarty par exemple et ceci est le cause de la dégradation de vitesse d’exécution de vos scripts.

Autant vous dire qu’avec un peu de logique et 2 fonctions, on peut très vite comprendre que PHP est déjà un moteur de Template. Êtes vous sérieux quand vous dites qu’il est moins compliqué d’écrire ceci: {ma_variable} que cela: <?php echo $ma_variable; ?> ? Il n’y a pas de réel différence et il suffit d’un mot pour changer nos habitudes: la tamporisation ! PHP nous mets déjà à disposition des fonctions pour traiter l’affichage de façon isolé par rapport à l’application, c’est notamment grâce à la fonction extract(). Vous pouvez regarder ce code PHP qui présente le fonctionnement global d’un moteur de Template intelligent:

<?php

class template
{
    private $attributs = array();
    private $properties = array();

    public function __construct( $file ) 
    { 
        $this->attributs['file'] = $file;

        if( file_exists( $file.'.cache' ) )
        {
            echo 'cache';
            echo file_get_contents( $file.'.cache' );
            die();
        }
    }

    public function assign( $property, $value )
    {
        $this->properties[ $property ] = $value;
    }
    public function parse()
    {
        extract( $this->properties );

        ob_start();

        require $this->attributs['file'];

        $result = ob_get_contents();

        ob_end_clean();

        file_put_contents( $this->attributs['file'].'.cache',
                           $result );

        return $result;
    }
}

?>

Voici la partie applicative:

<?php
    require 'template.class.php';

    $tpl = new template( 'index.tpl' );

    $tpl->assign( 'title', 'Le Titre du Document' );
    $tpl->assign( 'content', 'Le Contenu du Document' );

    $tpl->assign( 'tableau', array( 'test 1' => 'result 1',
                                    'test 2' => 'result 2',
                                    'test 3' => 'result 3' ) );

    echo $tpl->parse();
?>

Et le fichier TPL:

<h1><?php echo $title ?></h1>

<p><?php echo $content; ?></p>

<ul>
    <?php foreach( $tableau as $key => $value ) { ?>

    <li><?php echo $key; ?> | <?php echo $value; ?></li>

    <?php } ?>
</ul>

Il n’y a rien de bien complexe pour l’intégrateur HTML et par rapport à un moteur de Template comme Smarty je vous conseille vivement celui-ci qui est déjà plus rapide. Ce moteur intègre déjà un système de cache et même si la syntaxe dans le fichier TPL est toujours celle de PHP, elle n’est pas moins complexe que celle que utilise les moteurs de Template. En espérant que cette article vous a ouvert les yeux en ce qui concerne les moteurs de Template. En conclusion, on peut dire que le moteur de Template ce ressent quand l’intégrateur HTML a des problèmes avec le PHP, mais le véritablement problème ne serait pas la médiocrité de votre intégrateur HTML ?

Brainfuck et Masturbation Intellectuelle !

Il est là le plus dur, mais le plus passionnant, le Brainfuck. Écrire un programme en Brainfuck, c’est plus difficile je pense qu’écrire un programme en Assembleur quoique la comparaison ne devrait pas ce faire puisque je n’ai jamais programmer en Assembleur (trop dépendant de l’architecture du processeur). Néanmoins, programmer en Brainfuck, c’est tout de même difficile. Les notions à apprendre sont simple, il y 8 opérateurs, pas très compliqué à retenir. Mais quant on sait que la notion de condition n’y est pas nativement, on ce retrouve, pas restreint dans les possibilités du langages (puisqu’il est Turing-complet quand même) mais on ce retrouve à devoir taper un nombre incalculable de caractère pour au final faire peu de chose.

Là où le Brainfuck est réellement intéressant, c’est justement qu’il est Turing-complet et que théoriquement, il est capable de faire tout les programmes possibles et inimaginable. Après, c’est à savoir si il est humain de programmer avec car quand on regarde les opérateurs … C’est moche, affreux, etc … D’où le nom de Brainfuck. Alors programmer avec, non. Cependant, il existe un exercice très simple en lien avec le Brainfuck: un compilateur !

La compilation, pour les langages comme le C ou le C++, c’est une affaire d’ingénieur très compétent car il existe encore des zones obscures quant il s’agit de l’optimisation. Mais Brainfuck est un langage pas très compliqué à prendre en main et de ce fait, il est pas très compliqué de faire un compilateur de Branfuck. Si on ce réfère correctement à ce que nous dit Wikipédia et aux différents opérateurs (Wikipédia donne même une analogie en C donc bon), on sait immédiatement qu’au final, notre compilateur ne tiendra que par une suite de if et else if ou d’un switch ... case pour les plus propres. C’est mon cas. Et voici donc mon super compilateur Brainfuck :

#include <iostream>
#include <fstream>
#include <sstream>
#include <stack>

using namespace std;

int main(int argc, char* argv[]) {
    if(argc == 2) {
        ifstream        file(argv[1]);
        string          buffer;
        string          f;
        int             l = 0;

        while(getline(file, buffer)) {
            if(l != 0)
                f += buffer + "\n";

            ++l;
        }

        unsigned char   b[1024] = {0};
        char*           k;
        unsigned char*  x = b;
        stack<char*>    y;

        k = const_cast<char*>(f.c_str());

        while(*k) {
            switch(*k)
            {
                case '+': ++*x;
                    break;
                case '-': --*x;
                    break;
                case '>': ++x;
                    break;
                case '<': --x;
                    break;
                case ',': *x = cin.get();
                    break;
                case '.': cout << *x;
                    break;
                case '[': y.push(k + 1);
                    break;
                case ']':
                    if(*x) {
                        k = y.top();
                        continue;
                    } else
                        y.pop();
                    break;
           }

           cout.flush();
           ++k;
        }
    }

    return 0;
}

Bon là, il s’agit seulement de prendre ce que nous dit Wikipédia, hein … Mais, ici, il y a quelque chose qui change. En effet, si vous mettez ce programme dans votre /usr/local/bin (votre petit bac à sable de vos programmes) et que vous créez un fichier du genre:

#!/usr/local/bin/brainfuck

++++++++++
[
    >+++++++>++++++++++>+++>+<<<<-
]
>++.
>+.
+++++++.
.
+++.
>++.
<<+++++++++++++++.
>.
+++.
------.
--------.
>+.
>.

Un coup de chmod +x mon_brainfuck.bf et ensuite ./mon_brainfuck.bf, Et voilà, un beau « Hello World! » ! En effet, mon programme n’est pas réellement un compilateur comme on pourrait l’entendre mais un interpréteur de fichier Brainfuck (extension *.bf steuplé). Bon, après, je pense que ceci est spécifique à un environnement type UNIX mais voilà un programme qui n’a aucun intérêt à être utilisé mais qui est quand même ludique pour ce qui est du fonctionnement du langage Brainfuck. Le fonctionnement d’un compilateur tel que le g++ est un peu plus complexe que cela à vrai dire !

Notez tout de même le poids monstrueux de mon programme (32 Kio) mais je pense que c’est dû aux include … Enfin après, j’ai pas chercher non plus à faire la course aux poids (le compilateur actuel du créateur du Brainfuck fait 171 octets). Donc si vous voulez vous amuser à faire un compilateur je vous suggère celui-ci, c’est simple, rapide et pas pratique !

Combinaison sans répétitions

Dans un de mes programmes, j’ai dût faire un petit sous programme qui fait des combinaisons sans répétions. En effet, il fallait faire toutes les combinaisons possibles sans prendre en compte l’ordre de celles-ci. Par exemple, ["A","B","C"] équivaut à ["B","C","A"]. J’ai donc d’abord appliqué mes idées en Python (histoire d’éviter le compilation en C++). Je me suis référer à Wikipédia et j’ai demandé de l’aide à certaines personnes sur Progmod pas pour me donner un code déjà tout fait mais pour me donner un petit coup de pouce. Il fallait que je parte sur quelque chose de solide.

La première chose à savoir sur la combinaison sans répétitions, c’est ceci: . Cette formule mathématique détermine le nombre de combinaisons possibles qu’il peut y avoir avec n la taille de l’alphabet et k la taille d’une combinaison. Par exemple, si nous devions choisir 5 nombres différents entre 1 et 50, nous aurions: possibilités. Ce calcul est important pour notre programme et il nous suffira d’implémenter une fonction factorielle pour avoir le résultat.

Ensuite, il faut voir le lien possible avec les combinaisons, et là, c’est à l’aide d’un exemple que l’on peut s’en sortir. Pour un alphabet ["A","B","C","D","E"], on veut toutes les combinaisons à 3 éléments:

  • EAB
  • EAC
  • EAD
  • EBC
  • EBD
  • ECD
  • DAB
  • DAC
  • DBC
  • ABC

Bon il suffit de ce triturer un peu la tête pour obtenir ce résultat et dans ce cas ci, je vous ais ranger les combinaisons selon un ordre qui vous aidera à établir l’algorithme. Comme on peut le constater, cette liste ce compose en 3 grandes parties, les combinaisons contenant la lettre E, les combinaisons contenant la lettre D et la seul combinaison sans répétitions de 3 éléments possible pour l’alphabet ["A","B","C"]. On peut partir du constat donc que toutes les combinaisons d’un alphabet ["A","B","C","D","E"] équivaut à toutes les combinaisons d’un alphabet ["A","B","C","D"] plus toutes les combinaisons possibles contenant la lettre E. Ou plus généralement, l’ensemble des combinaisons parmi les 1..N, c’est l’union des combinaisons parmi les 1..N-1 (donc qui ne contiennent pas N), et des combinaisons qui contiennent N avec N la taille de notre alphabet.

Ceci fait référence à quoi au final ? À la récursivité ! Et oui, on pourrait présenter ce problème comme ceci:

L.pop() // On enlève la dernière lettre
G = G + combinaisons(L, k) // On fait l'union des combinaisons 
                              parmi les L[1]..L[N-1] et 
                              des combinaisons qui contiennent L[N]

L étant notre alphabet, G le résultat que nous recherchons et k la taille d’une combinaisons. Au final, il s’agit maintenant de faire seulement toutes les combinaisons possibles contenant L[N]. Mais attention, nous avons repérer un cas particulier. En effet, si k (la taille d’une combinaison) équivaut à la taille de notre alphabet L, on peut déduire que la seul combinaisons dans ce cas et directement L (en effet, on peut le voir avec la dernière combinaison de notre exemple: « ABC »). Il y a 2 autres cas que nous devons traiter avant d’attaquer le gros morceau.

En effet, si notre k équivaut à 1, alors, il suffit de retourner comme valeur les éléments qui compose L. Par exemple, si on veut toutes les combinaisons contenant 1 seul élément (k) pour un alphabet ["A","B","C"], nous aurons comme résultat, ceci:

  • A
  • B
  • C

Et le deuxième cas est l’impossibilité de faire toutes les combinaisons sans répétitions. Il nous est impossible de faire les combinaisons contenant 4 éléments pour un alphabet n’en contenant que 3. Ce cas est possibles seulement dans le cadre de combinaisons avec répétitions.

On peut maintenant mettre en forme notre application déjà en traitant nos 3 cas:

def combinaisons(L, N, k):
    i = 0

    G = []
    s = ""

    if len(L) < N: # L'impossibilité
        return G
    elif N == 1: # Si k = 1
        return L
    elif len(L) == N: # Si k équivaut à la taille de notre alphabet
        while i < len(L)
            s = s + L[i]
            i = i + 1

        G.append(s)

    return G

Bon, ce code est du Python, je pense que c’est un langage qui est compréhensible par tout ceux qui ont des bases en programmation. Mais attention, la notation est un peu plus différente que ce qu’on a vue précédemment. Dans ce code, k représente le rang mais c’est un détail que nous verrons plus tard. Il faut bien comprendre ici que N équivaut à la taille d’une combinaison et L est notre alphabet.

Maintenant, il s’agit de faire toutes les combinaisons avec la dernière lettre de notre alphabet. Dans notre exemple, c’est E:

  • EAB (5 1 2)
  • EAC (5 1 3)
  • EAD (5 1 4)
  • EBC (5 2 3)
  • EBD (5 2 4)
  • ECD (5 3 4)

Là aussi, il faut remarquer un cheminement logique entre les combinaisons. J’ai mis des nombres à côté pour que ce soit plus parlant, il faut juste comprendre que l’alphabet ["A","B","C","D","E"] équivaut à l’alphabet [1,2,3,4,5]. Et la, normalement, ça saute aux yeux. En effet, on incrémente le dernier élément de notre dernière combinaison mais si, celui-ci équivaut au dernier élément de notre alphabet, alors on incrémente l’avant dernier élément de notre dernière combinaison.

  • EAB = L[4] + L[0] + L[1]
  • EAC = L[4] + L[0] + L[2]
  • EAD = L[4] + L[0] + L[3] // 3 = 4 - 1
  • EBC = L[4] + L[1] + L[2]
  • EBD = L[4] + L[1] + L[3] // 3 = 4 - 1
  • ECD = L[4] + L[2] + L[3]

Autre remarque que l’on peut faire, c’est que si on enlève la lettre E de nos combinaisons, on a toutes les combinaisons possibles contenant 2 éléments pour l’alphabet ["A","B","C","D"]. On peut donc définir que ce nombre de combinaisons équivaut à: avec 4, la taille de notre alphabet moins 1 et 2, la taille d’une combinaison moins 1 ! On pourrait implémenter ce mécanisme de façon itérative (c’est à dire, à l’aide d’une boucle) donc la valeur maximal serait le résultat de notre formule mathématique.

L’idée donc serait de créer un tableau n qui contiendrait les différents curseurs de notre alphabet. Dans notre exemple n[0] équivaudrait à 0 pour notre première combinaison et n[1] serait égal à 1. Ensuite n[0] ne changera pas et n[1] sera incrémenter pour être égal à 2 pour notre deuxième combinaison. Et dans le cas où n[1] serait égal à la taille de notre alphabet, alors on incrémentera la valeur précédente du tableau n. Seulement, il faut s’intéresser au cas général et cette technique ce trouver limiter par le nombre d’élément dans une combinaison (ici 3). Il faut aussi dénoter que la taille du tableau n dépend de la taille d’une combinaison (comme il en est le curseur) mais comme le premier élément de nos combinaisons n’est pas important (il reste le même), la taille du tableau n équivaudra à N - 1 dans notre code.

C’est là où intervient la notion de rang. Dans ce cas ci, nous avons ce qu’on appelle 3 rangs. Le rang contenant la lettre E au début, le rang contenant la lettre D au début et le dernier rang qui est un cas particulier. Mais le rang ne nous permet pas que de savoir cela, il nous permet de savoir aussi à quelle valeur doit s’arrêter nos curseurs dans le tableau n. En effet, n[0] doit s’arrêter à la troisième valeur de notre alphabet et n[1] à la quatrième. Cela ce repère facilement quant il s’agit de faire des combinaisons à 4 éléments (ce que je vous invite à faire pour mieux comprendre).

Enfin, il faut souligner aussi une dernière notion qui est de bien comprendre que la première valeur d’une liste ou d’un tableau en programmation, c’est tableau[0] et pas tableau[1]. C’est peut être anodin mais j’utilise des notions comme 3 ou 4 où il faudrait, dans notre programmation, soustraire par 1.

On peut donc commencer un peu à coder après avoir mis toutes les idées en place. Nous nous positionnons directement dans le cas où nous devons faire une combinaisons, c’est à dire quand la taille de notre combinaison et strictement inférieur à la taille de notre alphabet:

l = factorielle(len(L) - 1)/(factorielle(N - 1)
     * factorielle((len(L) - 1) - (N - 1)));

Ici, il s’agit de calculer la limite de notre boucle pour générer de façon itérative nos combinaisons. Comme on l’a constater pour générer les combinaisons avec la lettre E, nous avons à appliquer la formule: où, je le rappelle 4 est la taille de notre alphabet (len(L)) moins 1 et 2, la taille d’une combinaison (N) moins 1.

while i < l:
    s = L[len(L) - 1]

On sait que nos combinaisons commencerons toujours par la dernière lettre de notre alphabet (ici E). Note tout de même que j’enregistre les combinaison dans une variable de type String. J’aurai pu enregistrer les combinaisons dans une liste, ce n’est pas très compliqué (c’est notamment ce que j’ai fait en C++) mais la variable de type String me suffit amplement.

    while h < len(n):
        if j > 0 and j < len(n):
            n[j] = n[j - 1] + 1

        s = s + L[n[h]]
        h = h + 1
        j = j + 1

C’est là où ça devient un peu compliqué. Comme on peut le remarquer, on utilise 2 nouvelles variables j et h. Il faut comprendre ici, que, quand i = 0, c’est à dire dès notre première combinaison, j et h sont aussi égal à 0. Néanmoins la variable j et tout aussi importante car elle permet de mémoriser quelle curseur a été modifié auparavant. Pour la première combinaison, il est logique que j ne renvoi à aucune valeur cependant, pour, par exemple, la deuxième combinaison, j permettra de nous dire que la dernière valeur de notre tableau n (contenant nos curseurs) a été modifié. C’est important de pouvoir dénoter ça car, dans le cas d’une génération de combinaisons avec 4 éléments ainsi que le même alphabet, j nous dira que ce serait (dans un des cas) l’avant l’avant dernière valeurs de notre tableau n qui a été modifié. L’intérêt ensuite est d’incrémenter toutes les valeurs du tableau n après j pour qu’on est un ensemble cohérent. En effet:

  • EAD = L[4] + L[0] + L[3] // L[4] + L[ n[0] ] + L[ n[1] ]
  • EBC = L[4] + L[1] + L[2]

Quand on arrive au maximum (ici D) possible pour le dernier élément de notre combinaison, on a vue qu’on modifiait l’avant dernière valeur de n. Cependant, on peut remarquer tout de même que la dernière valeur de n est elle aussi modifié ! Et elle n’équivaut pas à une soustraction de 1 de son ancienne valeur (2 = 3 - 1) car, si vous avez le cas d’une combinaison à 4 éléments avec le même alphabet, vous auriez remarquer que le tableau n est une suite récurrente n[j+1] = n[j] + 1 dont la valeur initial (et c’est important de le spécifié) et n[j]. Cette exemple montre aussi qu’on est capable d’implémenter quelque chose de récurrent de façon itérative (à l’aide d’une boucle). Mais revenons à nos moutons. Comme vous pouvez le remarquer, à l’initialisation quand i = 0, le tableau n équivaut au début à cela: [0,0] (n’oubliez pas que la taille du tableau n équivaut à la taille d’une combinaison moins 1), après passage de la deuxième boucle, il aura comme valeur ceci: [0,1] et si on ce réfère à la notre quatrième liste d’exemples, on peut retrouver que la première combinaison équivaut à EAB = L[4] + L[0] + L[1], donc dans notre cas à EAB = L[4] + L[ n[0] ] + L[ n[1] ]. Remarque tout de même que je rajoute immédiatement les valeurs dans la combinaison dans la boucle avec s = s + L[n[h]] et ensuite, je fais une incrémentation de h et de j.

Ensuite, il peut être déroutant de ce voir utiliser la variable j dans la boucle alors que la variable qui arrêtera la boucle est h. Mais j intervient seulement pour réinitialiser le tableau n selon la suite récurrente que l’on vient de voir. Mais l’intérêt de j s’explique véritablement après, quand il s’agit de savoir quelle valeur de n nous devons commencer à modifier.

    h = 0
    j = 0

    while j < len(n) and n[j] != j + k:
        j = j + 1

    if j > 0:
        n[j - 1] = n[j - 1] + 1

    i = i + 1 

Voilà le grand intérêt de j.D’abord, il faut remarquer une chose, c’est que la variable j n’est pas remis à 0 pour la boucle suivante (ce qui est le cas de h), car la valeur qu’on a trouver de j nous servira pour le passage dans la boucle suivant. Là où c’est réellement intéressant, c’est dans la deuxième boucle où la condition est bien particulière j < len(n) and n[j] != j + k. Il y a deux choses à repérer. Déjà, il faut que j soit strictement inférieur à la taille de notre tableau n (car si on ce réfère au code précédent, si j et supérieur ou égal à la taille du tableau n, il y aura une erreur puisqu’on utilise n[j]) et ensuite, il faut que n[j] soit différent de j + k. Il faut revenir en arrière pour comprendre un peu le pourquoi de cette condition. On a en effet, dit que la dernière valeur de notre tableau n dans le cas d’une combinaison à 3 éléments ne doit pas dépasser 3 car L[3] équivaut à D et si on dépasse 3, on ce retrouve à rajouter la lettre E à la prochaine combinaison ce que nous ne voulons pas faire. Cependant, il faut aussi savoir que l’avant dernière valeur de notre tableau n ne doit pas dépasser 2 (c’est à dire la lettre C). Cette explication ce voit plus particulièrement quant il s’agit de faire une combinaison à 4 éléments. Ainsi, à l’aide de cette boucle, si l’un de nos curseurs (une des valeurs de notre tableau n) équivaut à sa limite (D ou C selon le curseur), on arrête d’incrémenter j. j pointera sur une des valeurs du tableau n qui est à son maximum ! Notez tout de même qu’il y a un certain sens à prendre en compte. On pourrait très bien aussi prendre le curseur qui est à son maximum en partant de la fin mais si le suivant est aussi à son maximum (toujours en partant de la fin), j ne pointera pas vers celui-ci et nous aurons une erreur. Le fait est que nous devons avoir le premier curseur qui en est à son maximum en partant du début. Après cela, on a juste à incrémenter le curseur précédent à j de 1. On peut encore une fois voir ce fait dans une liste:

  • EAD, n[1] = 3, c'est le maximum
  • EBC, on incrément n[0] de 1

On peut aussi noter la notion de rang qui est primordiale car c’est grâce à lui et à j qu’on détermine le maximum par la formule: j + k. Et après qu’on a déterminer j on refait une réinitialisation de n avec comme valeur initial n[j]. C’est bien à prendre en compte car dans une combinaisons de 4 éléments avec le même alphabet, j pourrait être égal à 1 alors que la première valeur du tableau n est n[0]. Donc la valeur j est essentiel pour que les curseurs ont une suite logique. Au final, votre code doit ressembler à cela:

L = ["A","B","C","D","E"]
N = 4
G = []

def factorielle(x):
    if x < 2:
        return 1
    else:
        return x * factorielle(x - 1)

def combinaisons(L, N, k):
    h = 0
    i = 0
    j = 0

    n = [0] * (N - 1)

    G = []
    s = ""

    if len(L) < N:
        return G
    elif N == 1:
        return L
    elif len(L) == N:
        while i < len(L):
            s = s + L[i]
            i = i + 1

        G.append(s)
    elif len(L) > N:
        l = factorielle(len(L) - 1)/(factorielle(N - 1)
             * factorielle((len(L) - 1) - (N - 1)));

        while i < l:
            s = L[len(L) - 1]

            while h < len(n):
                if j > 0 and j < len(n):
                    n[j] = n[j - 1] + 1

                s = s + L[n[h]]
                h = h + 1
                j = j + 1

            G.append(s)

            h = 0
            j = 0

            while j < len(n) and n[j] != j + k:
                j = j + 1

            if j > 0:
                n[j - 1] = n[j - 1] + 1

            i = i + 1

        L.pop()
        G = G + combinaisons(L, N, k - 1)

    return G

G = combinaisons(L, N, len(L) - N)

print(G)

Notez tout de même l’ajout à la fin de L.pop() et G = G + combinaisons(L, N, k - 1) qu’on a expliquer au début de cette article. Il faut noter aussi que la fonction combinaisons reçoit 3 paramètres. Le paramètre en plus est le rang k. Et le dernier rang possible pour des combinaisons de 3 éléments pour un alphabet de 5 éléments et 2 car:

  • Deuxième rang: combinaisons contenant la lettre E
  • Premier rang: combinaisons contenant la lettre D
  • Le cas particulier ABC

Bon voilà, les combinaisons sans répétions, c’est compliqué … Sérieusement, cela nous permet d’appréhender d’autres problèmes qui peuvent être typique de celui-ci, le mieux c’est de trouver tout seul les rapports logiques qu’il peut y avoir entre les combinaisons et ensuite d’appliquer nos idées en terme de programmation (à l’aide de Python notamment) et c’est comme ça qu’on avance. C’est sûr, vous n’y arriverez pas du premier coup, j’ai mis un certains temps avant de trouver cette algorithme et je suis sûr, il existe encore un moyen plus rapide que celui-ci néanmoins, l’intérêt c’est justement de comprendre et d’apprendre. On ne vous demande pas de tout savoir, pour preuve, on est aller chercher certaines notions sur Wikipédia, on vous demande d’être avant tout logique et grâce à ça, vous pouvez voir le lien possible qu’il y a entre certains éléments et par la suite les générer.

Et ce fût Markdown !

Il y avait vraiment quelque chose qui me gêner dans Dotcleat ou WordPress … C’était le WYSIWYG. En effet, je déteste ce genre de truc. C’est pas que c’est pas pratique mais ça m’énerve de devoir regarde l’onglet HTML pour voir si tout ce passe bien dans mon écriture. Et je préfère de loin utiliser un langage qui ce compile ensuite en du HTML que de devoir sans cesse modifier mes articles car il y a une balise <p> en trop par exemple. J’ai donc apprécier l’utilisation du Markdown qui est un langage simple à prendre en main (pour preuve, il suffit de ce faire un petit aide-mémoire et ça passe tout seul) et pratique quant il s’agit d’écrire des documents comme des articles de blog.

J’ai donc trouver un plugin pour WordPress qui permet de d’écrire du Markdown pour générer ensuite du HTML. C’est d’ailleurs pour cela que j’ai fait un passage forcer à ce CMS en supprimant tout mon ancien blog (flemme de faire une importation d’articles qui plus est écrit en HTML alors que j’utilise désormais le Markdown). Bref, je fait ce petit article pour vous suggérer vivement le Markdown qui est pour moi (après le LaTeX) le meilleur moyen pour écrire des articles sans ce prendre la tête !

Sa propre Radio Anticonformiste !

C’est parfois le kiffe de montrer à ses potes sa propre radio qui émet de la musique de façon illégal et des textes qui prônent la révolution et ça de façon illégal ! C’est sûr, c’est cool et il faut pas grand chose pour mettre tout ça en place. En outre, un vieux PC qui date de 7 ans et un FAI (Orange et Numéricable sont à proscrire), une clé USB à la limite de 4 Go, et rouler jeunesse ! Il suffit juste ensuite de ce munir d’une bonne distribution GNU/Linux comme Arch Linux, de l’installer sur votre ordinosaure et de bien lire la documentation du logiciel MPD. Votre radio ne tient qu’à cela: MPD !

On peut dire que sa configuration est très simple. Personnellement, j’opte que pour le Streaming HTTP mais certains diront qu’il existe Icecast. Franchement, pour l’utilisation que j’en fait, le Streaming HTTP me suffit amplement. Bref, comment bien configurer votre super radio ? D’abord, on peut dire qu’il y a la configuration de base. C’est à dire spécifier les dossiers et tout le tralala. Ensuite, un conseil, créer un utilisateur mpd et compiler vous même MPD avec les options qui vont bien. Après, votre fichier doit ressembler à cela:

music_directory       "/home/mpd/music"
playlist_directory    "/var/lib/mpd/playlists"
db_file               "/var/lib/mpd/database.db"
log_file              "/var/log/mpd/mpd.log"
pid_file              "/var/run/mpd/mpd.pid"
state_file            "/var/lib/mpd/mpd.state"

user                  "mpd"

bind_to_address     "192.168.1.3"
port                "6660"

input {
    plugin  "curl"
}

audio_output {
    type        "httpd"
    name        "Radiodino - 320"
    encoder     "vorbis"
    port        "8434"
    bitrate     "320"
    format      "44100:16:2"
}

filesystem_charset    "UTF-8"

Bon dans mon cas, 192.168.1.3, c’est l’adresse de mon serveur. Le port 8434 (un peu compliqué), c’est le port où les gens vont écouter ma radio. L’encodage vorbis, c’est un encodage qui est reconnu sauf par iTunes, dans ce cas, il faudra mettre l’encodage lame (si vous avez bien mis lame dans les options de compilation), mais vorbis c’est plus mieux (on entend les boums boums quand la musique fait bam bam. Le bitrate 320, c’est la qualité de votre musique, mais là aussi il faut bien choisir car plus votre musique est high-quality, plus il faudra une super bonne connexion pour écouter votre musique. Dans mon cas, j’ai mis en place plusieurs niveau de qualité (128, 192 et 320) pour ceux qui ont une bonne/mauvaise connexion.

Note quand même que si vous avez spécifié un utilisateur mpd, vérifier bien les droits (notamment dans les fichiers qui sont dans le /var/).

Il n’y a plus qu’à mettre votre musique dans /home/mpd/music/, de faire un update à l’aide de ncmpcpp ou mpc (oui, la dernière version de MPD n’inclut plus l’option –create-db et il faut faire un update de votre base de données à l’aide des utilitaires désigner ci-dessus). Et ensuite, une configuration du NAT de votre *box (genre 8434 vers 192.168.1.3:8434, bon après, c’est selon votre *box) et à partir d’un autre réseau (comme celui de votre voisin que vous avez cracker) VLC – Ouvrir un flux – http://mon_ip_public:8434/ ! Ouai, c’est votre musique que vous écouter. Alors, vous pouvez le faire en local aussi en tapant l’IP local de votre serveur et le port mais le kiffe, c’est pouvoir écouter sa musique n’importe où !

Bon, on peut très bien optimiser la chose, la bête … On peut par exemple, dans mon cas, mettre en place un serveur SFTP pour qu’on puisse ce connecter de partout sur le dossier /home/mpd/music tout en chrootant la chose et invité d’autres utilisateurs à mettre leurs musiques. C’est une configuration un peu plus complexe (notamment le chroot qui est super chiant) néanmoins pratique si vos potes on des supers musiques ! Bon, sinon, vous pouvez écouter ma radio à ces adresses :

Si vous n’y arriver pas, c’est que j’ai eu une coupure de courant et j’ai pas penser à rallumer mon serveur … Sinon, si vous aussi, vous voulez participer à l’élaboration d’un discothèque qualité FLAC gratuitement et de façon sécuriser pour remplir mon coeur de joie et mon serveur plein de musiques, vous pouvez me contacter par E-Mail et je peux vous offrir un identifiant et un mot de passe pour une connexion SFTP sur mon serveur.