Les langages qui sux

Bonjour,

Comme vous le savez, le dada, c'est ma grande passion™, et c'est pourquoi j'ai décidé de vous faire part d'une pensée rectum sur les différents types de langages, afin de définir ce qu'est un langage qui sux (de manière totalement objective bien sur dans le sens où je détiens la vérité absolue).

Je vais prendre pour exemple le python, qui est un langage que je suis en train d'apprendre, complètement orienté objet, de très haut niveau, présentant de nombreux avantages mais aussi de nombreux inconvénients pour un amoureux du langage de bas niveau qu'est le C que je suis (formule volontairement mal tournée). Je le mettrai en opposition au langage C (voir C++ si y a des éléments concernant la POO que je souhaite critiquer (sachant que le C++ est aussi critiquable (oui je ne sais pas de quoi je vais parler dans ce billet, je suis super organisé))).

Le typage

Un langage qui sux est par définition un langage qui ne déclare pas explicitement le type des variables.

Ce qui fait qu'on se retrouve à "déclarer" (entre guillemets parce qu'il n'y a pas véritablement de déclaration) une variable de cette façon :

 
a = 1
 

Ce n'est pas gênant, ce qui l'est plus c'est pour le prototype des fonctions :

 
def __init__(self, servers, nickname, altnickname):
 

C'est une fonction membre d'une classe (un constructeur même, on le reconnaît à son nom (__init__)). Le problème est que pour l'utilisateur de la classe en question, on a AUCUNE idée du type de variable attendu. Alors là on peut deviner sans trop de mal que nickname et altnickname sont des chaînes.
Cependant pour servers on peut avoir un gros doute. Le 's' insinue qu'on en attends plusieurs, donc c'est probablement un tableau. Mais un tableau de quoi ? Au début on penserait à une chaîne, mais en fait il s'agit d'un tableau de tuple (un tuple est un couple, et en l'occurrence c'est une chaîne (l'hostname) et un entier (le port)).

Vous me direz "oui mais il faut que l'auteur d'une classe la documente". Ok, sauf que si ce n'est pas documenté là il y a un gros problème. Dans un langage qui rox, en C par exemple, le code est lui même une documentation :

 
typedef struct {
	const char hostname[HOSTLEN];
	unsigned int port;
} server_t;
 
void __init__(server_t servers[], const char* nickname, const char* altnickname);
 

C'est quand même plus explicite non ? Bon vous me direz par contre que pour faire passer un tableau à cette fonction __init__(), il faudra d'abord définir une variable de type server_t[] en l'initialisant avec les différentes valeurs, et soit mettre une case nulle à la fin, ou alors passer la taille à la fonction __init__(), alors qu'en python il suffit de faire :

 
__init__([("irc.freenode.org", 6667), ("eu.undernet.org", 6667)], "Progs", "Progs`")
 

Mais bon, au moins en C il n'y a pas de confusion possible !

Interface

Caractéristique d'un langage qui sux (on retrouve ça en Javasapumaismoinsdepuisquec'estlibremaisçapueencorepasmal par exemple (Erratum: il semblerait que le java ait également une interface. Donc ok le javasapu mais y a des interfaces. Du coup je vais citer le python), c'est le fait que l'interface ne soit pas séparé de l'implémentation.
Ceci concerne donc les langages orientés objets (mais aussi dans un langage procédural comme le C où le programme est décomposé en "modules" (les fichiers), le .h contenant les prototypes et les structures de données est l'interface et le .c contient l'implémentation (avec même la possibilité d'une pseudo encapsulation avec les fonctions statiques)).

Encore dans l'optique qu'un vrai langage est un langage qui s'auto documente, je considère que regarder l'interface d'une classe est la meilleurs documentation qui soit (enfin après il faut le commentaire qui va bien au dessus pour être plus explicite sur ce que fait la fonction, ce qu'elle retourne, des précisions sur les arguments si nécessaire, etc (mais même sans ça c'est toujours plus explicite qu'un prototype python (ce qui n'existe pas, je vais le dire après))).
C'est pourquoi il est très important pour moi de bien séparer l'interface de l'implémentation, pour pouvoir voir d'un trait l'ensemble de la classe, les différentes méthodes, les attributs, etc.
Pour une meilleurs lisibilité, je décompose mes interfaces ainsi :

 
class MaClasse
{
/* Constructors */
public:
 
    MaClasse();
    ~MaClasse();
 
/* Methodes */
public:
 
   void Methode();
 
/* Attributes */
public:
 
    int Value() const;
    void SetValue(int);
 
/* Private variables */
private:
 
    int value;
};
 

Cette séparation permet de bien différencier les constructeurs, méthodes, attributs et variables privées, ces dernières arrivant à la fin car normalement l'utilisateur de la classe n'a PAS à avoir connaissance de celles-ci. Répéter à chaque fois "public" ou "private" permet de lire directement une "section" sans avoir à connaître les permissions de la section précédente (bon c'est valable que dans les grandes classes, et à priori, si cette organisation est respectée partout, ben tout le début est public et la fin private (et en plus le commentaire l'indique).
Après bien sur il faut également rajouter quelques commentaires à chaque fonction (pas superflu) pour avoir une idée de ce que fait la fonction (bien sur en l'occurrence on a bien saisis l'idée). Un commentaire également juste avant la déclaration de la classe permet de savoir à quoi elle sert, voir comment elle marche (enfin bon suffit de respecter le style doxygen pour avoir une bonne documentation).
Bon ceci est mon organisation personnelle, j'imagine que je dois être le seul à faire ainsi, mais je trouve que c'est bien plus clair pour bien discerner les différents types de fonctions/variables.

Le problème avec le langage qui sux, c'est que n'ayant pas de séparation entre l'interface et l'implémentation, il est impossible d'avoir une vue d'ensemble d'une classe (à part avec des outils de génération de documentation). Il est d'ailleurs impossible, surtout si l'implémentation de la classe est assez grosse, de connaître facilement ses membres/attributs si il n'y a pas de documentation (à part avec des commandes python, mais bon encore une fois c'est un peu chiant, et en plus on obtient que le nom je crois, et pas le prototype ou une carte complète).

Bref, en python, adieu les jolis en-têtes documentés pour doxygen de la STL du C++, obligé d'ouvrir un prompt python, d'importer la lib en question, et de taper "doc jesaismemepasquoi". Alors ok le python addict a toujours un prompt python ouvert, c'est même son terminal secondaire, il lance ses applications avec, mais le C++ addict lui il aime son zsh et son nano /usr/include/* !

Références

Les références, c'est le mal. Une référence c'est un pointeur "sécurisé", sauf que l'on accède directement à la valeur, c'est une sorte d'alias.

Le problème avec les références, c'est que lorsqu'on lit du code, on ne sait pas si cette variable est une référence ou pas.

Regardons le code suivant :

 
int a = 0;
sodomie(&a);
prout(a);
 

La fonction sodomie demande très manifestement un int*, on sait donc qu'elle risque de modifier l'entier (à moins que ça soit un const int*, mais là il n'y a que peu d'intérêt (à la limite pour éviter la copie sur des objets plus gros)).
Par contre, la fonction prout, elle, on ne sait pas si elle demande un int ou un int& (référence), du coup on est dans le flou.
Le problème des références n'est pas dans l'écriture du code (ok c'est plus facile), mais dans la relecture (lorsqu'on a un peu oublié) où il faut voir la définition pour savoir ce que c'est.

De manière similaire, si un attribut est une référence, dans l'implémentation on peut être trompé encore une fois, il est nécessaire de regarder la définition de la classe.

Les pointeurs sont peut être un peu plus lourds niveau lisibilité, mais au moins c'est explicite. D'autant plus qu'on peut rajouter un & par inadvertance après un type, ça ne change *rien* à la compilation, et ça peut introduire des bugs pas forcément visibles tout de suite.

L'avantage de la référence est sa sécurité, le fait que si jamais la variable sur laquelle il pointe est libérée, celui-ci aura la une valeur nulle (aucun crash (tout du moins avec un int, je ne sais pas ce que ça donne avec une référence vers une classe, je n'ai pas testé)).

Bref, préférer les pointeurs, c'est vouloir savoir à chaque ligne ce qu'il se passe, contrôler son code !

Le cas de python

Le python a la particularité que toute variable est une référence.

Par exemple :

 
>>> a = [1,2]
>>> b = a
>>> a.append(3)
>>> b
[1, 2, 3]
 

L'avantage est que l'on sait que tout est référence (à part quelques types, les scalaires et autres), donc on sait à quoi s'attendre.

Le truc est qu'il faut faire gaffe que dans une fonction du type :

 
def prout(a):
     a += 1
     return a
 
b = 2
c = prout(b)
 

Ben b aura la même valeur que c. Bon ce cas là est un peu tiré par les cheveux, mais bon il n'est pas rare de rencontrer une fonction qui modifie par commodité un argument (pour un indice ou autre).

Bref, les références, c'est nul.

Les espaces de nom

Les espaces de noms sont là pour avoir des espaces pour chaque partie d'un programme (ou pour une librairie particulière). En C++, std est l'espace de nom de la librairie standard STL.
Le problème est que du coup ce n'est pas pleinement lisible :

 
#include <iostream>
#include <string>
#include <vector>
 
int main()
{
    std::vector<std::string> kikoo;
    kikoo.push_back("hello");
    kikoo.push_back("world");
 
    std::cout << kikoo << std::endl;
 
    return 0;
}
 

Je ne suis pas contre les espaces de nom, je trouve ça sympa, sauf que je ne m'en sers jamais pour mes propres applications. À mon avis l'utilité réside surtout pour les librairies, ou alors les très gros programmes.

Langage interprété

Le python est un langage interprété. Le problème est qu'un grand nombre d'erreurs n'est plus détecté à la compilation mais bien à l'exécution, au moment où la parcelle de code concernée est exécutée !

Par exemple si jamais on fait un typo dans le nom d'une fonction que l'on appelle dans un bloc qui n'est appelé que 1% du temps, ben on risque de ne découvrir ce bug qu'une fois le programme en production. Au risque de paraphraser haypo, il semblerait qu'un programme écrit en python atteigne un niveau de stabilité élevé bien plus tard qu'un programme équivalent en C++.

Le temps gagné à développer dans ce langage est très largement perdu en debugging à mon avis.. Et c'est dommage. Bien heureusement, des outils existent pour détecter un grand nombre d'erreurs sans avoir à exécuter.

Notez que le code suivant :

 
def prout(a):
    return pipi(a)
 
def chiasse():
    pipi = lambda e: e + 1
 

En gros, tant que chiasse() n'a pas été appelé, appeler prout() provoquera une erreur. L'appeler après par contre marchera. Ceci montre bien qu'il y a des cas difficilement détectables.

Conclusion

En conclusion, il y a pas mal de choses qui font que je préfère les langages de bas niveau comme le C (malgré que celui-ci ne possède pas l'orientation objet). Donc mon favoris est le C++ malgré ses quelques défauts.

Je pense que je pourrais encore trouver d'autres problèmes, mais bon je ne connais pas encore pleinement le python, et j'ignore le java (qui doit avoir un paquet de gros défauts aussi).

L'avantage des langages de haut niveau est qu'on n'a plus à se préoccuper de la mémoire, et qu'on se concentre sur l'algorithme que l'on écrit.
Mais justement j'aime savoir exactement ce que le programme fait.

Romain.

PS: Info de dernière minute : on me signale dans l'oreille qu'en php on ne peut pas faire de surcharge de fonctions, et qu'on ne peut faire un heritage multiple que si ce sont des classes abstaites. Donc le PHP, ça sux !

3 Responses to “Les langages qui sux”


  • En PHP on peut faire de la surcharge mais c’est une énorme bidouille.
    De toute façon en PHP tout est un peu comme ça, les développeurs essaient de rendre ce langage plus propre et logique, mais il faut aussi garder la compatibilité…
    Personnellement, je trouve que le paradigme « variable = référence » est génial. Combien de bugs de pointeurs as-tu déjà eu ? Par contre tu as raison dans ton exemple, c’est une erreur souvent commise par les débutants. Il y en a des encore plus sournoises en objet (facilement évitables en Java si on pense à utiliser la méthode clone).
    Pour les espaces de nom, il n’y a pas de moyen de les « importer » en C ?
    Enfin, toute ta partie sur les interfaces me semble plus un problème d’habitude.
    En Java, les interfaces ce n’est pas vraiment ce que tu veux ; une interface ne correspond pas à une classe, mais une classe peut implémenter une interface (ou plusieurs).
    Mais ce qu’on peut faire, en Java, Ruby ou plein d’autres langages (comme… le C et C ), c’est générer une documentation à partir du code et de ses commentaires. Je trouve ça bien plus pratique et lisible. Le côté « la doc est dans le code », c’est à mon avis quelque chose à éviter. Et de manière générale, c’est lourd et inutile de se taper des .h.
  • J’avais l’impression que tout comme le C, le C est restée assez bas niveau. Cela étant, je ne suis pas un théoricien des langages informatiques, préférant de loin voir des applications concrètes à des problèmes concrets.

    Pour ce qui est des héritages multiples (au sens « A hérite de B et C » et non au sens « A hérite de B, B hérite de C »), il est vivement recommandé de ne pas en faire, préférant découper le code en héritages simples (« A hérite de B, B hérite de C »), évitant ainsi des ambiguités. Le seul inconvénient de l’absence d’héritages multiples est un code plus long, mais il n’existe aucun autre inconvénient, à ma connaissance, du moins, alors que les avantages sont réels (moins d’ambiguité implique un code mieux maîtrisé et plus stable, donc des délais de déboguage, donc de développement, et par conséquent des coûts moindres).

Leave a Reply




Bear