Je vais essayer d'expliquer rapidement, mais il y a des ouvrages entiers sur la question. Si l'on souhaite vraiment comprendre il faut aller plus loin.
Héritage
Donc "l'héritage", c'est récupérer du code écrit ailleurs. C'est pas bien différent d'un simple "#include" (après l'implémentation peut-être plus complexe qu'une simple substitution de texte, mais c'est un détail).
Sous-typage
Le sous-typage, c'est déterminer quand tu as le droit d'utiliser un objet à la place d'un autre (le contexte attend un X mais toi tu peux fournir un Y : est-ce correct ou pas ?).
Un petit exemple en supposant que B est un sous-type de A (peu importe comment la relation "être sous-type de" est définie dans le langage).
Tu implémentes une méthode qui renvoie un A (parce que tu hérites d'une classe qui a défini cette méthode comme renvoyant A). Tu as le droit de renvoyer éventuellement un peu plus (un type plus précis), un B est acceptable :
- pour les utilisateurs de la classe héritée, ils auront accès au type le plus précis, ils pourront profiter des fonctionnalités de B ;
- pour les utilisateurs qui accède à la méthode par la classe parente, ils attendent un A, mais il est correct de considérer cette valeur de type B que tu as renvoyé au type A, ils sont contents.
A contrario, si cette méthode reçoit un argument de type B mais dont tu n'utilises que les méthodes définies dans A, tu peux oublier que c'est un B et être plus général en ne demandant qu'un A. Le raisonnement ci-dessus s'applique dans l'autres sens (les utilisateurs de la classe parente devront toujours fournir un B, c'est pas grave, les utilisateurs de la classe fille auront la privilège de n'avoir qu'un A à fournir).
Donc dans certains contextes pour être un sous-type, il faut qu'un certain type soit plus précis, dans d'autre il faut qu'il soit plus général.
Les contextes il faut être plus précis sont dits covariants, car le contexte varie dans le même sens que le type (je suis plus préçis si mon type de retour est plus préçis).
Les contextes il faut être plus général sont dits contravariants, car le contexte varie dans le sens opposé à ce type (je suis plus préçis si mon argument l'est moins).
Plus généralement, pour être plus "fort", il faut consommer moins d'informations et en renvoyer d'avantage.
Sous-typage nominal
Jusque là c'est de la logique, un dialogue entre appelants et appelés dans les langages de programmations.
La logique permet de distinguer ce qui a du sens ou pas. On pourrait se dire qu'un langage de programmation peut n'en faire qu'à sa tête et suivre d'autres règles, mais on ne peut vraiment y échapper, ça conduirait à des "Runtime Type Error" : pour être correct il faut au moins faire ça.
Si l'on considère que l'héritage définit le sous-typage (on dit sous-typage nominal parce que pour savoir si une classe est sous-type d'une autre, on prend les noms des deux classes et on regarde où ils se trouvent dans le diagramme d'héritage), on va se retrouver dans une situation pas très confortable:
1) Pour commencer, la relation de sous-typage est très grossière, ce sont juste des noms. On voudrait peut-être quelque chose de plus fin, ben on peut pas. Enfin des fois c'est mieux de ne pas donner trop d'outils au développeur.
2) Comme héritage et sous-typage coïncide, on ne peut pas réutiliser de code sans récupérer toutes les contraintes énoncées au dessus.
3) Ou alors on décide d'ignorer les contraintes. Les systèmes de types des langages objets ne sont en général pas très fins pour commencer, donc c'est pas très grave on peut s'en sortir un certain temps sans être amené à des incohérences (au pire on "cast").
Notez que ce n'est pas vraiment une solution recevable : comme on a sous-spécifié le problème, il y a plus de solutions. L'aboutissement est un langage "typé dynamiquement": avec un seul type, on ne risque pas de créer d'incohérences.
Cela conduit évidemment a des désastres, on encourage donc quand même informellement les règles logiques ci-dessus avec le principe de substitution de Liskov.
Sous-typage structurel
En OCaml il n'y a pas de cast, donc on ne peut pas contourner les contraintes logiques. Par contre son système de type (le langage utilisé pour la spécification logique) est suffisamment expressif pour être utilisable sans ces casts: on se retrouve parfois à devoir spécifier précisément, ou bien face à ses propres incohérences et cela aide beaucoup à concevoir une hiérarchie d'objets pas bancale (quitte à la traduire dans un autre langage a posteriori).
Enfin plutôt que de se restreindre à des noms, OCaml utilise le sous-typage structurel: le sous-typage ne va considérer que les parties d'un objet (représentées par une liste de méthodes) utilisé par un code pour déterminer les contraintes qui s'appliquent. Cette pratique est informellement appelée "duck-typing".
Le principe de substitution sera respecté par construction, à la granularité de spécification que permet le langage de type d'OCaml.
[^] # Re: Reason
Posté par Def . En réponse à la dépêche OCaml 4.04 et 4.05. Évalué à 7.
Je vais essayer d'expliquer rapidement, mais il y a des ouvrages entiers sur la question. Si l'on souhaite vraiment comprendre il faut aller plus loin.
Héritage
Donc "l'héritage", c'est récupérer du code écrit ailleurs. C'est pas bien différent d'un simple "#include" (après l'implémentation peut-être plus complexe qu'une simple substitution de texte, mais c'est un détail).
Sous-typage
Le sous-typage, c'est déterminer quand tu as le droit d'utiliser un objet à la place d'un autre (le contexte attend un X mais toi tu peux fournir un Y : est-ce correct ou pas ?).
Un petit exemple en supposant que B est un sous-type de A (peu importe comment la relation "être sous-type de" est définie dans le langage).
Tu implémentes une méthode qui renvoie un A (parce que tu hérites d'une classe qui a défini cette méthode comme renvoyant A). Tu as le droit de renvoyer éventuellement un peu plus (un type plus précis), un B est acceptable :
- pour les utilisateurs de la classe héritée, ils auront accès au type le plus précis, ils pourront profiter des fonctionnalités de B ;
- pour les utilisateurs qui accède à la méthode par la classe parente, ils attendent un A, mais il est correct de considérer cette valeur de type B que tu as renvoyé au type A, ils sont contents.
A contrario, si cette méthode reçoit un argument de type B mais dont tu n'utilises que les méthodes définies dans A, tu peux oublier que c'est un B et être plus général en ne demandant qu'un A. Le raisonnement ci-dessus s'applique dans l'autres sens (les utilisateurs de la classe parente devront toujours fournir un B, c'est pas grave, les utilisateurs de la classe fille auront la privilège de n'avoir qu'un A à fournir).
Donc dans certains contextes pour être un sous-type, il faut qu'un certain type soit plus précis, dans d'autre il faut qu'il soit plus général.
Les contextes il faut être plus précis sont dits covariants, car le contexte varie dans le même sens que le type (je suis plus préçis si mon type de retour est plus préçis).
Les contextes il faut être plus général sont dits contravariants, car le contexte varie dans le sens opposé à ce type (je suis plus préçis si mon argument l'est moins).
Plus généralement, pour être plus "fort", il faut consommer moins d'informations et en renvoyer d'avantage.
Sous-typage nominal
Jusque là c'est de la logique, un dialogue entre appelants et appelés dans les langages de programmations.
La logique permet de distinguer ce qui a du sens ou pas. On pourrait se dire qu'un langage de programmation peut n'en faire qu'à sa tête et suivre d'autres règles, mais on ne peut vraiment y échapper, ça conduirait à des "Runtime Type Error" : pour être correct il faut au moins faire ça.
Si l'on considère que l'héritage définit le sous-typage (on dit sous-typage nominal parce que pour savoir si une classe est sous-type d'une autre, on prend les noms des deux classes et on regarde où ils se trouvent dans le diagramme d'héritage), on va se retrouver dans une situation pas très confortable:
1) Pour commencer, la relation de sous-typage est très grossière, ce sont juste des noms. On voudrait peut-être quelque chose de plus fin, ben on peut pas. Enfin des fois c'est mieux de ne pas donner trop d'outils au développeur.
2) Comme héritage et sous-typage coïncide, on ne peut pas réutiliser de code sans récupérer toutes les contraintes énoncées au dessus.
3) Ou alors on décide d'ignorer les contraintes. Les systèmes de types des langages objets ne sont en général pas très fins pour commencer, donc c'est pas très grave on peut s'en sortir un certain temps sans être amené à des incohérences (au pire on "cast").
Notez que ce n'est pas vraiment une solution recevable : comme on a sous-spécifié le problème, il y a plus de solutions. L'aboutissement est un langage "typé dynamiquement": avec un seul type, on ne risque pas de créer d'incohérences.
Cela conduit évidemment a des désastres, on encourage donc quand même informellement les règles logiques ci-dessus avec le principe de substitution de Liskov.
Sous-typage structurel
En OCaml il n'y a pas de cast, donc on ne peut pas contourner les contraintes logiques. Par contre son système de type (le langage utilisé pour la spécification logique) est suffisamment expressif pour être utilisable sans ces casts: on se retrouve parfois à devoir spécifier précisément, ou bien face à ses propres incohérences et cela aide beaucoup à concevoir une hiérarchie d'objets pas bancale (quitte à la traduire dans un autre langage a posteriori).
Enfin plutôt que de se restreindre à des noms, OCaml utilise le sous-typage structurel: le sous-typage ne va considérer que les parties d'un objet (représentées par une liste de méthodes) utilisé par un code pour déterminer les contraintes qui s'appliquent. Cette pratique est informellement appelée "duck-typing".
Le principe de substitution sera respecté par construction, à la granularité de spécification que permet le langage de type d'OCaml.