Bonjour à tous.
Depuis quelques temps, je cherche à voir s'il serait possible d'exprimer le fonctionnement du typage utilisé par un langage fonctionnel puissant comme Haskell dans un langage impératif relativement proche de la machine, comme C++.
Je ne cherche pas ici à faire du beau C++, mais à utiliser au mieux les possibilités de ce langage pour se rapprocher le plus possible du fonctionnement des types en Haskell.
Alors voici ce à quoi je suis arrivé jusqu'à présent, et là où je bloque.
Types sommes
Le principe d'un type somme, c'est qu'un type peut prendre un nombre fini de valeurs. Par exemple, en Haskell, on pourra l'exprimer ainsi.
1 | data FeuTricolore = Rouge | Orange | Vert |
Pas vraiment de difficulté pour transposer ça en C++, ça correspond directement à une énumération.
1 | enum FeuTricolore { Rouge, Orange, Vert }; |
Types produits
Un type produit consiste à « emballer » un ou plusieurs champs dans le nouveau type, comme dans l'exemple qui suit.
1 | data Point = Point Double Double |
Si je vous dit que le Point
après le =
est appelé un constructeur, je pense que vous conviendrez que la transposition en C++ tombe sous le sens.
1 2 3 4 5 6 7 8 9 | class Point { protected: double _x, _y; public: Point(double x, double y) : _x(x), _y(y) { } }; |
Le Haskell offre également ce que l'on appelle la « syntaxe d'enregistrement », qui permet de donner un nom aux champs du type produit, et crée automatiquement des fonctions portant le même nom pour y accéder.
1 | data Point = Point { x :: Double, y :: Double } |
En Haskell, ces fonctions accesseurs sont dans l'espace de nom général, mais tout le monde s'accorde à dire que c'est plus une faiblesse qu'autre chose, donc on va plutôt en faire des méthodes en C++.
1 2 3 4 5 6 7 8 9 10 11 12 | class Point { protected: double _x, _y; public: Point(double x, double y) : _x(x), _y(y) { } double x() { return _x; } double y() { return _y; } }; |
Là où ça devient un peu plus tordu, c'est que le constructeur n'a pas nécessairement besoin de porter le même nom que son type. Par exemple, ce code est tout à fait valable en Haskell.
1 2 | data DroitePoints = DPts Point Point -- Une droite définie par deux points par lesquels elle passe. |
C'est là qu'on commence à faire du C++ dégueulasse…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class DroitePoints { protected: Point& _a, _b DroitePoints(const Point& a, const Point& b) : _a(a), _b(b) { } }; class DPts : public DroitePoints { public: DPts(const Point& a, const Point& b) : DroitePoints(a, b) { } }; |
Types algébriques
On rentre dans le vraiment rigolo : un type algébrique est une somme de types produits et/ou de valeurs spécifiques. Voici un exemple.
1 2 | data Quidonc = Personne | Quelquun String -- Soit on n'est personne, soit on a un nom |
On pourrait envisager de faire de Personne
une variable globale pré-déclarée, instance de Quelquun, avec nullptr
en guise de contenu, mais ça ne marcherait pas si le contenu doit être un int
ou un char
, par exemple. Du coup, on peut tenter ça à la place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Quidonc { protected: Quidonc(); }; class Personne : public Quidonc { public: Personne() { // Insérer code pour en faire un singleton. } }; class Quelquun : public Quidonc { private: std::string _nom; public: Quelquun(const std::string& nom) : _nom(nom) { } }; |
Types paramétrés
Ça commence à devenir puissant. Un type paramétré, c'est un type produit dont on ne définit pas à l'avance le type du contenu. Par exemple…
1 | data Boite a = Boite a |
Ce a
peut-être n'importe quel autre type. Pour transposer ça en C++, j'ai évidemment pensé aux templates.
1 2 3 4 5 6 7 8 9 | template <typename T> class Boite { protected: T _a; public: Boite(T a) : _a(a) { } }; |
Mélangé aux types algébriques, ça peut devenir plus ardu. Voici l'exemple classique d'un type algébrique et paramétré en Haskell.
1 | data Maybe a = Nothing | Just a |
La meilleure transposition à laquelle j'arrive en C++, c'est la suivante.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | template <typename T> class Maybe { protected: Maybe() { } }; template <typename T> class Nothing : public Maybe { public: Nothing() { } }; template <typename T> class Just : public Maybe { protected: T _a; public: Just(T a) : _a(a) { } }; |
La limite de ce système, c'est qu'en Haskell, il n'y a qu'un seul Nothing
, qui sera reconnu comme étant de n'importe quel type Maybe queqchose
. Alors qu'avec le code que j'ai là, il faudra spécifier Nothing<int>
ou Nothing<MyClass>
.
Est-ce que je me trompe ? Est-ce qu'il est possible de déclarer la classe Nothing
comme héritant de Maybe<T>
sans la paramétrer pour autant ? Est-ce que, le cas échéant, un objet de type Nothing
sera reconnu aussi bien comme un Maybe<int>
que comme un Maybe<std::string>
? Est-ce que je passe à côté d'un outil du C++ qui permettrait de mieux imiter ce comportement du Haskell ?
Classes de types
Attention à la confusion de sens ! Une classe de types en Haskell, c'est ce que l'on appellerait une interface en POO : un type appartient à une classe donnée s'il implémente les fonctions que cette classe définit. C'est le seul moyen existant en Haskell de surcharger des fonctions existantes.
1 2 3 4 5 6 | class Show a where show :: a -> String instance Show Bool where show True = "True" show False = "False" |
Une des classes de base du langage : si un type l'implémente, il est possible d'afficher des variables de ce type dans la console. La transposition logique en C++ serait une classe virtuelle pure.
1 2 3 4 | class Show { public: virtual std::string show() = 0; }; |
Cependant, je vois un problème par rapport au comportement du Haskell. En effet, ce langage permet d'implémenter un comportement générique pour la fonction, ce qui fait qu'il n'est pas nécessaire que toutes les instances de la classe définissent elles-mêmes le comportement. On peut par exemple faire plutôt ce qui suit.
1 | data Bool = True | False deriving (Show) |
Vous allez me dire, il suffit que la class Show
ne soit pas purement virtuelle. J'acquiesce. Cependant, le Haskell permet d'écrire des comportement génériques, qui sont interdépendants. Voyez par exemple la classe qui définit des relations d'égalité ou non.
1 2 3 | class Eq a where (==) v1 v2 = if v1 /= v2 then False else True (/=) v1 v2 = if v1 == v2 then False else True |
De cette manière, les types instances de cette classe peuvent ne définir qu'une seule des deux fonctions, et la seconde fonctionnera quand même, mais il faut quand même en définir au moins une.
Si je ne dis pas de bêtise, obtenir ce comportement en C++ n'est pas possible ?
Contraintes de classe
C'est là le deuxième intérêt majeur des classes de types en Haskell : à tout moment, quand on manipule un type paramétré, on peut mettre des contraintes sur les types qui peuvent être passés en paramètre. Voyez plutôt.
1 2 3 4 5 6 7 8 9 10 | class Bits a where xor b1 b2 :: a -> a -> a -- On va faire simple, en vrai, elle est beaucoup plus fournie data MaybeBits a where NothingBits :: Bits a => MaybeBits a JustBits :: Bits a => a -> MaybeBits a -- Le type Char n'étant pas membre de la classe Bits, on -- ne pourra pas créer de MaybeBits Char, alors qu'on pouvait -- faire Maybe Char |
Je ne trouve pas de moyen satisfaisant d'imiter ce fonctionnement en C++… Est-ce qu'il existe un moyen d'exiger que le T
de template <typename T>
soit instance d'une certaine classe ? Existe-t-il un autre moyen dont je n'aurais pas connaissance d'obtenir ce comportement ?
Conclusion
Il y a aussi les types récursifs, mais ils ne posent pas de difficulté à implémenter en C++. À priori, j'ai fait le tour de ce que je cherche à obtenir. Évidemment, j'ai mis en lumière les endroits où je bloque, mais que cela ne vous empêche pas de faire des remarques sur le reste, si vous pensez que je m'y suis mal pris quelque part.
Et pour être sûr que vous n'avez pas oublié depuis le début de ce message…
Je ne cherche pas ici à faire du beau C++, mais à utiliser au mieux les possibilités de ce langage pour se rapprocher le plus possible du fonctionnement des types en Haskell.