Quais idiomas de tipo estaticamente suportam tipos de interseção para valores de retorno de função?


17

Nota inicial:

Essa questão foi encerrada após várias edições, porque eu não tinha a terminologia adequada para indicar com precisão o que estava procurando. Sam Tobin-Hochstadt postou um comentário que me fez reconhecer exatamente o que era: linguagens de programação que suportam tipos de interseção para valores de retorno de função.

Agora que a pergunta foi reaberta, decidi melhorá-la reescrevendo-a de uma maneira (esperançosamente) mais precisa. Portanto, algumas respostas e comentários abaixo podem não fazer mais sentido, pois se referem a edições anteriores. (Consulte o histórico de edições da pergunta nesses casos.)

Existem linguagens de programação populares de tipo estatístico e fortemente tipado (como Haskell, Java genérico, C #, F # etc.) que suportam tipos de interseção para valores de retorno de função? Se sim, qual e como?

(Se eu for honesto, eu adoraria ver alguém demonstrar uma maneira de expressar tipos de interseção em uma linguagem convencional como C # ou Java.)

Vou dar um exemplo rápido de como os tipos de interseção podem parecer, usando algum pseudocódigo semelhante ao 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();
}

Ou seja, a função fnretorna uma instância de algum tipo T, da qual o chamador sabe apenas que implementa interfaces IXe IY. (Ou seja, diferentemente dos genéricos, o chamador não escolhe o tipo concreto de T- a função escolhe . A partir disso, eu suponho que, Tna verdade, não seja um tipo universal, mas existencial.)

PS: Estou ciente de que alguém poderia simplesmente definir ae interface IXY : IX, IYalterar o tipo de retorno fnpara IXY. No entanto, isso não é realmente a mesma coisa, porque geralmente você não pode anexar uma interface adicional IXYa um tipo definido anteriormente Aque apenas implementa IXe IYseparadamente.


Nota de rodapé: Alguns recursos sobre os tipos de interseção:

O artigo da Wikipedia para "Sistema de tipos" possui uma subseção sobre tipos de interseções .

Relatório de Benjamin C. Pierce (1991), "Programação com tipos de interseção, tipos de união e polimorfismo"

David P. Cunningham (2005), "Tipos de interseção na prática" , que contém um estudo de caso sobre o idioma de Forsythe, mencionado no artigo da Wikipedia.

Uma pergunta sobre estouro de pilha, "Tipos de união e tipos de interseção", que obteve várias boas respostas, entre elas uma que fornece um exemplo em pseudocódigo de tipos de interseção semelhantes aos meus acima.


6
Como isso é ambíguo? Tdefine um tipo, mesmo que apenas seja definido na declaração da função como "algum tipo que estende / implementa IXe IY". O fato de o valor de retorno real ser um caso especial disso ( Aou Brespectivamente) não é nada de especial aqui, você também pode conseguir isso usando em Objectvez de T.
Joachim Sauer

1
Ruby permite retornar o que você quiser de uma função. O mesmo para outras linguagens dinâmicas.
22612 Thorsten

Eu atualizei minha resposta. @ Joachim: Estou ciente de que o termo "ambíguo" não captura o conceito em questão com muita precisão, portanto, o exemplo para esclarecer o significado pretendido.
stakx

1
Anúncio PS: ... que altera sua pergunta para "qual idioma permite tratar o tipo Tcomo interface Iquando implementa todos os métodos da interface, mas não declarou essa interface".
Jan Hudec

6
Foi um erro encerrar esta pergunta, porque há uma resposta precisa, que é do tipo união . Os tipos de união estão disponíveis em idiomas como (Typed Racket) [ docs.racket-lang.org/ts-guide/] .
Sam Tobin-Hochstadt

Respostas:


5

O Scala possui tipos de interseção completos incorporados ao idioma:

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

a escala baseada em pontos teria tipos de interseção verdadeiros, mas não para a escala atual / anterior.
Hongxu Chen

9

De fato, a resposta óbvia é: Java

Embora você possa se surpreender ao saber que o Java suporta tipos de interseção ... ele realmente funciona através do operador vinculado ao tipo "&". Por exemplo:

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

Veja este link em vários limites de tipo em Java e também na API Java.


Isso funcionará se você não souber o tipo em tempo de compilação? Ou seja, alguém pode escrever <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. E como você chama a função nesse caso? Nem A nem B podem aparecer no site da chamada, porque você não sabe qual deles receberá.
Jan Hudec

Sim, você está certo - não é equivalente ao exemplo original dado, pois você precisa fornecer um tipo concreto. Se pudéssemos usar curingas com limites de interseção, teríamos. Parece que não podemos ... e eu realmente não sei por que não (veja isso ). Mas, ainda assim Java tem tipos de intersecção de uma espécie ...
redjamjar

1
Sinto-me decepcionado por nunca ter aprendido sobre os tipos de interseção nos 10 anos em que aprendi Java. Agora que eu uso o tempo todo com o Flowtype, pensei que eles eram o maior recurso que faltava em Java, apenas para descobrir que eu nunca os havia visto na natureza. As pessoas os estão subutilizando seriamente. Eu acho que se eles fossem mais conhecidos, os frameworks de injeção de dependência como o Spring nunca teriam se tornado tão populares.
Andy

8

Pergunta original solicitada "tipo ambíguo". Para isso a resposta foi:

Tipo ambíguo, obviamente nenhum. O chamador precisa saber o que receberá, por isso não é possível. Todo o idioma que você pode retornar é do tipo base, interface (possivelmente gerado automaticamente como no tipo de interseção) ou dinâmico (e o tipo dinâmico é basicamente apenas o tipo com métodos de chamada por nome, obtenção e configuração).

Interface inferida:

Então, basicamente, você deseja que ele retorne uma interface IXYque deriva IX e, IY embora essa interface não tenha sido declarada em um Aou outro B, possivelmente porque não foi declarada quando esses tipos foram definidos. Nesse caso:

  • Qualquer um que seja digitado dinamicamente, obviamente.
  • Não me lembro de nenhuma linguagem mainstream estaticamente digitada capaz de gerar a interface (é o tipo de uniãoA e / Bou o tipo de interseção IXe IY).
  • GO , porque suas classes implementam interface se tiverem os métodos corretos, sem nunca declará-los. Então você apenas declara uma interface que deriva as duas e a retorna.
  • Obviamente, qualquer outra linguagem em que tipo possa ser definida para implementar uma interface fora da definição desse tipo, mas acho que não me lembro de outra coisa senão GO.
  • Não é possível em nenhum tipo em que a implementação de uma interface deve ser definida na própria definição de tipo. No entanto, você pode contornar a maioria deles definindo o wrapper que implementa as duas interfaces e delega todos os métodos para um objeto empacotado.

PS Uma linguagem fortemente tipada é aquela em que um objeto de um determinado tipo não pode ser tratado como objeto de outro tipo, enquanto a linguagem tipicamente fraca é aquela que tem uma conversão reinterpretada. Assim, todas as linguagens dinamicamente tipadas são fortemente tipadas , enquanto as linguagens fracamente tipadas são assembly, C e C ++, todas as três sendo tipadas estaticamente .


Isso não está correto, não há nada ambíguo no tipo. Um idioma pode retornar os chamados "tipos de interseção" - são poucos os idiomas principais, se houver.
Redheadjar 18/10/12

@redjamjar: A pergunta tinha uma redação diferente quando eu respondi e perguntei por "tipo ambíguo". É por isso que começa com isso. A questão foi reescrita significativamente desde então. Expandirei a resposta para mencionar o texto original e o atual.
Jan Hudec

desculpe, eu perdi isso obviamente!
redjamjar

+1 por mencionar Golang, que é provavelmente o melhor exemplo de uma linguagem comum que permite isso, mesmo que a maneira de fazer isso seja um pouco tortuosa.
Jules

3

O Go Programming Language meio que tem isso, mas apenas para tipos de interface.

Em Ir, qualquer tipo para o qual os métodos corretos sejam definidos implementa automaticamente uma interface; portanto, a objeção no seu PS não se aplica. Em outras palavras, basta criar uma interface que tenha todas as operações dos tipos de interface a serem combinadas (para as quais existe uma sintaxe simples) e tudo isso apenas funcione.

Um exemplo:

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

Você pode fazer o que quiser usando um tipo existencial limitado, que pode ser codificado em qualquer idioma com genéricos e polimorfismo limitado, por exemplo, C #.

O tipo de retorno será algo como (no código psuedo)

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

ou em 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: Eu não testei isso.

O ponto é que IABdeve ser capaz de aplicar um IABFunc para qualquer tipo de retorno Re um IABFuncdeve ser capaz de trabalhar em qualquer um dos Tsubtipos de IAe IB.

A intenção de DefaultIABé apenas agrupar um existente Tque subtipos IAe IB. Observe que isso é diferente do seu IAB : IA, IBque DefaultIABsempre pode ser adicionado a um existente Tposteriormente.

Referências:


A abordagem funciona se adicionarmos um tipo genérico de empacotador de objetos com os parâmetros T, IA, IB, com T restrito às interfaces, que encapsula uma referência do tipo T e permite Applyque ele seja invocado. O grande problema é que não há como usar uma função anônima para implementar uma interface; portanto, construções como essa acabam sendo uma verdadeira dor de usar.
Supercat

3

TypeScript é outra linguagem digitada que suporta tipos de interseção T & U(junto com tipos de união T | U). Aqui está um exemplo citado em sua página de documentação sobre tipos avançados :

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

O Ceilão tem suporte total para os tipos de união e interseção de primeira classe .

Você escreve um tipo de união como X | Ye um tipo de interseção como X & Y.

Melhor ainda, o Ceilão apresenta muitos argumentos sofisticados sobre esses tipos, incluindo:

  • instanciações principais: por exemplo, Consumer<X>&Consumer<Y>é do mesmo tipo Consumer<X|Y>que Consumeré contravariante Xe
  • disjunção: por exemplo, Object&Nullé do mesmo tipo que Nothing, do tipo inferior.

0

Todas as funções do C ++ têm um tipo de retorno fixo, mas se retornarem ponteiros, os ponteiros poderão, com restrições, apontar para tipos diferentes.

Exemplo:

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

O comportamento do ponteiro retornado dependerá de quais virtualfunções são definidas e você pode fazer um downcast verificado com, por exemplo,

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

É assim que o polimorfismo funciona em C ++.


E, é claro, pode-se usar um anyou um varianttipo, como o modelo fornece. Assim, a restrição não fica.
Deduplicator

Este não é o que a questão estava pedindo, porém, que era uma maneira de especificar que o tipo de retorno se estende duas superclasses identificadas, ao mesmo tempo, ou seja, class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... agora que tipo podemos especificar que permite o retorno quer Derived1ou Derived2mas nem Base1nem Base2diretamente?
Jules

-1

Pitão

É muito, muito fortemente tipado.

Mas o tipo não é declarado quando uma função é criada; portanto, os objetos retornados são "ambíguos".

Na sua pergunta específica, um termo melhor pode ser "Polimórfico". Esse é o caso de uso comum no Python é retornar tipos de variantes que implementam uma interface comum.

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

Como o Python é fortemente digitado, o objeto resultante será uma instância de Thisou Thate não poderá (facilmente) ser coagido ou convertido para outro tipo de objeto.


Isso é bastante enganador; enquanto o tipo de um objeto é praticamente imutável, os valores podem ser convertidos entre tipos com bastante facilidade. Para str, por exemplo, trivialmente.
James Youngman

1
@JamesYoungman: O que? Isso é verdade para todos os idiomas. Todos os idiomas que eu já vi têm conversões de to_string à esquerda, à direita e ao centro. Não recebo seu comentário. Você pode elaborar?
24512 S.Lott

Eu estava tentando entender o que você quis dizer com "Python é fortemente digitado". Talvez eu tenha entendido mal o que você quis dizer com "fortemente tipado". Francamente, Python tem algumas das características que eu associaria a linguagens fortemente tipadas. Por exemplo, ele aceita programas nos quais o tipo de retorno de uma função não é compatível com o uso do valor pelo chamador. Por exemplo, "x, y = F (z)" onde F () retorna (z, z, z).
James Youngman

O tipo de um objeto Python não pode (sem mágica séria) ser alterado. Não há operador "cast", como existe com Java e C ++. Isso torna cada objeto fortemente digitado. Nomes de variáveis ​​e nomes de funções não têm ligação de tipo, mas os próprios objetos são fortemente digitados. O conceito chave aqui não é a presença de declarações. O conceito principal é a disponibilidade de operadores de elenco. Observe também que isso me parece factual; moderadores podem contestar isso, no entanto.
31512 S.Lott

1
As operações de conversão em C e C ++ também não alteram o tipo de seu operando.
James Youngman
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.