Какие статически типизированные языки поддерживают типы пересечений для возвращаемых значений функции?


17

Начальная нота:

Этот вопрос был закрыт после нескольких правок, потому что мне не хватало правильной терминологии, чтобы точно указать, что я искал. Затем Сэм Тобин-Хохштадт опубликовал комментарий, который позволил мне точно понять, что это было: языки программирования, которые поддерживают типы пересечений для возвращаемых значений функций.

Теперь, когда вопрос был вновь открыт, я решил улучшить его, переписав его (надеюсь) более точно. Поэтому некоторые ответы и комментарии ниже могут больше не иметь смысла, поскольку они ссылаются на предыдущие изменения. (Пожалуйста, смотрите историю редактирования вопроса в таких случаях.)

Существуют ли какие-либо популярные статически и строго типизированные языки программирования (такие как Haskell, универсальный Java, C #, F # и т. Д.), Которые поддерживают типы пересечений для возвращаемых значений функции? Если да, то что и как?

(Если честно, мне бы очень хотелось, чтобы кто-то продемонстрировал способ выражения типов пересечений в основном языке, таком как C # или Java.)

Я приведу краткий пример того, как могут выглядеть типы пересечений, используя некоторый псевдокод, похожий на 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();
}

То есть функция fnвозвращает экземпляр некоторого типа T, о котором вызывающая сторона знает только то, что она реализует интерфейсы IXи IY. (То есть, в отличие от обобщений, вызывающая сторона не может выбрать конкретный тип T- функция делает. Из этого я бы предположил, что Tна самом деле это не универсальный тип, а экзистенциальный тип.)

PS: я знаю, что можно просто определить interface IXY : IX, IYи изменить тип возвращаемого значения fnна IXY. Однако это не совсем то же самое, потому что часто вы не можете подключить дополнительный интерфейс IXYк ранее определенному типу, Aкоторый реализуется только IXи IYотдельно.


Сноска. Некоторые ресурсы о типах перекрестков:

Статья в Википедии для «Системы типов» содержит подраздел о типах пересечений .

Доклад Бенджамина С. Пирса (1991), «Программирование с типами пересечений, типов соединений и полиморфизма»

Дэвид П. Каннингем (2005), "Типы пересечений на практике" , который содержит тематическое исследование о языке Форсайт, которое упоминается в статье Википедии.

Вопрос переполнения стека, «Типы объединения и типы пересечений», который получил несколько хороших ответов, среди них этот, который дает пример псевдокода типов пересечений, подобных моему выше.


6
Как это неоднозначно? Tопределяет тип, даже если он просто определен в объявлении функции как «некоторый тип, который расширяет / реализует IXи IY». Тот факт, что фактическое возвращаемое значение является частным случаем этого ( Aили, Bсоответственно), здесь не является чем-то особенным, вы также можете достичь этого, используя Objectвместо T.
Йоахим Зауэр

1
Ruby позволяет вам возвращать из функции все, что вы хотите. То же самое для других динамических языков.
Торстен Мюллер

Я обновил свой ответ. @Joachim: я знаю, что термин «неоднозначный» не очень точно отражает рассматриваемое понятие, таким образом, пример для разъяснения предполагаемого значения.
stakx

1
Ad PS: ... который меняет ваш вопрос на «какой язык позволяет рассматривать тип Tкак интерфейс, Iкогда он реализует все методы интерфейса, но не объявляет этот интерфейс».
Ян Худек

6
Закрытие этого вопроса было ошибкой, потому что есть точный ответ - объединение типов . Типы объединения доступны на таких языках, как (Типовая ракетка) [ docs.racket-lang.org/ts-guide/] .
Сэм Тобин-Хохштадт

Ответы:


5

Scala имеет полные типы пересечений, встроенные в язык:

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

Скала, основанная на точках, будет иметь истинные типы пересечений, но не для текущей / бывшей скалы.
Хунсю Чен

9

На самом деле, очевидный ответ: Java

Хотя вас может удивить то, что Java поддерживает типы пересечений ... это действительно происходит через оператор ограничения типа "&". Например:

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

Посмотрите эту ссылку на множественные границы типов в Java, а также это из Java API.


Будет ли это работать, если вы не знаете тип во время компиляции? Т.е. можно написать <T extends IX & IY> T f() { if(condition) return new A(); else return new B(); }. А как вы вызываете функцию в таком случае? Ни A, ни B не могут появиться на сайте вызовов, потому что вы не знаете, какой из них вы получите.
Ян Худек

Да, вы правы - это не эквивалентно приведенному оригинальному примеру, поскольку вам нужно предоставить конкретный тип. Если бы мы могли использовать подстановочные знаки с границами пересечения, то у нас было бы это. Похоже, мы не можем ... и я действительно не знаю, почему нет (см. Это ). Но, тем не менее, у Java есть типы пересечений своего рода ...
redjamjar

1
Я чувствую разочарование, что я так или иначе никогда не узнал о типах пересечений за те 10 лет, что я занимался Java. Теперь, когда я все время использую Flowtype, я подумал, что это самая большая недостающая функция в Java, но обнаружил, что я никогда не видел их в дикой природе. Люди серьезно их не используют. Я думаю, что если бы они были более известны, то структуры внедрения зависимостей, такие как Spring, никогда бы не стали такими популярными.
Энди

8

Оригинальный вопрос, заданный для «неоднозначного типа». Для этого ответ был:

Двусмысленного типа, очевидно, нет. Звонящий должен знать, что они получат, поэтому это невозможно. Все, что может вернуть любой язык, - это либо базовый тип, либо интерфейс (возможно, автоматически сгенерированный как в типе пересечения), либо динамический тип (а динамический тип - это просто тип с вызовами по имени, get и set).

Предполагаемый интерфейс:

Так что в основном вы хотите, чтобы вернуть интерфейс , IXYкоторый наследуется IX и IY хотя этот интерфейс не был объявлен ни в одном Aили B, возможно , потому , что не была объявлена , когда были определены эти типы. В этом случае:

  • Любой, который динамически типизирован, очевидно.
  • Я не помню ни статически типизированный язык мейнстрим будет иметь возможность создавать интерфейс (это объединение типа Aи Bили типа пересечения IXи IY) сам.
  • GO , потому что классы реализуют интерфейс, если они имеют правильные методы, даже не объявляя их. Таким образом, вы просто объявляете интерфейс, который получает их там и возвращает их.
  • Очевидно, что любой другой язык, где тип может быть определен для реализации интерфейса вне определения этого типа, но я не думаю, что помню какой-либо другой, кроме GO.
  • Это невозможно в любом типе, где реализация интерфейса должна быть определена в самом определении типа. Однако в большинстве из них можно обойтись, определив оболочку, которая реализует два интерфейса и делегирует все методы обернутому объекту.

PS сильно типизированный язык является тот , в котором объект данного типа не может рассматриваться как объект другого типа, в то время как слабо типизированный язык является тот , который имеет переинтерпретировать оттенок. Таким образом, все динамически типизированные языки строго типизированы , в то время как слабо типизированные языки являются ассемблерными, C и C ++, причем все три типизированы статически .


Это не правильно, в типе нет ничего неоднозначного. Язык может возвращать так называемые «типы пересечений» - это очень мало, если это делают какие-либо основные языки.
redjamjar

@redjamjar: у вопроса была другая формулировка, когда я ответил на него и спросил «двусмысленный тип». Вот почему это начинается с этого. Вопрос был значительно переписан с тех пор. Я расширю ответ, чтобы упомянуть как оригинальную, так и текущую формулировку.
Ян Худек

извини, я пропустил это, очевидно!
Redjamjar

+1 за упоминание Голанга, который, вероятно, является лучшим примером общего языка, который позволяет это, даже если способ сделать это немного окольным.
Жюль

3

Go Язык программирования вид имеет, но только для типов интерфейсов.

В Go любой тип, для которого определены правильные методы, автоматически реализует интерфейс, поэтому возражения в вашем PS не применяются. Другими словами, просто создайте интерфейс, в котором есть все операции типов интерфейсов, которые нужно объединить (для которых есть простой синтаксис), и все это просто работает.

Пример:

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

Вы можете сделать то, что вы хотите, используя ограниченный экзистенциальный тип, который может быть закодирован на любом языке с обобщениями и ограниченным полиморфизмом, например, C #.

Тип возвращаемого значения будет примерно таким (в коде psuedo)

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

или в 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);
    }
}

Примечание: я не проверял это.

Дело в том, что IABдолжен быть в состоянии применить IABFunc для любого возвращаемого типа R, и он IABFuncдолжен быть в состоянии работать с любыми Tподтипами IAи IB.

Цель DefaultIABсостоит в том, чтобы просто обернуть существующие Tподтипы IAи IB. Обратите внимание, что это отличается от вашего, IAB : IA, IBчто DefaultIABвсегда может быть добавлено к существующему Tпозже.

Ссылки:


Подход работает, если добавить общий тип объекта-обертки с параметрами T, IA, IB, с T, ограниченным интерфейсами, который инкапсулирует ссылку типа T и позволяет Applyвызывать ее. Большая проблема заключается в том, что нет способа использовать анонимную функцию для реализации интерфейса, поэтому такие конструкции в конечном итоге становятся реальной болью в использовании.
суперкат

3

TypeScript - это другой типизированный язык, который поддерживает типы пересечений T & U(наряду с типами объединения T | U). Вот пример, приведенный на их странице документации о расширенных типах :

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

Цейлон имеет полную поддержку первоклассных типов соединений и пересечений .

Вы пишете тип объединения как X | Yи тип пересечения как X & Y.

Более того, на Цейлоне есть много сложных рассуждений об этих типах, в том числе:

  • основные инстанции: например, Consumer<X>&Consumer<Y>тот же тип, что и Consumer<X|Y>если он Consumerявляется контравариантным X, и
  • несвязность: например, Object&Nullтот же тип Nothing, что и нижний тип.

0

Все функции C ++ имеют фиксированный тип возвращаемого значения, но если они возвращают указатели, указатели могут, с ограничениями, указывать на разные типы.

Пример:

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

Поведение возвращаемого указателя будет зависеть от того, какие virtualфункции определены, и вы можете выполнить проверку с помощью, скажем,

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

Вот как полиморфизм работает в C ++.


И, конечно, можно использовать тип anyили variantтип, как это предусмотрено в шаблонах Boost. Таким образом, ограничение не остается.
дедупликатор

Это не совсем то, о чем спрашивал вопрос, хотя для того, чтобы указать, что возвращаемый тип расширяет два идентифицированных суперкласса одновременно, то есть class Base1{}; class Base2{}; class Derived1 : public Base1, public Base2 {}; class Derived2 : public Base1, public Base2 {}... теперь какой тип мы можем указать, который позволяет возвращать либо одно, Derived1либо Derived2нет Base1ни Base2прямо?
Жюль

-1

питон

Это очень, очень сильно напечатано.

Но тип не объявляется при создании функции, поэтому возвращаемые объекты являются «неоднозначными».

В вашем конкретном вопросе лучшим термином может быть «Полиморфный». Это общий случай использования в Python - возвращать типы вариантов, которые реализуют общий интерфейс.

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

Поскольку Python строго типизирован, результирующий объект будет экземпляром Thisили Thatне может (легко) быть принудительно приведен или приведен к объекту другого типа.


Это вводит в заблуждение; в то время как тип объекта в значительной степени неизменен, значения могут быть легко преобразованы между типами. К примеру, тривиально.
Джеймс Янгман

1
@JamesYoungman: что? Это верно для всех языков. Все языки, которые я когда-либо видел, имеют преобразования to_string слева, справа и по центру. Я не получаю ваш комментарий вообще. Можете ли вы уточнить?
S.Lott

Я пытался понять, что вы имели в виду под "Python строго типизирован". Возможно, я неправильно понял, что вы имели в виду под «строго типизированным». Откровенно говоря, Python имеет несколько характеристик, которые я бы ассоциировал со строго типизированными языками. Например, он принимает программы, в которых тип возвращаемого значения функции несовместим с использованием значения вызывающей стороной. Например, «x, y = F (z)», где F () возвращает (z, z, z).
Джеймс Янгман

Тип объекта Python нельзя (без серьезной магии) изменить. Оператора «cast» нет, как в случае с Java и C ++. Это делает каждый объект строго типизированным. Имена переменных и имена функций не имеют привязки типов, но сами объекты строго типизированы. Ключевой концепцией здесь является не наличие деклараций. Ключевой концепцией является доступность операторов приведения. Также отметьте, что это кажется мне фактическим; модераторы могут оспаривать это, однако.
S.Lott

1
Операции приведения C и C ++ также не меняют тип своего операнда.
Джеймс Янгман
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.