Что случилось с классами классов?

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

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

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

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

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

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

Если бы нам понадобилась абстракция для моноида, мы могли бы неплохо написать интерфейс и позволить нашим типам реализовать это, но встроенные типы, такие как 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

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

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

Ответы

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

Вы хотите невиртуальный ad hoc-полиморфизм.

  • ad hoc: реализация может варьироваться.
  • nonvirtual: по соображениям производительности; отправка compiletime

Остальное - это сахар, на мой взгляд.

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

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

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

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

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

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

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

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

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

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

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

 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) и, следовательно, способен скомпилировать эту строку.

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

Я не вижу реальной необходимости в instanciation конструкции класса типа. Если что-то не получится при компиляции, мы получим ошибки ограничений или если они были связаны вместе с "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 interface" в список поддерживаемых интерфейсов.

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

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

Ответ 4

Какова аргументация против типов?

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

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

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

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

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

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

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

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

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

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

Ответ 5

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

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

Каковы аргументы против [type classes]?

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

Вкратце рассмотрим, как классы типов отличаются от интерфейсов на таких языках, как Java или С#. На этих языках класс поддерживает только интерфейсы, которые явно упоминаются и реализуются в определении этого класса. Классы типов, однако, являются интерфейсами, которые могут быть позже добавлены к любому уже определенному типу, даже в другом модуле. Этот тип расширяемости типов, очевидно, сильно отличается от механизмов в некоторых "основных" языках OO.


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

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

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

С++. Как вы уже сказали, концепции были исключены из спецификации С++ 11.

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

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

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

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

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

( VB.NET. Microsoft некоторое время "совместно развивает" языки С# и VB.NET, поэтому мои утверждения о С# также действительны для VB.NET. )

Java. Я не очень хорошо знаю Java, но из языков С++, С# и Java это, вероятно, "самый чистый" язык OO. Я не вижу, как классы классов будут естественно вписываться в этот язык.

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

Ответ 6

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