Considérons une situation où le type et la classe existent tous deux dans le même programme. Le type peut être une instance de la classe, mais c'est plutôt trivial. Ce qui est plus intéressant, c'est que vous pouvez écrire une fonction fromProblemClass :: (CProblem p s a) => p s a -> TProblem s a
.
Le remaniement que vous avez effectué est à peu près équivalent à un inlining manuel. fromProblemClass
partout où vous construisez quelque chose qui sert de CProblem
et faire en sorte que chaque fonction qui accepte une instance CProblem
au lieu d'accepter TProblem
.
Puisque les seules parties intéressantes de cette refactorisation sont la définition de TProblem
et la mise en œuvre de fromProblemClass
Si vous pouvez écrire un type et une fonction similaires pour n'importe quelle autre classe, vous pouvez également la remanier pour éliminer complètement la classe.
Quand cela fonctionne-t-il ?
Pensez à la mise en œuvre de fromProblemClass
. En fait, vous appliquerez partiellement chaque fonction de la classe à une valeur du type instance et, ce faisant, vous éliminerez toute référence à la classe p
(qui est ce que le type remplace).
Toute situation où le remaniement d'une classe de type est simple va suivre un modèle similaire.
Quand cela est-il contre-productif ?
Imaginez une version simplifiée de Show
avec seulement le show
fonction définie. Cela permet le même remaniement, en appliquant show
et en remplaçant chaque instance par... un String
. Il est clair que nous avons perdu quelque chose ici - à savoir, la possibilité de travailler avec les types originaux et de les convertir en un String
à différents moments. La valeur de Show
est qu'il est défini sur une grande variété de types non liés.
En règle générale, s'il existe de nombreuses fonctions différentes spécifiques aux types qui sont des instances de la classe, et que celles-ci sont souvent utilisées dans le même code que les fonctions de la classe, retarder la conversion est utile. S'il y a une ligne de démarcation nette entre le code qui traite les types individuellement et le code qui utilise la classe, les fonctions de conversion peuvent être plus appropriées, la classe de type étant une commodité syntaxique mineure. Si les types sont utilisés presque exclusivement par les fonctions de la classe, la classe de type est probablement complètement superflue.
Quand cela est-il impossible ?
Par ailleurs, le remaniement est similaire à la différence entre une classe et une interface dans les langages OO ; de même, les classes de type pour lesquelles ce remaniement est impossible sont celles qui ne peuvent pas être exprimées directement du tout dans de nombreux langages OO.
Plus précisément, quelques exemples de choses que vous ne pouvez pas traduire facilement, voire pas du tout, de cette manière :
-
Le paramètre de type de la classe apparaissant uniquement en position covariante comme le type de résultat d'une fonction ou comme une valeur non fonctionnelle. Les principaux contrevenants sont mempty
para Monoid
y return
para Monad
.
-
Le paramètre de type de la classe apparaît plus d'une fois dans le type d'une fonction. ne rend pas la chose vraiment impossible, mais elle complique sérieusement les choses. Les principaux contrevenants sont Eq
, Ord
et pratiquement toutes les classes numériques.
-
Utilisation non triviale de types supérieurs dont je ne suis pas sûr de pouvoir préciser les détails, mais (>>=)
para Monad
est un contrevenant notable dans ce domaine. D'autre part, le p
dans votre classe n'est pas un problème.
-
Utilisation non triviale des classes de type multi-paramètres Je ne suis pas non plus certain de savoir comment la définir et elle devient horriblement compliquée en pratique de toute façon, étant comparable à la distribution multiple dans les langages OO. Encore une fois, votre classe n'a pas de problème ici.
Notez que, compte tenu de ce qui précède, cette refactorisation n'est même pas posible pour la plupart des classes de type standard, et serait contre-productive pour les quelques exceptions. Ce n'est pas une coïncidence :]
Qu'est-ce que vous abandonnez en appliquant ce remaniement ?
Vous renoncez à la capacité de distinguer les types originaux. Cela semble évident, mais c'est potentiellement significatif - s'il existe des situations où vous vraiment Si vous avez besoin de contrôler lequel des types d'instance de la classe originale a été utilisé, l'application de ce remaniement perd un certain degré de sécurité de type, que vous ne pouvez récupérer qu'en passant par le même genre de cerceaux utilisés ailleurs pour assurer les invariants au moment de l'exécution.
Inversement, s'il y a des situations où vous avez vraiment besoin de rendre les différents types d'instance interchangeable --L'emballage alambiqué que vous avez mentionné en est un symptôme classique : vous gagnez beaucoup à vous débarrasser des types originaux. C'est le plus souvent le cas lorsque vous ne vous souciez pas vraiment des données originales en elles-mêmes, mais plutôt de la façon dont elles vous permettent d'opérer sur d'autres données ; ainsi, l'utilisation directe des enregistrements de fonctions est plus naturelle qu'une couche supplémentaire d'indirection.
Comme indiqué plus haut, cela est étroitement lié à la POO et au type de problèmes auxquels elle est le mieux adaptée, et représente l'"autre côté" du problème de l'expression par rapport à ce qui est typique des langages de type ML.