Сравнение обобщений C # с параметризованными типами Haskell - PullRequest
21 голосов
/ 06 апреля 2009

Основываясь на совете, который я нашел в StackOverflow, я копаюсь в Haskell. Мне было приятно видеть, что параметризованные типы Haskell очень похожи на дженерики C #. Оба языка рекомендуют одну букву для параметра типа (обычно), и оба языка, похоже, следуют аналогичному процессу для замены фактического типа для параметра типа. Из-за этого я довольно быстро ухватился за эту идею.

Что приводит к этому: чем отличаются параметризованные типы Haskell от универсальных типов C #? Из изучения Ruby я знаю, что вы можете столкнуться с большими трудностями, думая, что концепция, с которой вы знакомы на одном языке, совпадает с тем, на котором вы новичок. Обычно проблема усугубляется, когда функции очень похожи ... потому что они обычно не 100% одинаковы. Так какие же «ошибки» я мог бы укусить, если бы предположил, что я понимаю параметризованные типы, основываясь на моих знаниях об обобщениях C #?

Спасибо.

Ответы [ 4 ]

31 голосов
/ 06 апреля 2009

Вот одно отличие, о котором следует помнить:

C # имеет подтипы, но у Haskell нет, что означает, с одной стороны, что вы знаете больше вещей, просто взглянув на тип Haskell.

id :: a -> a

Эта функция Haskell принимает значение типа и возвращает то же значение того же типа. Если вы дадите ему Bool, он вернет Bool. Дайте ему Int, он вернет Int. Дайте ему Person, он вернет Person.

В C # вы не можете быть так уверены. Это та «функция» в C #:

public T Id<T>(T x);

Теперь из-за подтипа вы можете назвать это так:

var pers = Id<Person>(new Student());

Хотя pers имеет тип Person, аргумент функции Id - нет. На самом деле pers может иметь более конкретный тип, чем просто Person. Person может даже быть абстрактным типом, гарантируя, что pers будет иметь более конкретный тип.

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


И, во-вторых, в Haskell есть ad hoc полиморфизм (он же перегрузка) через механизм, известный как «классы типов».

equals :: Eq a => a -> a -> Bool

Эта функция проверяет, равны ли два значения. Но не только любые два значения, а только значения, которые имеют экземпляры для класса Eq. Это своего рода ограничения на параметры типа в C #:

public bool Equals<T>(T x, T y) where T : IComparable

Однако есть разница. С одной стороны, подтип: вы можете создать его с помощью Person и вызвать его с помощью Student и Teacher.

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

Принимая во внимание, что код на Haskell соответствует примерно так:

equals :: EqDict -> a -> a -> Bool

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

b1 = equals 2 4          --> b1 = equals intEqFunctions 2 4
b2 = equals True False   --> b2 = equals boolEqFunctions True False

Это также показывает, что делает подтип такой болью, представьте, если это возможно.

b3 = equals someStudent someTeacher
     --> b3 = equals personEqFunctions someStudent someTeacher

Как определить словарь personEqFunctions, если Student равно Teacher? У них даже нет одинаковых полей.

Короче говоря, хотя ограничения типа Haskell на первый взгляд могут выглядеть как ограничения типа .NET, они реализованы совершенно по-разному и компилируются в две действительно разные вещи.

18 голосов
/ 07 апреля 2009

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

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

Я могу сделать класс, скажем, списки,

class Listy a where

    data List a 
             -- this allows me to write a specific representation type for every particular 'a' I might store!

    empty   :: List a
    cons    :: a -> List a -> List a
    head    :: List a -> a
    tail    :: List a -> List a

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

map :: (Listy a, Listy b) => (a -> b) -> List a -> List b
map f as = go as
  where
    go xs
        | null xs   = empty
        | otherwise = f (head xs) `cons` go (tail xs)

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

Теперь это класс для общего списка. Я могу дать конкретные хитрые представления, основанные на типах элементов. Так, например для списков Int, я мог бы использовать массив:

instance Listy Int where

data List Int = UArray Int Int

...

Так что вы можете начать делать довольно мощное общее программирование.

7 голосов
/ 16 июня 2009

Другое большое отличие состоит в том, что универсальные шаблоны C # не допускают абстрагирование над конструкторами типов (т.е. видами, отличными от *), в то время как это делает Haskell. Попробуйте перевести следующий тип данных в класс C #:

newtype Fix f = In { out :: f (Fix f) }
6 голосов
/ 15 июня 2009

В продолжение вопроса «вы можете столкнуться с большими трудностями, думая, что концепция, с которой вы знакомы по одному языку, совпадает с другим языком [для которого вы новичок]»:

Вот ключевое отличие (скажем, от Ruby), которое вы должны понимать, когда используете классы типа Haskell. Учитывая функцию, такую ​​как

add :: Num a => a -> a -> a
add x y = x + y

Это не означает, что x и y являются любыми типами класса Num. Это означает, что x и y относятся к одному и тому же типу, тип которого относится к классу Num. «Ну, конечно, вы говорите; a - это то же самое, что и a ». Что я и говорю, но мне потребовалось много месяцев, чтобы перестать думать, что если x было бы Int, а y было бы Integer, это было бы все равно, что добавить Fixnum и Bignum в Ruby , Скорее всего:

*Main> add (2::Int) (3::Integer)

<interactive>:1:14:
    Couldn't match expected type `Int' against inferred type `Integer'
    In the second argument of `add', namely `(3 :: Integer)'
    In the expression: add (2 :: Int) (3 :: Integer)
    In the definition of `it': it = add (2 :: Int) (3 :: Integer)

Другими словами, создание подклассов (хотя оба этих Num экземпляра, конечно, также являются экземплярами Eq) и утки не прошли, детка.

Это звучит довольно просто и очевидно, но требуется некоторое время, чтобы научиться понимать это инстинктивно, а не просто интеллектуально, по крайней мере, если вы пришли из Java и Ruby.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...