Что не так с классами типов? - PullRequest
15 голосов
/ 28 ноября 2009

Классы типов кажутся отличной возможностью для написания универсальных и многократно используемых функций очень последовательным, эффективным и расширяемым способом. Но все же нет"mainstream-language" предоставляет их - наоборот: Концепции , которые являются весьма аналогичной идеей, были исключены из следующего C ++!

В чем причина против классов типов? По-видимому, многие языки ищут способ справиться с подобными проблемами: .NET ввел общие ограничения и интерфейсы, такие как IComparable, которые допускают такие функции, как

T Max<T>(T a, T b) where T : IComparable<T> { // }

для работы со всеми типами, которые реализуют интерфейс.

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

Но обе концепции, показанные здесь, имеют большие недостатки: интерфейсы основаны на наследовании и, следовательно, относительно медленны из-за косвенности, и, кроме того, нет возможности позволить существующему типу реализовать их.

Если нам нужна абстракция для Monoid, мы вполне можем написать интерфейс и позволить нашим типам реализовать это, но встроенные типы, такие как int, никогда не смогут работать с вашими функциями.

Вместо этого неявные параметры не соответствуют обычным интерфейсам / признакам.

С классами типов проблем не будет (псевдокод)

typeclass Monoid of A where
    static operator (+) (x : A, y : A) : A
    static val Zero : A 
end

instance Int of Monoid where
   static operator (+) (x : Int, y : Int) : Int = x + y
   static val Zero : Int = 0
end

Так почему бы нам не использовать классы типов? Есть ли у них серьезные недостатки?

Редактировать : Пожалуйста, не путайте классы типов со структурной типизацией, чистыми шаблонами C ++ или утиной типизацией. Класс типов явно создается по типам, а не просто удовлетворяется по соглашению . Более того, он может нести полезные реализации, а не просто определять интерфейс.

Ответы [ 6 ]

9 голосов
/ 28 ноября 2009

Концепции были исключены, потому что комитет не думал, что сможет их правильно уложить вовремя, и потому что они не считались необходимыми для выпуска. Не то, чтобы они не думали, что они хорошая идея, они просто не думают, что выражение их для C ++ является зрелым: http://herbsutter.wordpress.com/2009/07/21/trip-report/

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

Концепции пытаются запретить передачу параметра шаблона, который не удовлетворяет требованиям шаблона. Но в то время, когда компилятор обращается к параметру шаблона, уже проверяет, что это правильно, даже без Concepts. Если вы попытаетесь использовать его так, как он не поддерживает, вы получите ошибку компилятора [*]. В случае интенсивного использования шаблона вы можете получить три экрана, заполненных угловыми скобками, но в принципе это информативное сообщение. Необходимость отлавливать ошибки до неудачной компиляции менее актуальна, чем необходимость отлавливать ошибки до неопределенного поведения во время выполнения.

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

В ответ на ваш вопрос - любое формальное утверждение «Я реализую этот интерфейс» имеет один большой недостаток, заключающийся в том, что он требует, чтобы интерфейс был изобретен до реализации. Системы вывода типов не имеют, но они имеют большой недостаток, заключающийся в том, что языки в целом не могут выразить весь интерфейс с использованием типов, и поэтому у вас может быть объект, который, как предполагается, имеет правильный тип, но который не имеет семантика приписывается этому типу. Если ваш язык обращается к интерфейсам вообще (в частности, если он соответствует их классам), то AFAIK, вы должны занять здесь позицию и выбрать свой недостаток.

[*] Обычно. Есть некоторые исключения, например, система типов C ++ в настоящее время не запрещает вам использовать входной итератор, как если бы он был прямым итератором. Для этого вам нужны черты итератора. Один только ввод утки не мешает вам пройти мимо объекта, который ходит, плавает и крякает, но при внимательном рассмотрении фактически не делает ничего из того, что делает утка, и удивляется, узнав, что вы думали, что это будет ;-)

4 голосов
/ 28 ноября 2009

Интерфейсы не должны быть основаны на наследовании ... это другое и отдельное проектное решение. Новый язык Go имеет интерфейсы, но не имеет наследования, например: «тип автоматически удовлетворяет любому интерфейсу, который указывает подмножество его методов», как говорит Go FAQ Это. рассуждений Simionato о наследовании и интерфейсах, вызванные недавним выпуском Go, возможно, стоит прочитать.

Я согласен, что классы типов являются еще более мощными, в основном потому, что, подобно абстрактным базовым классам , они позволяют дополнительно указывать полезный код (определяя дополнительный метод X в терминах других для всех типов, которые в противном случае соответствуют базовый класс, но сами не определяют X) - без наследственного багажа, который почти неизбежно несут ABC (в отличие от интерфейсов). Почти неизбежно, потому что, например, азбуки Python "заставляют поверить", что они включают наследование, с точки зрения концептуализации, которую они предлагают ... но, на самом деле, они не должны быть основаны на наследовании (многие просто проверка наличия и подписи определенных методов, как и интерфейсы Go).

Что касается того, почему разработчик языка (например, Гвидо, в случае с Python) выбирает таких «волков в овечьей шкуре», как азбука Python, вместо более простых классов типов, подобных Хаскеллу, которые я предлагал с 2002 года, на этот вопрос сложнее ответить. В конце концов, это не так, как будто Python имеет какую-либо услугу против заимствования концепций у Haskell (например, списочные выражения / выражения генератора - Python здесь нужна двойственность, в то время как Haskell нет, потому что Haskell «ленив»). Лучшая гипотеза, которую я могу предложить, заключается в том, что к настоящему времени наследование настолько знакомо большинству программистов, что большинство разработчиков языков считают, что они могут получить более легкое признание, создавая таким образом вещи (хотя дизайнеры Go должны быть благодарны за то, что они этого не делают).

1 голос
/ 09 ноября 2013

Позвольте мне начать жирным шрифтом: Я прекрасно понимаю мотивацию этого и не могу понять, почему некоторые люди спорят с этим ...

То, что вы хотите, это невиртуальный специальный полиморфизм.

  • ad hoc: реализация может варьироваться
  • не виртуальный: по причинам производительности; отправка во время компиляции

Остальное, по-моему, сахар.

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

C # просто не может это сделать. Подход, который не был бы невиртуальным : Если бы такие типы, как float, просто реализовали бы что-то вроде «INumeric» или «IAddable» (...), мы бы по крайней мере могли написать общий минимум , max, lerp и на основе этого зажима, maprange, bezier (...). Однако это не будет быстро. Вы этого не хотите.

Способы исправления: Так как .NET в любом случае выполняет JIT-компиляцию, он также генерирует другой код для List<int>, чем для List<MyClass> (из-за различий между типами значений и ссылочными типами), он, вероятно, не добавит столько накладных расходов, чтобы также генерировать другой код для рекламы специальные полиморфные части. Языку C # просто нужен способ выразить это. Один путь - это то, что вы набросали.

Другой способ - добавить ограничения типа в функцию , используя специальную полиморфную функцию:

    U SuperSquare<T, U>(T a) applying{ 
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    }
    {
        return Foo(a * a);
    }

Конечно, вы можете столкнуться с новыми и новыми ограничениями при реализации Bar, который использует Foo. Поэтому вам может понадобиться механизм присвоения имени нескольким ограничениям, которые вы регулярно используете ... Однако это опять-таки сахар, и один из способов подойти к нему - просто использовать концепцию класса типов ...

Назначение имени нескольким ограничениям похоже на определение класса типа, но я хотел бы просто рассмотреть его как своего рода механизм аббревиатур - сахар для произвольного набора ограничений типа функции:

    // adhoc is like an interface: it is about collecting signatures
    // but it is not a type: it dissolves during compilation 
    adhoc AMyNeeds<T, U>
    {
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    } 

    U SuperSquare<T, U>(T a) applying AMyNeeds<T, U>        
    {
        return Foo(a * a);
    }

В каком-то месте "main" все аргументы типа известны, и все становится конкретным и может быть скомпилировано вместе.

Чего не хватает, так это отсутствия создания разных реализаций. В верхнем примере мы просто использовали полиморфные функции и дали всем знать ...

Реализация затем может идти по пути методов расширения - в их способности добавлять функциональность к любому классу в любой точке:

 public static class SomeAdhocImplementations
 {
    public nonvirtual int Foo(float x)
    {
        return round(x);
    }
 }

В основном теперь можно написать:

    int a = SuperSquare(3.0f); // 3.0 * 3.0 = 9.0 rounded should return 9

Компилятор проверяет все «невиртуальные» специальные функции, находит как встроенный оператор float (*), так и int Foo (float) и, следовательно, может скомпилировать эту строку.

Специальный полиморфизм, конечно, идет с недостатком, который вы должны перекомпилировать для каждого типа времени компиляции, чтобы вставить правильные реализации. И, вероятно, IL не поддерживает это вложение в dll. Но, может быть, они все равно работают над этим ...

Не вижу реальной необходимости в создании конструкции класса типа. Если что-то не получится при компиляции, мы получим ошибки ограничений или, если они были связаны вместе с помощью кодека «adhoc», сообщение об ошибке может стать еще более читабельным.

    MyColor a = SuperSquare(3.0f); 
    // error: There are no ad hoc implementations of AMyNeeds<float, MyColor> 
    // in particular there is no implementation for MyColor Foo(float)

Но, конечно же, вполне возможно создание экземпляра класса типов / "интерфейса adhoc полиморфизма". Сообщение об ошибке тогда заявит: "The AMyNeeds constraint of SuperSquare has not been matched. AMyNeeds is available as StandardNeeds : AMyNeeds<float, int> as defined in MyStandardLib". Также было бы возможно поместить реализацию в класс вместе с другими методами и добавить «интерфейс adhoc» в список поддерживаемых интерфейсов.

Но не зависит от конкретного языкового дизайна: я не вижу недостатка в том, чтобы добавлять их так или иначе. Сохранение статически типизированных языков всегда должно выходить за границы выразительной силы, поскольку они начинались с того, что позволяли слишком мало, что, как правило, было бы меньшим набором выразительной мощности, которого нормальный программист ожидал бы возможным ...

tldr: я на вашей стороне. Подобные вещи - отстой в основных статически типизированных языках Хаскелл показал путь.

0 голосов
/ 03 января 2012

Но все еще «основной язык» не предоставляет [классов типов.]

Когда был задан этот вопрос, это могло быть правдой. Сегодня интерес к таким языкам, как Haskell и Clojure, значительно возрос. У Haskell есть классы типов (class / instance), Clojure 1.2+ имеет протоколов (defprotocol / extend).

В чем причина [классов типов]?

Я не думаю, что классы типов объективно "хуже", чем другие механизмы полиморфизма; они просто следуют другому подходу. Таким образом, реальный вопрос заключается в том, хорошо ли они вписываются в определенный язык программирования?

Давайте кратко рассмотрим, как классы типов отличаются от интерфейсов в таких языках, как Java или C #. В этих языках класс поддерживает только те интерфейсы, которые явно упомянуты и реализованы в определении этого класса. Классы типов, тем не менее, представляют собой интерфейсы, которые впоследствии могут быть добавлены к любому уже определенному типу, даже в другом модуле. Этот тип расширяемости типов, очевидно, сильно отличается от механизмов в некоторых «основных» языках ОО.


Давайте теперь рассмотрим классы типов для нескольких основных языков программирования.

Haskell : Нет необходимости говорить, что этот язык имеет классов типов .

Clojure : Как уже было сказано выше, Clojure имеет нечто вроде классов типов в виде протоколов .

C ++ : Как вы сказали сами, понятия были исключены из спецификации C ++ 11.

Напротив: понятия, которые являются весьма аналогичной идеей, были исключены из следующего C ++!

Я не следил за всей полемикой вокруг этого решения. Из того, что я прочитал, концепции еще не были «готовы»: все еще обсуждались концепции карт. Однако концепции не были полностью оставлены, ожидается, что они войдут в следующую версию C ++.

C # : С языковой версией 3 C # по сути стал гибридом парадигм объектно-ориентированного и функционального программирования. В язык было добавлено одно дополнение, концептуально очень похожее на классы типов: методы расширения . Основное отличие состоит в том, что вы (похоже) присоединяете новые методы к существующему типу, а не к интерфейсам.

(Конечно, механизм метода расширения не так элегантен, как синтаксис instance … where в Haskell. Методы расширений не «действительно» привязаны к типу, они реализованы как синтаксическое преобразование. В конце концов, это не не имеет большого практического значения.)

Не думаю, что это произойдет в ближайшее время & mdash; разработчики языка, вероятно, даже не добавят расширение properties к языку, а расширение interfaces будет даже дальше, чем это.

( VB.NET : Microsoft в течение некоторого времени «совместно развивала» языки C # и VB.NET, поэтому мои утверждения о C # оказались верными и для VB.NET.)

Java : Я не очень хорошо знаю Java, но из языков C ++, C # и Java это, пожалуй, самый «чистый» ОО-язык. Я не понимаю, как классы типов вписались бы в этот язык естественным образом.

F # : Я нашел сообщение на форуме, объясняющее , почему классы типов могут никогда не появиться в F # . Это объяснение основано на том факте, что F # имеет номинативную, а не структурную систему типов. (Хотя я не уверен, является ли это достаточным основанием для F # не иметь классов типов.)

0 голосов
/ 11 сентября 2010

В чем причина против классов типов?

Сложность реализации для разработчиков компиляторов всегда вызывает озабоченность при рассмотрении новых языковых возможностей. C ++ уже допустил эту ошибку, и мы уже страдали годами ошибочных компиляторов C ++.

Интерфейсы основаны на наследовании и, следовательно, относительно медленны из-за косвенности, и, кроме того, нет возможности позволить существующему типу реализовать их

Не правда. Посмотрите на структурно типизированную объектную систему OCaml, например:

# let foo obj = obj#bar;;
val foo : < bar : 'a; .. > -> 'a = <fun>

Эта функция foo принимает любой объект любого типа, который обеспечивает необходимый метод bar.

То же самое для модульной системы высшего порядка ML. Действительно, существует даже формальная эквивалентность между этим и классами типов. На практике классы типов лучше подходят для мелкомасштабных абстракций, таких как перегрузка операторов, в то время как модули более высокого порядка лучше подходят для крупномасштабных абстракций, таких как параметризация Окасаки списков, которые можно катить по очередям.

Есть ли у них серьезные недостатки в конце концов?

Посмотрите на собственный пример, общая арифметика. F # уже может обрабатывать этот конкретный случай благодаря интерфейсу INumeric. Тип F # Matrix даже использует этот подход.

Однако вы только что заменили машинный код для добавления динамической диспетчеризацией на отдельную функцию, что делает арифметические порядки медленнее. Для большинства приложений это бесполезно медленно. Вы можете решить эту проблему, выполнив целую оптимизацию программы, но это имеет очевидные недостатки. Более того, существует небольшая общность между числовыми методами для int против float из-за числовой устойчивости, поэтому ваша абстракция также практически бесполезна.

Вопрос, безусловно, должен звучать так: может ли кто-нибудь убедительно аргументировать за принятие классов типов?

0 голосов
/ 28 ноября 2009

Попробуйте определить Matroid, что мы и делаем (логически, а не устно, говоря Matroid), и это все еще, вероятно, что-то вроде структуры Си. Принцип Лискова (последний призер Тьюринга) становится слишком абстрактным, слишком категоричным, слишком теоретическим, менее обработанным фактическими данными и более чисто теоретической системой классов, для практического решения прагматических проблем, кратко взглянул на него, который выглядел как PROLOG, код о код о коде о коде ... в то время как алгоритм описывает последовательности и путешествия, которые мы понимаем, на бумаге или доске. Зависит от того, какая у вас цель, решение проблемы с минимальным кодом или наиболее абстрактным.

...