Quali lingue tipicamente statiche supportano i tipi di intersezione per i valori restituiti dalla funzione?


17

Nota iniziale:

Questa domanda è stata chiusa dopo varie modifiche perché mancava della terminologia corretta per affermare con precisione ciò che stavo cercando. Sam Tobin-Hochstadt ha quindi pubblicato un commento che mi ha fatto riconoscere esattamente quello che era: linguaggi di programmazione che supportano i tipi di intersezione per i valori di ritorno della funzione.

Ora che la domanda è stata riaperta, ho deciso di migliorarla riscrivendola in modo (si spera) più preciso. Pertanto, alcune risposte e commenti di seguito potrebbero non avere più senso in quanto si riferiscono a modifiche precedenti. (In questi casi, consultare la cronologia delle modifiche della domanda.)

Esistono linguaggi di programmazione popolari staticamente e fortemente tipizzati (come Haskell, Java generico, C #, F #, ecc.) Che supportano i tipi di intersezione per i valori di ritorno della funzione? In caso affermativo, quale e come?

(Se sono onesto, mi piacerebbe davvero vedere qualcuno dimostrare un modo per esprimere i tipi di intersezione in un linguaggio tradizionale come C # o Java.)

Darò un rapido esempio di come potrebbero apparire i tipi di intersezione, usando alcuni pseudocodici simili a 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();
}

Cioè, la funzione fnrestituisce un'istanza di qualche tipo T, di cui il chiamante sa solo che implementa interfacce IXe IY. (Cioè, a differenza dei generici, il chiamante non riesce a scegliere il tipo concreto di T- la funzione lo fa. Da questo suppongo che Tin realtà non sia un tipo universale, ma un tipo esistenziale.)

PS: Sono consapevole che si potrebbe semplicemente definire un interface IXY : IX, IYe cambiare il tipo di ritorno di fnin IXY. Tuttavia, non è proprio la stessa cosa, perché spesso non è possibile fissare un'interfaccia aggiuntiva IXYa un tipo precedentemente definito Ache implementa solo IXe IYseparatamente.


Nota a piè di pagina: alcune risorse sui tipi di intersezione:

L'articolo di Wikipedia per "Tipo di sistema" contiene una sottosezione relativa ai tipi di intersezione .

Rapporto di Benjamin C. Pierce (1991), "Programmazione con tipi di intersezione, tipi di unione e polimorfismo"

David P. Cunningham (2005), "Tipi di intersezione in pratica" , che contiene un caso di studio sul linguaggio Forsythe, che è menzionato nell'articolo di Wikipedia.

Una domanda Stack Overflow, "Tipi di unione e tipi di intersezione" che ha ottenuto diverse risposte valide, tra cui questa che fornisce un esempio di pseudocodice di tipi di intersezione simili ai miei sopra.


6
Com'è ambiguo? Tdefinisce un tipo, anche se è appena definito nella dichiarazione di funzione come "un tipo che estende / implementa IXe IY". Il fatto che il valore di ritorno effettivo sia un caso speciale di quello ( Ao Brispettivamente) non è qualcosa di speciale qui, si potrebbe anche ottenere ciò usando Objectinvece di T.
Joachim Sauer,

1
Ruby ti consente di restituire quello che vuoi da una funzione. Lo stesso per altre lingue dinamiche.
Thorsten Müller,

Ho aggiornato la mia risposta. @Joachim: sono consapevole che il termine "ambiguo" non cattura il concetto in questione in modo molto accurato, quindi l'esempio per chiarire il significato previsto.
stakx,

1
Ad PS: ... che cambia la tua domanda in "quale lingua consente di trattare il tipo Tcome interfaccia Iquando implementa tutti i metodi dell'interfaccia, ma non ha dichiarato tale interfaccia".
Jan Hudec,

6
È stato un errore chiudere questa domanda, perché c'è una risposta precisa, che è i tipi di unione . I tipi di unione sono disponibili in lingue come (Typed Racket) [ docs.racket-lang.org/ts-guide/] .
Sam Tobin-Hochstadt,

Risposte:


5

Scala ha tipi di intersezione completi integrati nella lingua:

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()

scala basata a punti avrebbe veri tipi di intersezione, ma non per scala attuale / precedente.
Hongxu Chen,

9

In effetti, la risposta ovvia è: Java

Mentre può sorprenderti apprendere che Java supporta i tipi di intersezione ... lo fa effettivamente tramite l'operatore di tipo "&". Per esempio:

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

Vedi questo collegamento su più limiti di tipo in Java e anche questo dall'API Java.


Funzionerà se non conosci il tipo in fase di compilazione? Vale a dire uno può scrivere <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. E come si chiama la funzione in tal caso? Né A né B possono apparire sul sito della chiamata, perché non sai quale otterrai.
Jan Hudec,

Sì, hai ragione --- non è equivalente all'esempio originale dato che devi fornire un tipo concreto. Se potessimo usare i caratteri jolly con limiti di intersezione, allora avremmo. Sembra che non possiamo ... e non so davvero perché no (vedi questo ). Ma Java ha ancora tipi di intersezione di un tipo ...
redjamjar,

1
Sono deluso dal fatto che in 10 anni non ho mai imparato a conoscere i tipi di intersezione con Java. Ora che uso tutto il tempo con Flowtype, ho pensato che fossero la più grande caratteristica mancante in Java, solo per scoprire che non li avevo mai visti nemmeno una volta in natura. Le persone li stanno seriamente sottoutilizzando. Penso che se fossero stati conosciuti meglio, allora i framework di iniezione di dipendenza come Spring non sarebbero mai diventati così popolari.
Andy,

8

Domanda originale per "tipo ambiguo". Per questo la risposta è stata:

Tipo ambiguo, ovviamente nessuno. Il chiamante deve sapere cosa otterrà, quindi non è possibile. Tutto ciò che qualsiasi lingua può restituire è di tipo base, interfaccia (possibilmente generata automaticamente come nel tipo di intersezione) o tipo dinamico (e il tipo dinamico è fondamentalmente solo il tipo con chiamata per nome, metodi get e set).

Interfaccia dedotta:

Quindi sostanzialmente vuoi che ritorni un'interfaccia IXYche deriva IX e IY sebbene quell'interfaccia non sia stata dichiarata in nessuno dei due Ao B, forse perché non è stata dichiarata quando quei tipi sono stati definiti. In quel caso:

  • Ovviamente qualsiasi cosa sia tipizzata in modo dinamico.
  • Non ricordo alcuna lingua corrente principale tipizzazione statica sarebbe in grado di generare l'interfaccia (ad esempio l' unione tipo di Ae Bo il tipo di intersezione di IXe IY) stesso.
  • GO , perché le sue classi implementano l'interfaccia se hanno i metodi corretti, senza mai dichiararli. Quindi devi semplicemente dichiarare un'interfaccia che ne derivi i due e restituirla.
  • Ovviamente qualsiasi altra lingua in cui il tipo può essere definito per implementare l'interfaccia al di fuori della definizione di quel tipo, ma non credo di ricordare altro che GO.
  • Non è possibile in alcun tipo in cui l'implementazione di un'interfaccia deve essere definita nella definizione del tipo stesso. Tuttavia, è possibile aggirare la maggior parte di essi definendo il wrapper che implementa le due interfacce e delega tutti i metodi a un oggetto avvolto.

PS Un linguaggio fortemente tipizzato è un linguaggio in cui un oggetto di un determinato tipo non può essere trattato come un oggetto di un altro tipo, mentre un linguaggio debolmente tipizzato è un linguaggio che ha un cast reinterpretato. Quindi tutti i linguaggi tipizzati dinamicamente sono fortemente tipizzati , mentre i linguaggi debolmente tipizzati sono assembly, C e C ++, tutti e tre sono tipizzati staticamente .


Questo non è corretto, non c'è nulla di ambiguo nel tipo. Una lingua può restituire i cosiddetti "tipi di intersezione" --- sono solo pochi se qualche lingua tradizionale lo fa.
Redjamjar,

@redjamjar: la domanda aveva una formulazione diversa quando ho risposto e ho chiesto "tipo ambiguo". Ecco perché inizia con quello. La domanda è stata significativamente riscritta da allora. Espanderò la risposta per menzionare sia la formulazione originale che quella attuale.
Jan Hudec,

scusa, mi è mancato ovviamente!
Redjamjar,

+1 per menzionare Golang, che è probabilmente il miglior esempio di un linguaggio comune che lo consente, anche se il modo per farlo è un po 'tortuoso.
Jules,

3

Il tipo di linguaggio di programmazione Go ha questo, ma solo per i tipi di interfaccia.

In Go qualsiasi tipo per il quale sono definiti i metodi corretti implementa automaticamente un'interfaccia, quindi l'obiezione nel tuo PS non si applica. In altre parole, basta creare un'interfaccia che abbia tutte le operazioni dei tipi di interfaccia da combinare (per le quali esiste una semplice sintassi) e tutto funziona perfettamente.

Un esempio:

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

Potresti essere in grado di fare ciò che desideri utilizzando un tipo esistenziale limitato, che può essere codificato in qualsiasi linguaggio con generici e polimorfismo limitato, ad esempio C #.

Il tipo restituito sarà simile a (nel codice psuedo)

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

o in 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);
    }
}

Nota: non l'ho provato.

Il punto è che IABdeve essere in grado di applicare un IABFunc per qualsiasi tipo di ritorno Re IABFuncdeve essere in grado di lavorare su qualsiasi Tsottotipo sia IAe IB.

L'intento di DefaultIABè solo quello di avvolgere un esistente Tche sottotipi IAe IB. Nota che questo è diverso dal tuo IAB : IA, IBin che DefaultIABpuò sempre essere aggiunto a un esistente in Tseguito.

Riferimenti:


L'approccio funziona se si aggiunge un tipo generico di oggetto-wrapper con parametri T, IA, IB, con T vincolato alle interfacce, che incapsula un riferimento di tipo T e consente Applydi invocarlo. Il grosso problema è che non c'è modo di usare una funzione anonima per implementare un'interfaccia, quindi i costrutti come quello finiscono per essere un vero dolore da usare.
supercat

3

TypeScript è un altro linguaggio tipizzato che supporta i tipi di intersezione T & U(insieme ai tipi di unione T | U). Ecco un esempio citato dalla loro pagina di documentazione sui tipi avanzati :

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

Ceylon ha il pieno supporto per i tipi di unione e intersezione di prima classe .

Scrivi un tipo di unione come X | Ye un tipo di intersezione come X & Y.

Ancora meglio, Ceylon offre molti ragionamenti sofisticati su questi tipi, tra cui:

  • istanze principali: ad esempio, Consumer<X>&Consumer<Y>è dello stesso tipo di Consumer<X|Y>se Consumerè contraddittorio in X, e
  • disgiunzione: ad esempio, Object&Nullè lo stesso tipo Nothingdel tipo inferiore.

0

Tutte le funzioni C ++ hanno un tipo di ritorno fisso, ma se restituiscono puntatori i puntatori possono, con restrizioni, puntare a tipi diversi.

Esempio:

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;
}

Il comportamento del puntatore restituito dipenderà dalle virtualfunzioni definite e puoi eseguire un downcast verificato con, per esempio,

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

Ecco come funziona il polimorfismo in C ++.


E ovviamente si può usare un tipo anyo variant, come il boost di template fornisce. Pertanto, la restrizione non rimane.
Deduplicatore

Questo non è esattamente quello che la domanda stava ponendo, tuttavia, che era per un modo per specificare che il tipo di ritorno estende contemporaneamente due superclassi identificate, cioè class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... ora che tipo possiamo specificare che consente di restituire uno Derived1o Derived2entrambi Base1Base2direttamente?
Jules,

-1

Pitone

È molto, molto fortemente tipizzato.

Ma il tipo non viene dichiarato quando viene creata una funzione, quindi gli oggetti restituiti sono "ambigui".

Nella tua domanda specifica, un termine migliore potrebbe essere "polimorfico". Questo è il caso d'uso comune in Python è restituire tipi di varianti che implementano un'interfaccia comune.

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

Poiché Python è fortemente tipizzato, l'oggetto risultante sarà un'istanza Thiso Thatnon potrà (facilmente) essere forzato o proiettato su un altro tipo di oggetto.


Questo è abbastanza fuorviante; mentre il tipo di un oggetto è praticamente immutabile, i valori possono essere convertiti tra tipi abbastanza facilmente. Str, ad esempio, banalmente.
James Youngman,

1
@JamesYoungman: What? Questo è vero per tutte le lingue. Tutte le lingue che abbia mai visto hanno conversioni to_string a sinistra, a destra e al centro. Non capisco affatto il tuo commento. Puoi elaborare?
S.Lott

Stavo cercando di capire cosa intendevi per "Python è fortemente tipizzato". Forse ho capito male cosa intendevi per "fortemente tipizzato". Francamente Python ha alcune delle caratteristiche che assocerei a linguaggi fortemente tipizzati. Ad esempio, accetta programmi in cui il tipo restituito di una funzione non è compatibile con l'uso del valore da parte del chiamante. Ad esempio, "x, y = F (z)" dove F () restituisce (z, z, z).
James Youngman,

Il tipo di un oggetto Python non può essere modificato (senza magia seria). Non esiste un operatore "cast" come lo è con Java e C ++. Ciò rende ogni oggetto fortemente tipizzato. I nomi delle variabili e i nomi delle funzioni non hanno alcun tipo di bind, ma gli oggetti stessi sono fortemente tipizzati. Il concetto chiave qui non è la presenza di dichiarazioni. Il concetto chiave è la disponibilità degli operatori del cast. Nota anche che questo mi sembra essere reale; i moderatori possono contestarlo, tuttavia.
S. Lott,

1
Le operazioni di cast C e C ++ non cambiano neanche il tipo del loro operando.
James Youngman,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.