¿Qué lenguajes de tipo estático admiten tipos de intersección para valores de retorno de funciones?


17

Nota inicial:

Esta pregunta se cerró después de varias ediciones porque carecía de la terminología adecuada para indicar con precisión lo que estaba buscando. Sam Tobin-Hochstadt luego publicó un comentario que me hizo reconocer exactamente qué era eso: lenguajes de programación que admiten tipos de intersección para valores de retorno de funciones.

Ahora que la pregunta ha sido reabierta, he decidido mejorarla reescribiéndola de una manera (con suerte) más precisa. Por lo tanto, algunas respuestas y comentarios a continuación pueden no tener sentido porque se refieren a ediciones anteriores. (Consulte el historial de edición de la pregunta en tales casos).

¿Existen lenguajes de programación populares tipados estáticamente y fuertemente (como Haskell, Java genérico, C #, F #, etc.) que admiten tipos de intersección para valores de retorno de funciones? Si es así, ¿cuál y cómo?

(Si soy sincero, me encantaría ver a alguien demostrar una forma de expresar los tipos de intersección en un lenguaje convencional como C # o Java).

Daré un ejemplo rápido de cómo se verían los tipos de intersección, usando un pseudocódigo similar 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();
}

Es decir, la función fndevuelve una instancia de algún tipo T, de la cual la persona que llama solo sabe que implementa interfaces IXy IY. (Es decir, a diferencia de los genéricos, la persona que llama no puede elegir el tipo concreto de T- la función sí. De esto supongo que Ten realidad no es un tipo universal, sino un tipo existencial).

PD: Soy consciente de que uno podría simplemente definir ay interface IXY : IX, IYcambiar el tipo de retorno de fna IXY. Sin embargo, eso no es realmente lo mismo, porque a menudo no puede atornillar una interfaz adicional IXYa un tipo definido previamente Aque solo se implementa IXy por IYseparado.


Nota al pie: Algunos recursos sobre tipos de intersección:

El artículo de Wikipedia para "Sistema de tipos" tiene una subsección sobre los tipos de intersección .

Informe de Benjamin C. Pierce (1991), "Programación con tipos de intersección, tipos de unión y polimorfismo"

David P. Cunningham (2005), "Tipos de intersección en la práctica" , que contiene un estudio de caso sobre el lenguaje Forsythe, que se menciona en el artículo de Wikipedia.

Una pregunta de desbordamiento de pila, "Tipos de unión y tipos de intersección" que obtuvo varias buenas respuestas, entre ellas esta que da un ejemplo de pseudocódigo de tipos de intersección similares a los míos anteriores.


66
¿Cómo es esto ambiguo? Tdefine un tipo, incluso si solo se define dentro de la declaración de función como "algún tipo que se extiende / implementa IXy IY". El hecho de que el valor de retorno real sea ​​un caso especial de eso ( Ao Brespectivamente) no es nada especial aquí, también podría lograrlo usando en Objectlugar de T.
Joachim Sauer

1
Ruby te permite devolver lo que quieras de una función. Lo mismo para otros lenguajes dinámicos.
thorsten müller

He actualizado mi respuesta. @Joachim: Soy consciente de que el término "ambiguo" no captura el concepto en cuestión con mucha precisión, por lo tanto, el ejemplo para aclarar el significado deseado.
stakx

1
Anuncio PS: ... que cambia su pregunta a "qué idioma permite tratar el tipo Tcomo interfaz Icuando implementa todos los métodos de la interfaz, pero no declaró esa interfaz".
Jan Hudec

66
Fue un error cerrar esta pregunta, porque hay una respuesta precisa, que son los tipos de unión . Los tipos de unión están disponibles en idiomas como (Typed Racket) [ docs.racket-lang.org/ts-guide/] .
Sam Tobin-Hochstadt

Respuestas:


5

Scala tiene tipos de intersección completos integrados en el lenguaje:

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 escala basada en puntos tendría verdaderos tipos de intersección, pero no para la escala actual / anterior.
Hongxu Chen

9

De hecho, la respuesta obvia es: Java

Si bien puede sorprenderle saber que Java admite tipos de intersección ... de hecho lo hace a través del operador de tipo "&". Por ejemplo:

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

Vea este enlace en múltiples límites de tipo en Java, y también esto desde la API de Java.


¿Funcionará si no conoce el tipo en tiempo de compilación? Es decir, uno puede escribir <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. ¿Y cómo se llama a la función en tal caso? Ni A ni B pueden aparecer en el sitio de la llamada, porque no sabes cuál obtendrás.
Jan Hudec

Sí, tiene razón --- no es equivalente al ejemplo original dado ya que necesita proporcionar un tipo concreto. Si pudiéramos usar comodines con límites de intersección, entonces lo tendríamos. Parece que no podemos ... y realmente no sé por qué no (ver esto ). Sin embargo, todavía tiene Java tipos de intersección de una especie ...
redjamjar

1
Me siento decepcionado porque de alguna manera nunca aprendí sobre los tipos de intersección en los 10 años que hice Java. Ahora que uso todo el tiempo con Flowtype, pensé que eran la característica más grande que faltaba en Java, solo para descubrir que nunca las había visto en la naturaleza. La gente los está infrautilizando seriamente. Creo que si fueran más conocidos, entonces los marcos de inyección de dependencia como Spring nunca se habrían vuelto tan populares.
Andy

8

Pregunta original para "tipo ambiguo". Para eso la respuesta fue:

Tipo ambiguo, obviamente ninguno. La persona que llama necesita saber qué obtendrá, por lo que no es posible. Todo lo que cualquier lenguaje puede devolver es el tipo base, la interfaz (posiblemente generada automáticamente como en el tipo de intersección) o el tipo dinámico (y el tipo dinámico es básicamente solo escribir con los métodos de llamada por nombre, get y set).

Interfaz inferida:

Entonces, básicamente, desea que devuelva una interfaz IXYque se derive IX y, IY aunque esa interfaz no se declaró en ninguno Ao B, posiblemente porque no se declaró cuando se definieron esos tipos. En ese caso:

  • Cualquiera que esté escrito dinámicamente, obviamente.
  • No recuerdo que ningún lenguaje convencional estáticamente escrito pueda generar la interfaz (es el tipo de unión de Ay / Bo el tipo de intersección de IXy IY) en sí.
  • GO , porque sus clases implementan la interfaz si tienen los métodos correctos, sin declararlos nunca. Entonces, simplemente declara una interfaz que deriva los dos allí y la devuelve.
  • Obviamente, cualquier otro lenguaje donde se pueda definir el tipo para implementar la interfaz fuera de la definición de ese tipo, pero no creo recordar ningún otro que no sea GO.
  • No es posible en ningún tipo en el que la implementación de una interfaz deba definirse en la definición del tipo en sí. Sin embargo, puede solucionarlos en la mayoría de ellos definiendo un contenedor que implemente las dos interfaces y delegue todos los métodos en un objeto envuelto.

PD Un lenguaje fuertemente tipado es aquel en el que un objeto de un tipo dado no puede ser tratado como objeto de otro tipo, mientras que un lenguaje débilmente tipado es aquel que tiene un reparto reinterpretado. Por lo tanto, todos los lenguajes escritos dinámicamente están fuertemente tipados , mientras que los lenguajes débilmente tipados son ensamblados, C y C ++, los tres están tipados estáticamente .


Esto no es correcto, no hay nada ambiguo sobre el tipo. Un idioma puede devolver los llamados "tipos de intersección", es muy poco si alguno de los idiomas principales lo hace.
redjamjar

@redjamjar: La pregunta tenía una redacción diferente cuando la respondí y pedí "tipo ambiguo". Por eso comienza con eso. La pregunta fue reescrita significativamente desde entonces. Ampliaré la respuesta para mencionar tanto la redacción original como la actual.
Jan Hudec

lo siento, me perdí eso obviamente!
redjamjar

+1 por mencionar Golang, que es probablemente el mejor ejemplo de un lenguaje común que lo permite, incluso si la forma de hacerlo es un poco tortuosa.
Jules

3

El lenguaje de programación Go tipo de cuenta de esto, pero sólo para los tipos de interfaz.

En Go, cualquier tipo para el que se definan los métodos correctos implementa automáticamente una interfaz, por lo que la objeción en su PS no se aplica. En otras palabras, solo cree una interfaz que tenga todas las operaciones de los tipos de interfaz que se combinarán (para lo cual hay una sintaxis simple) y todo funciona.

Un ejemplo:

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

Es posible que pueda hacer lo que quiera utilizando un tipo existencial acotado, que puede codificarse en cualquier idioma con genéricos y polimorfismo acotado, por ejemplo, C #.

El tipo de retorno será algo así (en código psuedo)

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

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

Nota: no he probado esto.

El punto es que IABtiene que poder aplicar un IABFunc para cualquier tipo de retorno R, y IABFuncdebe poder trabajar en cualquiera de los Tsubtipos de ambos IAy IB.

La intención de DefaultIABes sólo para envolver un existentes Tque subtipos IAy IB. Tenga en cuenta que esto es diferente de su IAB : IA, IBen que DefaultIABsiempre se puede agregar a un existente Tmás adelante.

Referencias


El enfoque funciona si se agrega un tipo genérico de envoltura de objetos con los parámetros T, IA, IB, con T restringido a las interfaces, que encapsula una referencia de tipo T y permite Applyinvocarlo. El gran problema es que no hay forma de usar una función anónima para implementar una interfaz, por lo que las construcciones como esa terminan siendo realmente difíciles de usar.
supercat

3

TypeScript es otro lenguaje escrito que admite tipos de intersección T & U(junto con tipos de unión T | U). Aquí hay un ejemplo citado de su página de documentación sobre tipos avanzados :

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

Ceilán tiene soporte completo para los tipos de unión e intersección de primera clase .

Escribe un tipo de unión como X | Yy un tipo de intersección como X & Y.

Aún mejor, Ceilán presenta muchos razonamientos sofisticados sobre estos tipos, que incluyen:

  • instanciaciones principales: por ejemplo, Consumer<X>&Consumer<Y>es del mismo tipo que Consumer<X|Y>si Consumeres contravariante en X, y
  • disyunción: por ejemplo, Object&Nulles del mismo tipo que Nothingel tipo inferior.

0

Todas las funciones de C ++ tienen un tipo de retorno fijo, pero si devuelven punteros, los punteros pueden, con restricciones, señalar diferentes tipos.

Ejemplo:

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

El comportamiento del puntero devuelto dependerá de qué virtualfunciones estén definidas, y puede hacer un downcast marcado con, por ejemplo,

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

Así es como funciona el polimorfismo en C ++.


Y, por supuesto, uno puede usar uno anyo un varianttipo, como las plantillas que proporciona el impulso. Por lo tanto, la restricción no se queda.
Deduplicador

Sin embargo, esto no es exactamente lo que estaba preguntando, que era una forma de especificar que el tipo de retorno extiende dos superclases identificadas al mismo tiempo, es decir class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... ahora qué tipo podemos especificar que permita el retorno Derived1o Derived2no Base1ni Base2directamente?
Jules

-1

Pitón

Está muy, muy fuertemente escrito.

Pero el tipo no se declara cuando se crea una función, por lo que los objetos devueltos son "ambiguos".

En su pregunta específica, un término mejor podría ser "polimórfico". Ese es el caso de uso común en Python es devolver tipos de variantes que implementan una interfaz común.

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

Dado que Python está fuertemente tipado, el objeto resultante será una instancia de Thiso Thaty no puede (fácilmente) ser coaccionado o lanzado a otro tipo de objeto.


Esto es bastante engañoso; Si bien el tipo de un objeto es prácticamente inmutable, los valores se pueden convertir entre tipos con bastante facilidad. Str, por ejemplo, trivialmente.
James Youngman

1
@JamesYoungman: ¿Qué? Eso es cierto para todos los idiomas. Todos los idiomas que he visto tienen que convertir las cadenas hacia la izquierda, derecha y centro. No recibo tu comentario en absoluto. ¿Puedes elaborar?
S.Lott

Intentaba entender lo que querías decir con "Python está fuertemente tipado". Quizás entendí mal lo que querías decir con "fuertemente tipeado". Francamente, Python tiene algunas de las características que asociaría con lenguajes fuertemente tipados. Por ejemplo, acepta programas en los que el tipo de retorno de una función no es compatible con el uso del valor por parte de la persona que llama. Por ejemplo, "x, y = F (z)" donde F () devuelve (z, z, z).
James Youngman

El tipo de un objeto Python no se puede cambiar (sin magia seria). No hay un operador "cast" como lo hay con Java y C ++. Eso hace que cada objeto esté fuertemente tipado. Los nombres de variables y nombres de funciones no tienen enlace de tipo, pero los objetos en sí están fuertemente tipados. El concepto clave aquí no es la presencia de declaraciones. El concepto clave es la disponibilidad de los operadores de reparto. También tenga en cuenta que esto me parece factual; los moderadores pueden disputar eso, sin embargo.
S.Lott

1
Las operaciones de conversión C y C ++ tampoco cambian el tipo de su operando.
James Youngman
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.