Które języki o typie statycznym obsługują typy przecięć dla zwracanych wartości funkcji?


17

Uwaga wstępna:

To pytanie zostało zamknięte po kilku edycjach, ponieważ brakowało mi odpowiedniej terminologii, aby dokładnie określić, czego szukałem. Sam Tobin-Hochstadt opublikował następnie komentarz, dzięki któremu rozpoznałem dokładnie, co to było: języki programowania obsługujące typy przecięć dla zwracanych wartości funkcji.

Teraz, gdy pytanie zostało ponownie otwarte, postanowiłem je poprawić, przepisując je w (mam nadzieję) bardziej precyzyjny sposób. Dlatego niektóre poniższe odpowiedzi i komentarze mogą nie mieć sensu, ponieważ odnoszą się do poprzednich edycji. (W takich przypadkach zapoznaj się z historią edycji pytania).

Czy są jakieś popularne statycznie i silnie typowane języki programowania (takie jak Haskell, ogólna Java, C #, F # itp.), Które obsługują typy przecięć dla zwracanych wartości funkcji? Jeśli tak, to jakie i jak?

(Szczerze mówiąc, bardzo chciałbym zobaczyć, jak ktoś demonstruje sposób wyrażania typów skrzyżowań w głównym nurcie, takim jak C # lub Java.)

Dam szybki przykład tego, jak mogą wyglądać typy skrzyżowań, używając jakiegoś pseudokodu podobnego do 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();
}

Oznacza to, że funkcja fnzwraca instancję pewnego typu T, o której program wywołujący wie tylko, że implementuje interfejsy IXi IY. (To znaczy, w przeciwieństwie do generycznych, osoba wywołująca nie może wybrać konkretnego typu T- funkcja działa. Z tego przypuszczam, że Tw rzeczywistości nie jest to typ uniwersalny, ale typ egzystencjalny.)

PS: Wiem, że można po prostu zdefiniować a interface IXY : IX, IYi zmienić typ zwracanego fnna IXY. Jednak tak naprawdę nie jest to to samo, ponieważ często nie można podłączyć dodatkowego interfejsu IXYdo wcześniej zdefiniowanego typu, Aktóry tylko implementuje IXi IYosobno.


Przypis: Niektóre zasoby na temat typów skrzyżowań:

Artykuł w Wikipedii dla „Systemu typów” zawiera podrozdział o typach skrzyżowań .

Raport Benjamina C. Pierce'a (1991), „Programowanie z typami skrzyżowań, typami Unii i polimorfizmem”

David P. Cunningham (2005), „Typy skrzyżowań w praktyce” , który zawiera studium przypadku dotyczące języka Forsythe, o którym wspomniano w artykule w Wikipedii.

Pytanie przepełnienia stosu, „Typy Unii i typy skrzyżowań”, które otrzymało kilka dobrych odpowiedzi, w tym to, które daje pseudokodowy przykład typów skrzyżowań podobny do mojego powyżej.


6
Jak to jest niejednoznaczne? Tdefiniuje typ, nawet jeśli jest on zdefiniowany w deklaracji funkcji jako „jakiś typ, który rozszerza / implementuje IXi IY”. Fakt, że faktyczna wartość zwracana jest szczególnym przypadkiem tego ( Alub Bodpowiednio), nie jest tu niczym szczególnym, równie dobrze można to osiągnąć, używając Objectzamiast tego T.
Joachim Sauer

1
Ruby pozwala ci zwrócić cokolwiek chcesz z funkcji. To samo dotyczy innych dynamicznych języków.
thorsten müller

Zaktualizowałem swoją odpowiedź. @Jachachim: Zdaję sobie sprawę z tego, że termin „niejednoznaczny” nie oddaje bardzo dokładnie omawianej koncepcji, stąd przykład wyjaśniający zamierzone znaczenie.
stakx

1
Reklama PS: ... która zmienia pytanie na „który język pozwala traktować typ Tjako interfejs, Igdy implementuje on wszystkie metody interfejsu, ale nie zadeklarował tego interfejsu”.
Jan Hudec

6
Błędem było zamknięcie tego pytania, ponieważ istnieje precyzyjna odpowiedź, która dotyczy typów związków . Typy unijne są dostępne w językach takich jak (Typed Racket) [ docs.racket-lang.org/ts-guide/] .
Sam Tobin-Hochstadt

Odpowiedzi:


5

Scala ma pełne typy skrzyżowań wbudowane w język:

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 oparta na dotty miałaby prawdziwe typy przecięć, ale nie dla obecnej / poprzedniej scali.
Hongxu Chen,

9

W rzeczywistości oczywistą odpowiedzią jest: Java

Chociaż może Cię zaskoczyć informacja, że ​​Java obsługuje typy skrzyżowań ... tak naprawdę dzieje się to za pośrednictwem operatora powiązanego z typem „&”. Na przykład:

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

Zobacz ten link w wielu typach granic w Javie, a także w interfejsie API Java.


Czy to zadziała, jeśli nie znasz typu w czasie kompilacji? Czyli można pisać <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. Jak w takim przypadku wywołujesz funkcję? Ani A, ani B nie mogą pojawić się na stronie połączenia, ponieważ nie wiesz, którą otrzymasz.
Jan Hudec

Tak, masz rację --- nie jest to odpowiednik podanego oryginalnego przykładu, ponieważ musisz podać konkretny typ. Gdybyśmy mogli użyć symboli wieloznacznych z granicami przecięcia, mielibyśmy to. Wydaje się, że nie możemy ... i tak naprawdę nie wiem, dlaczego nie (zobacz to ). Ale wciąż Java ma coś w rodzaju przecięcia ...
redjamjar,

1
Czuję się rozczarowany, że jakoś nigdy nie dowiedziałem się o typach skrzyżowań w ciągu 10 lat, kiedy tworzyłem Javę. Teraz, gdy cały czas korzystam z Flowtype, pomyślałem, że są największą brakującą funkcją w Javie, ale okazało się, że nigdy nie widziałem ich na wolności. Ludzie poważnie ich nie wykorzystują. Myślę, że gdyby były one lepiej znane, ramy wstrzykiwania zależności, takie jak Spring, nigdy nie stałyby się tak popularne.
Andy

8

Pierwotne pytanie dotyczyło „dwuznacznego typu”. Dlatego odpowiedzią było:

Dwuznaczny typ, oczywiście żaden. Dzwoniący musi wiedzieć, co dostanie, więc nie jest to możliwe. Każdy język, który może zwrócić, jest albo typem bazowym, interfejsem (ewentualnie generowanym automatycznie jak w typie skrzyżowania) lub typem dynamicznym (a typ dynamiczny to po prostu typ z metodami wywoływania według nazw, pobierania i ustawiania).

Wnioskowany interfejs:

Więc w zasadzie chcesz, aby powrócić interfejs IXY, który wywodzi się IX i IY mimo, że interfejs nie został ogłoszony w jednej Alub B, ewentualnie, ponieważ nie został ogłoszony, gdy zostały określone te typy. W tym wypadku:

  • Wszystko, co jest dynamicznie wpisywane, oczywiście.
  • Nie pamiętam, aby jakikolwiek statycznie typowany język głównego nurtu byłby w stanie wygenerować interfejs (jest to typ uniiA i Blub typ przecięcia IXi IY).
  • GO , ponieważ to klasy implementują interfejs, jeśli mają poprawne metody, bez ich deklarowania. Więc po prostu zadeklarujesz interfejs, który wyprowadza te dwa tam i zwrócisz go.
  • Oczywiście każdy inny język, w którym można zdefiniować typ, aby zaimplementować interfejs poza definicją tego typu, ale nie sądzę, żebym pamiętał inny niż GO.
  • Nie jest to możliwe w żadnym typie, w którym implementacja interfejsu musi być zdefiniowana w samej definicji typu. Można jednak obejść większość z nich, definiując opakowanie, które implementuje dwa interfejsy i przekazuje wszystkie metody do zawiniętego obiektu.

PS Język silnie typowany to taki, w którym obiekt danego typu nie może być traktowany jako obiekt innego typu, natomiast język słabo typowany to taki, który ma rzutowaną reinterpretację. Tak więc wszystkie języki dynamicznie typowane są silnie typowane , podczas gdy słabo typowane języki to asembler, C i C ++, wszystkie trzy są wpisywane statycznie .


To nie jest poprawne, nie ma nic dwuznacznego w typie. Język może zwracać tak zwane „typy skrzyżowań” - to tylko kilka, jeśli robią to języki głównego nurtu.
redjamjar

@redjamjar: Pytanie miało inne brzmienie, kiedy odpowiedziałem na nie i poprosiłem o „dwuznaczny typ”. Dlatego zaczyna się od tego. Od tego czasu pytanie zostało znacznie przepisane. Rozszerzę odpowiedź, aby wymienić zarówno oryginalne, jak i obecne sformułowanie.
Jan Hudec

przepraszam, oczywiście mi tego brakowało!
redjamjar

+1 za wzmiankę o Golang, który jest prawdopodobnie najlepszym przykładem wspólnego języka, który pozwala na to, nawet jeśli sposób na zrobienie tego jest trochę okrężny.
Jules

3

Język programowania Go niby ma, ale tylko dla typów interfejsów.

W Go dowolny typ, dla którego zdefiniowano prawidłowe metody, automatycznie implementuje interfejs, więc sprzeciw w PS nie ma zastosowania. Innymi słowy, po prostu stwórz interfejs, który ma wszystkie operacje typów interfejsów, które mają być połączone (dla których istnieje prosta składnia) i wszystko działa.

Przykład:

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

Możesz być w stanie zrobić to, co chcesz, używając ograniczonego typu egzystencjalnego, który można zakodować w dowolnym języku z ogólnikami i ograniczonym polimorfizmem, np. C #.

Rodzaj zwrotu będzie podobny (w kodzie psuedo)

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

lub w 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);
    }
}

Uwaga: nie testowałem tego.

Chodzi o to, że IABmusi być w stanie zastosować IABFunc dla dowolnego typu zwrotu Ri IABFuncmusi być w stanie pracować na każdym Tpodtypie zarówno IAi IB.

Chodzi o DefaultIABto, aby owinąć istniejące, Tktóre podtypy IAi IB. Pamiętaj, że różni się on od twojego, IAB : IA, IBktóry DefaultIABzawsze można Tpóźniej dodać do istniejącego .

Bibliografia:


Podejście to działa, jeśli dodaje się ogólny typ opakowania obiektu z parametrami T, IA, IB, z ograniczeniem T do interfejsów, który zawiera odniesienie typu T i pozwala Applyna wywołanie go. Dużym problemem jest to, że nie ma sposobu na użycie anonimowej funkcji do implementacji interfejsu, więc takie konstrukcje stają się prawdziwym problemem.
supercat

3

TypeScript jest innym językiem pisanym, który obsługuje typy skrzyżowań T & U(wraz z typami unii T | U). Oto przykład cytowany ze strony dokumentacji na temat typów zaawansowanych :

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 ma pełne wsparcie dla najwyższej klasy typów złączy i skrzyżowań .

Piszemy typ połączenia jako X | Yi typ skrzyżowania jako X & Y.

Co więcej, Ceylon ma wiele wyrafinowanych argumentów na temat tych typów, w tym:

  • główne instancje: na przykład, Consumer<X>&Consumer<Y>jest tego samego typu, Consumer<X|Y>jakby Consumerbył sprzeczny z X, i
  • rozłączność: na przykład Object&Nulljest tego samego typu Nothing, co typ dolny.

0

Wszystkie funkcje w C ++ mają stały typ zwracania, ale jeśli zwracają wskaźniki, wskaźniki mogą, z ograniczeniami, wskazywać na różne typy.

Przykład:

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

Zachowanie zwróconego wskaźnika będzie zależeć od tego, jakie virtualfunkcje są zdefiniowane, i możesz wykonać sprawdzony downcast, powiedzmy,

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

Tak działa polimorfizm w C ++.


I oczywiście można użyć typu anylub varianttypu, jak zapewnia szablony boost. Zatem ograniczenie nie zostaje.
Deduplicator

Nie jest to jednak dokładnie to, o co pytało pytanie, które miało na celu określenie, że typ zwracany rozszerza dwie zidentyfikowane nadklasy w tym samym czasie, tj. class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... teraz jaki typ możemy określić, który pozwala na zwrócenie jednego Derived1lub Derived2obu Base1ani Base2bezpośrednio?
Jules

-1

Pyton

Jest bardzo, bardzo mocno napisany.

Ale typ nie jest deklarowany podczas tworzenia funkcji, więc zwracane obiekty są „niejednoznaczne”.

W twoim konkretnym pytaniu lepszym terminem może być „polimorficzny”. To typowy przypadek użycia w Pythonie to zwracanie typów wariantów, które implementują wspólny interfejs.

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

Ponieważ Python jest silnie typowany, wynikowy obiekt będzie instancją Thislub Thatnie będzie (łatwo) mógł zostać przymuszony ani rzutowany na inny typ obiektu.


Jest to dość mylące; podczas gdy typ obiektu jest prawie niezmienny, wartości można łatwo konwertować między typami. Na przykład trywialnie.
James Youngman

1
@JamesYoungman: Co? Dotyczy to wszystkich języków. Wszystkie języki, które kiedykolwiek widziałem, muszą następować po lewej, prawej i środkowej konwersji. W ogóle nie rozumiem twojego komentarza. Czy możesz rozwinąć?
S.Lott,

Próbowałem zrozumieć, co rozumiesz przez „Python jest silnie napisany”. Być może źle zrozumiałem, co miałeś na myśli, mówiąc „mocno wpisane”. Szczerze mówiąc Python ma kilka cech, które kojarzyłbym z silnie napisanymi językami. Na przykład akceptuje programy, w których typ zwracany przez funkcję nie jest zgodny z użyciem wartości przez osobę wywołującą. Na przykład „x, y = F (z)”, gdzie F () zwraca (z, z, z).
James Youngman

Typ obiektu Python nie może (bez poważnej magii) zostać zmieniony. Nie ma operatora „rzutowania”, jak ma to miejsce w przypadku Java i C ++. To sprawia, że ​​każdy obiekt jest mocno wpisany. Nazwy zmiennych i nazwy funkcji nie są powiązane typami, ale same obiekty są silnie typowane. Kluczową koncepcją tutaj nie jest obecność deklaracji. Kluczową koncepcją są dostępne operatory obsady. Zauważ też, że wydaje mi się to oparte na faktach; moderatorzy mogą jednak to kwestionować.
S.Lott,

1
Operacje rzutowania w C i C ++ również nie zmieniają typu ich operandów.
James Youngman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.