Quels langages typés statiquement prennent en charge les types d'intersection pour les valeurs de retour de fonction?


17

Note initiale:

Cette question a été fermée après plusieurs modifications car je n'avais pas la terminologie appropriée pour énoncer avec précision ce que je cherchais. Sam Tobin-Hochstadt a ensuite posté un commentaire qui m'a fait reconnaître exactement ce que c'était: des langages de programmation qui prennent en charge les types d'intersection pour les valeurs de retour de fonction.

Maintenant que la question a été rouverte, j'ai décidé de l'améliorer en la réécrivant d'une manière (je l'espère) plus précise. Par conséquent, certaines réponses et commentaires ci-dessous peuvent ne plus avoir de sens car ils font référence à des modifications précédentes. (Veuillez consulter l'historique des modifications de la question dans de tels cas.)

Existe-t-il des langages de programmation populaires et fortement typés (tels que Haskell, Java générique, C #, F #, etc.) qui prennent en charge les types d'intersection pour les valeurs de retour de fonction? Si oui, lequel et comment?

(Si je suis honnête, j'aimerais vraiment voir quelqu'un montrer comment exprimer les types d'intersection dans un langage courant comme C # ou Java.)

Je vais donner un rapide exemple de ce à quoi pourraient ressembler les types d'intersection, en utilisant un pseudocode similaire à C #:

interface IX { … }
interface IY { … }
interface IB { … }

class A : IX, IY { … }
class B : IX, IY, IB { … }

T fn()  where T : IX, IY
{
    return … ? new A()  
             : new B();
}

Autrement dit, la fonction fnretourne une instance d'un certain type T, dont l'appelant sait seulement qu'il implémente des interfaces IXet IY. (C'est-à-dire que, contrairement aux génériques, l'appelant ne peut pas choisir le type concret de T- la fonction le fait. De cela, je suppose que ce Tn'est en fait pas un type universel, mais un type existentiel.)

PS: Je suis conscient que l'on pourrait simplement définir un interface IXY : IX, IYet changer le type de retour de fnen IXY. Cependant, ce n'est pas vraiment la même chose, car souvent vous ne pouvez pas boulonner une interface supplémentaire IXYà un type précédemment défini Aqui implémente uniquement IXet IYséparément.


Note de bas de page: Quelques ressources sur les types d'intersection:

L'article de Wikipedia pour "Type system" contient une sous - section sur les types d'intersection .

Rapport de Benjamin C. Pierce (1991), "Programmation avec les types d'intersection, les types d'union et le polymorphisme"

David P. Cunningham (2005), "Intersection types in practice" , qui contient une étude de cas sur le langage de Forsythe, qui est mentionnée dans l'article Wikipedia.

Une question Stack Overflow, "Union types and intersection types" qui a obtenu plusieurs bonnes réponses, dont celle-ci qui donne un exemple pseudocode de types d'intersection similaires au mien ci-dessus.


6
Comment est-ce ambigu? Tdéfinit un type, même s'il est juste défini dans la déclaration de fonction comme "un type qui étend / implémente IXet IY". Le fait que la réelle valeur de retour est un cas particulier de ce ( Aou Brespectivement) ne sont pas quelque chose de spécial ici, vous pourriez tout aussi bien y parvenir en utilisant au Objectlieu de T.
Joachim Sauer

1
Ruby vous permet de retourner tout ce que vous voulez d'une fonction. Idem pour les autres langues dynamiques.
thorsten müller

J'ai mis à jour ma réponse. @Joachim: Je suis conscient que le terme "ambigu" ne saisit pas très précisément le concept en question, d'où l'exemple pour clarifier le sens recherché.
stakx

1
Annonce PS: ... qui change votre question en "quelle langue permet de traiter le type Tcomme interface Iquand il implémente toutes les méthodes de l'interface, mais n'a pas déclaré cette interface".
Jan Hudec

6
C'était une erreur de fermer cette question, car il y a une réponse précise, qui est les types d'union . Les types d'union sont disponibles dans des langues telles que (Raquette typée) [ docs.racket-lang.org/ts-guide/] .
Sam Tobin-Hochstadt le

Réponses:


5

Scala a des types d'intersection complets intégrés dans la langue:

trait IX {...}
trait IY {...}
trait IB {...}

class A() extends IX with IY {...}

class B() extends IX with IY with IB {...}

def fn(): IX with IY = if (...) new A() else new B()

la scala basée sur les points aurait de vrais types d'intersection, mais pas pour la scala actuelle / ancienne.
Hongxu Chen

9

En fait, la réponse évidente est: Java

Bien que cela puisse vous surprendre d'apprendre que Java prend en charge les types d'intersection ... il le fait en effet via l'opérateur lié de type "&". Par exemple:

<T extends IX & IY> T f() { ... }

Voir ce lien sur plusieurs types de bornes en Java, et aussi depuis l'API Java.


Cela fonctionnera-t-il si vous ne connaissez pas le type au moment de la compilation? C'est-à-dire que l'on peut écrire <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. Et comment appelez-vous la fonction dans un tel cas? Ni A ni B ne peuvent apparaître sur le site d'appel, car vous ne savez pas lequel vous obtiendrez.
Jan Hudec

Oui, vous avez raison --- ce n'est pas équivalent à l'exemple original donné car vous devez fournir un type concret. Si nous pouvions utiliser des caractères génériques avec des limites d'intersection, nous l'aurions. On dirait que nous ne pouvons pas ... et je ne sais pas vraiment pourquoi (voir ceci ). Mais, Java a toujours des types d'intersection en quelque sorte ...
redjamjar

1
Je suis déçu de n'avoir jamais appris les types d'intersection en 10 ans que j'ai fait Java. Maintenant que j'utilise tout le temps avec Flowtype, je pensais que c'était la plus grande fonctionnalité manquante de Java, seulement pour découvrir que je ne les avais jamais vus une seule fois dans la nature. Les gens les sous-utilisent sérieusement. Je pense que s'ils étaient mieux connus, les frameworks d'injection de dépendances comme Spring n'auraient jamais été aussi populaires.
Andy

8

Question d'origine posée pour "type ambigu". Pour cela, la réponse a été:

Type ambigu, évidemment aucun. L'appelant doit savoir ce qu'il obtiendra, donc ce n'est pas possible. Tout langage peut renvoyer soit le type de base, l'interface (éventuellement généré automatiquement comme dans le type d'intersection) ou le type dynamique (et le type dynamique est simplement un type avec des méthodes d'appel par nom, get et set).

Interface déduite:

Donc, fondamentalement, vous voulez qu'il retourne une interface IXYqui dérive IX et IY bien que cette interface n'ait pas été déclarée dans l'un Aou l' autre B, peut-être parce qu'elle n'a pas été déclarée lorsque ces types ont été définis. Dans ce cas:

  • Tout ce qui est typé dynamiquement, évidemment.
  • Je ne me souviens pas qu'un langage traditionnel typé statiquement serait capable de générer l'interface (c'est le type d' union de Aet Bou le type d'intersection de IXet IY) lui-même.
  • ALLER , car ses classes implémentent l'interface si elles ont les bonnes méthodes, sans jamais les déclarer. Il vous suffit donc de déclarer une interface qui en dérive les deux et de la renvoyer.
  • Évidemment, tout autre langage où le type peut être défini pour implémenter une interface en dehors de la définition de ce type, mais je ne pense pas me souvenir d'autre que GO.
  • Ce n'est possible dans aucun type où l'implémentation d'une interface doit être définie dans la définition de type elle-même. Vous pouvez cependant contourner la plupart d'entre elles en définissant un wrapper qui implémente les deux interfaces et délègue toutes les méthodes à un objet encapsulé.

PS Un langage fortement typé est un langage dans lequel un objet de type donné ne peut pas être traité comme un objet d'un autre type, tandis qu'un langage faiblement typé est un langage qui a une distribution réinterprétée. Ainsi, tous les langages typés dynamiquement sont fortement typés , tandis que les langages faiblement typés sont l'assembly, C et C ++, les trois étant typés statiquement .


Ce n'est pas correct, le type n'a rien d'ambigu. Un langage peut renvoyer des soi-disant "types d'intersection" --- c'est juste que peu ou pas de langages traditionnels le font.
redjamjar

@redjamjar: La question avait un libellé différent lorsque j'ai répondu et demandé "type ambigu". C'est pourquoi ça commence par ça. La question a été considérablement réécrite depuis. J'élargirai la réponse pour mentionner à la fois l'original et le libellé actuel.
Jan Hudec

désolé, j'ai manqué ça évidemment!
redjamjar

+1 pour mentionner Golang, qui est probablement le meilleur exemple d'une langue commune qui le permet, même si la façon de le faire est un peu détournée.
Jules

3

Le type de langage de programmation Go a cela, mais uniquement pour les types d'interface.

Dans Go, tout type pour lequel les bonnes méthodes sont définies implémente automatiquement une interface, donc l'objection dans votre PS ne s'applique pas. En d'autres termes, créez simplement une interface qui a toutes les opérations des types d'interface à combiner (pour lesquelles il existe une syntaxe simple) et tout cela fonctionne correctement.

Un exemple:

package intersection

type (
    // The first component type.
    A interface {
        foo() int
    }
    // The second component type.
    B interface {
        bar()
    }

    // The intersection type.
    Intersection interface {
        A
        B
    }
)

// Function accepting an intersection type
func frob(x Intersection) {
    // You can directly call methods defined by A or B on Intersection.
    x.foo()
    x.bar()

    // Conversions work too.
    var a A = x
    var b B = x
    a.foo()
    b.bar()
}

// Syntax for a function returning an intersection type:
// (using an inline type definition to be closer to your suggested syntax)
func frob2() interface { A; B } {
    // return something
}

3

Vous pourriez être capable de faire ce que vous voulez en utilisant un type existentiel borné, qui peut être encodé dans n'importe quel langage avec des génériques et un polymorphisme borné, par exemple C #.

Le type de retour sera quelque chose comme (en code pseudo)

IAB = exists T. T where T : IA, IB

ou en C #:

interface IAB<IA, IB>
{
    R Apply<R>(IABFunc<R, IA, IB> f);
}

interface IABFunc<R, IA, IB>
{
    R Apply<T>(T t) where T : IA, IB;
}

class DefaultIAB<T, IA, IB> : IAB<IA, IB> where T : IA, IB 
{
    readonly T t;

    ...

    public R Apply<R>(IABFunc<R, IA, IB> f) {
        return f.Apply<T>(t);
    }
}

Remarque: je n'ai pas testé cela.

Le fait est qu'il IABdoit être en mesure d'appliquer un IABFunc pour n'importe quel type de retour R, et qu'il IABFuncdoit être en mesure de travailler sur tous les Tsous-types IAet IB.

L'intention de DefaultIABest simplement d'envelopper un existant Tqui sous IA- types et IB. Notez que ceci est différent de votre IAB : IA, IBdans qui DefaultIABpeut toujours être ajouté à un existantT plus tard.

Les références:


L'approche fonctionne si l'on ajoute un type générique d'objet wrapper avec les paramètres T, IA, IB, avec T contraint aux interfaces, qui encapsule une référence de type T et permet Applyd'y être invoqué. Le gros problème est qu'il n'y a aucun moyen d'utiliser une fonction anonyme pour implémenter une interface, donc les constructions comme celle-ci finissent par être vraiment pénibles à utiliser.
supercat

3

TypeScript est un autre langage typé qui prend en charge les types d'intersection T & U(ainsi que les types d'union T | U). Voici un exemple cité sur leur page de documentation sur les types avancés :

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

2

Ceylan a un support complet pour les types d' union et d' intersection de première classe .

Vous écrivez un type d'union comme X | Yet un type d'intersection comme X & Y.

Encore mieux, Ceylan présente de nombreux raisonnements sophistiqués sur ces types, notamment:

  • instanciations principales: par exemple, Consumer<X>&Consumer<Y>est du même type que Consumer<X|Y>si Consumerest contravariant dans X, et
  • disjointness: par exemple, Object&Nullest le même type que Nothing, le type inférieur.

0

Les fonctions C ++ ont toutes un type de retour fixe, mais si elles renvoient des pointeurs, les pointeurs peuvent, avec des restrictions, pointer vers différents types.

Exemple:

class Base {};
class Derived1: public Base {};
class Derived2: public Base{};

Base * function(int derived_type)
{
    if (derived_type == 1)
        return new Derived1;
    else
        return new Derived2;
}

Le comportement du pointeur renvoyé dépendra des virtualfonctions définies, et vous pouvez faire un downcast vérifié avec, disons,

Base * foo = function(...);dynamic_cast<Derived1>(foo).

C'est ainsi que le polymorphisme fonctionne en C ++.


Et bien sûr, on peut utiliser un anyou un varianttype, comme le boost de modèles fournit. Ainsi, la restriction ne reste pas.
Déduplicateur

Ce n'est pas tout à fait ce que la question demandait, cependant, qui était un moyen de spécifier que le type de retour étend deux superclasses identifiées en même temps, c'est-à-dire class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... maintenant quel type pouvons-nous spécifier qui permet de retourner l'un Derived1ou l' Derived2autre ni Base1ni Base2directement?
Jules

-1

Python

C'est très, très fortement tapé.

Mais le type n'est pas déclaré lors de la création d'une fonction, les objets renvoyés sont donc "ambigus".

Dans votre question spécifique, un meilleur terme pourrait être "polymorphe". C'est le cas d'utilisation courant en Python qui consiste à renvoyer des types de variantes qui implémentent une interface commune.

def some_function( selector, *args, **kw ):
    if selector == 'this':
        return This( *args, **kw )
    else:
        return That( *args, **kw )

Puisque Python est fortement typé, l'objet résultant sera une instance de Thiset Thatet ne peut pas (facilement) être contraint ou converti en un autre type d'objet.


C'est assez trompeur; alors que le type d'un objet est à peu près immuable, les valeurs peuvent être converties assez facilement entre les types. Pour str, par exemple, trivialement.
James Youngman

1
@JamesYoungman: Quoi? C'est vrai pour toutes les langues. Toutes les langues que j'ai vues ont des conversions to_string à gauche, à droite et au centre. Je n'ai pas du tout votre commentaire. Peux-tu élaborer?
S.Lott

J'essayais de comprendre ce que vous vouliez dire par "Python est fortement typé". J'ai peut-être mal compris ce que vous entendiez par «fortement tapé». Franchement Python a peu de caractéristiques que j'associerais à des langages fortement typés. Par exemple, il accepte les programmes dans lesquels le type de retour d'une fonction n'est pas compatible avec l'utilisation de la valeur par l'appelant. Par exemple, "x, y = F (z)" où F () renvoie (z, z, z).
James Youngman

Le type d'un objet Python ne peut pas (sans magie sérieuse) être changé. Il n'y a pas d'opérateur "cast" comme c'est le cas avec Java et C ++. Cela rend chaque objet fortement typé. Les noms de variable et les noms de fonction n'ont pas de liaison de type, mais les objets eux-mêmes sont fortement typés. Le concept clé ici n'est pas la présence de déclarations. Le concept clé est la disponibilité des opérateurs de distribution. Notez également que cela me semble factuel; les modérateurs peuvent toutefois contester cela.
S.Lott

1
Les opérations de transtypage C et C ++ ne changent pas non plus le type de leur opérande.
James Youngman
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.