Классы типов Haskell и классы шаблонов C ++

Можно ли имитировать функциональность классов типов Haskell с помощью шаблонов C ++ (или C #)?

Есть ли в этом смысл или есть ли в этом выгода?

Я пытался создать класс Functor на C ++, но не смог. Я пробовал что-то вроде этого:

#include <iostream>
using namespace std;

//A function class to make types more readable
template <class input, class output> class Function {
private:
  output (*ptrfunc )(input);
public:
  Function(output (* ptr)(input)) {
    ptrfunc = ptr;
  }
  output call(input x) {return  (*ptrfunc)(x);}
  output operator() (input x) { return call(x);}
};


//the functor "typeclass"
template <class a> class Functor{
public:
  template <class b> Functor<b> fmap(Function<a,b> func);
};

// an container type to be declared "instance" of functor:
template <class a> class List : public Functor<a> { 
private:
  a * ptrList;
  int size;
public:
  List(int n) {  //constructor;
    ptrList = new a[n]; 
    size = n;
  }
  List(List<a> const& other) { //copy constructor
    size = other.size;
    ptrList = new a[size];
    for(int i = 0; i<size; i++)
      (*this)[i] = other[i];
  }
  ~List() { delete ptrList;} //destructor
  a& operator[](int i) { return ptrList[i];} // subscript operator just for easy notation
  const a& operator[](int i) const { return ptrList[i];}// subscript operator just for easy notation

  template <class b> List<b> fmap(Function<a,b> func) { //"instance" version of fmap
    List<b> temp(size);
    for(int i = 0; i < size; i++)
      temp[i] = func((*this)[i]);
    return temp;
  }
};


int test(int k) { return 2 * k;}

int main(void) {
  Function<int, int> func(&test);
  List<int> lista(10);
  for(int i = 0; i < 10; i++)
    lista[i] = i;
  List<int> lista2(lista.fmap(func));
  for(int i = 0; i < 10; i++)
    cout << lista2[i] << " ";
  cout << endl;
  return 0;
}

Он делает то, что должен делать, но имеет ли смысл использовать этот шаблон в C ++? Неужели это тот же образец, что и в haskell:

data List a = -- some stuff 

class  Functor f  where
fmap :: (a -> b) -> f a -> f b

instance (Functor List) where
-- some stuff    

Мне кажется, что это не то же самое, потому что в Functor f, f - это своего рода конструктор типа * -> *, а в моем определении, приведенном выше в Functor<a>, a - это не шаблон a<something>, а сам "содержащийся" тип данных.

Есть ли выход из этого? И что еще более важно: есть ли смысл пытаться скопировать такие шаблоны на C ++? Мне кажется, что C # больше похож на стиль функционального программирования, чем C ++. Есть ли способ сделать это на C #?


person Rafael S. Calsaverini    schedule 10.12.2010    source источник
comment
Хорошее практическое правило: нет, не стоит делать вид, будто вы программируете на другом языке, чем вы используете. Если вы хотите написать код на Haskell, напишите его для компилятора Haskell. Пока вы пишете код на C ++, вам лучше писать идиоматический C ++.   -  person jalf    schedule 10.12.2010
comment
возможно, osl.iu.edu/~kyross/pub/20040929 -type-class-slides.pdf или citeseerx.ist.psu.edu/viewdoc/ поможет. однако в вашем Functor случае базовый класс абсолютно бессмысленен (потому что fmap не может быть объявлен virtual).   -  person lijie    schedule 10.12.2010
comment
Но да, вы можете писать универсальные функторы, используя шаблоны. Хотя стиль этого будет немного отличаться от Haskal.   -  person Martin York    schedule 10.12.2010
comment
Я просто хочу отметить, что вы можете запутать программистов на C ++ термином «функтор», потому что в C ++ этим термином злоупотребляют для обозначения функционального объекта (который в некотором смысле является одним конкретным типом функтора). В haskell функтор означает категориальный функтор, а не функторы C ++ (функциональные объекты).   -  person snk_kid    schedule 10.12.2010
comment
Вы также можете прочитать концепции C ++ против повышения BCCL: stackoverflow.com/questions/1352571/. Это нигде не исчерпывающий ответ, но я считаю, что он помогает прояснить вопрос об удаленных концепциях C ++ 0x.   -  person Johannes Schaub - litb    schedule 11.12.2010
comment
Похоже, что для C ++ ответ положительный: stackoverflow.com/ questions / 2565097 / высшие-родственные-типы-с-с   -  person Erik Kaplun    schedule 02.03.2014


Ответы (5)


Можно ли имитировать функциональность классов типов Haskell с помощью шаблонов C ++ (или C #)?

Я недостаточно разбираюсь в шаблонах C ++, чтобы ответить на этот вопрос. Тем не менее, я могу немного поговорить об общих типах C #.

Короткий ответ - нет. Система классов «более высокого» типа в Haskell более мощная, чем система универсальных типов в C #.

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

interface IEnumerable<T> { ... }

«Универсальный» тип «IEnumerable одного параметра типа» на самом деле не является «типом» сам по себе, это шаблон, из которого вы можете создавать бесконечно много новых типов, заменяя аргументы типа («int») на параметры типа (« Т "). В этом смысле он «выше» нормального типа.

Вы можете наложить ограничения на параметры типов универсальных типов:

class C<T> where T : IEnumerable<int>

Универсальный тип C может быть сконструирован с любым аргументом типа, если аргумент типа является типом, который неявно преобразуется через преобразование ссылки или упаковки в IEnumerable<int>.

Но система типов Haskell идет еще дальше. Он поддерживает «классы типов», где ограничения, которые вы можете наложить на T, - это такие вещи, как «T имеет оператор равенства, определенный на нем». В C # операторы определены как статические методы, и нет аналога интерфейса для статических методов. В C # нет возможности обобщать многие типы на основе произвольных статических методов.

Примером, обычно приводимым для Haskell, является шаблон «монада». В нотации C # предположим, что у нас есть тип:

class MyMonad<T>
{
    public static MyMonad<TOut> Bind<TIn, TOut>(MyMonad<TIn>, Func<TIn, MyMonad<TOut>>) { ... }
    public MyMonad(T t) { ... }
}

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

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

class MyMonad<T>
{
    public MyMonad<TOut> Bind<TOut>(MyMonad<T>, Func<T, MyMonad<TOut>>) { ... }
    public MyMonad(T t) { ... }
}

Это помогает? Нет. Даже если оставить в стороне проблему с конструктором, мы не сможем придумать интерфейс, который фиксирует этот шаблон. Мы можем попробовать:

interface IMonad<T>
{
    public IMonad<TOut> Bind<TOut>(IMonad<T>, Func<T, IMonad<TOut>>);
}

Но это неправильно. Это говорит о том, что монада - это что-то, что принимает монаду и функцию, которая возвращает монаду, и возвращает монаду. Это означает, что у вас может быть две реализации IMonad<T>, скажем Maybe<T> и Sequence<T>, а затем иметь связыватель, который принимает последовательность и возвращает "возможно"! В этом нет никакого смысла; паттерн, который мы хотим запечатлеть, это

highertype Monad makes a pattern with TheImplementingType<T> like
{
    public TheImplementingType<TOut> Bind<TOut>(TheImplementingType<T>, Func<T, TheImplementingType<TOut>>);
}

но у нас нет способа выразить это на C #.

Давайте рассмотрим ваш пример Functor. В C # у нас может быть тип

class List<T> 
{
    public static List<TOut> Map<TIn, TOut>(Func<TIn, TOut> mapper, List<TIn> list) 
    { ... }

Или, возможно, более идиоматично, метод экземпляра:

class List<T> 
{
    public List<TOut> Map<TOut>(Func<T, TOut> mapper) 
    { ... }

Или, что еще более идиоматично, у нас может быть статический метод как метод расширения. (Фактически, этот метод существует в библиотеке операторов последовательности в C #; его можно создать, составив «Select» на IEnumerable<T> с «ToList»).

Что бы ни. Неважно. Дело в том, что в вашем коде на Haskell:

class  Functor f  where 
fmap :: (a -> b) -> f a -> f b 

Вы можете сказать, что «любой универсальный тип, который предоставляет операцию сопоставления, которая соответствует вышеприведенному шаблону, называется« Functor »», а затем вы можете создавать методы, которые принимают Functors. У нас нет никакого способа обобщить «все типы, обеспечивающие операции сопоставления» на уровне пользователя в C #.

Чтобы обойти это ограничение системы типов, мы выбрали несколько наиболее мощных высших типов и встроили их прямо в язык. Сам язык распознает более высокие типы, такие как шаблон последовательности (при обработке цикла foreach), шаблон обобщенной монады (в понимании запросов; «SelectMany» - это «привязка» к произвольной монаде), шаблон продолжения (в «ожидании», входящий в C # 5), монада «Может быть» (в типах значений, допускающих значение NULL) и так далее.

Итак, чтобы решить вашу конкретную проблему, да, нет, идея создания проекции не фиксируется в системе типов, но фиксируется в языке < / em> с пониманием запросов LINQ. Если вы скажете

from x in y select z

тогда будет работать любое выражение y типа, у которого есть метод Select, который принимает y и отображение от x к z. Этот шаблон встроен в язык C #. Но если вы хотите описать какой-нибудь другой «высший» образец, вам не повезло.

Было бы неплохо иметь возможность в системе типов описывать более высокие типы, но более вероятно, что мы сохраним систему типов как есть и при необходимости запечем больше шаблонов в языке.

В этом вопросе описывается место, где я обычно вижу попытку эмуляции типов высшего порядка в C #:

Почему это общее ограничение компилируется, если кажется, что оно имеет циклическую ссылку

Идея здесь в том, что разработчик хочет разработать такой тип «Animal», чтобы:

abstract class Animal
{
    public abstract void MakeFriends<T>(T newFriend)
    where T : THISTYPE;
}

Вымышленный «where T: THISTYPE» пытается донести идею о том, что кошка может подружиться только с другой кошкой, собака может подружиться только с другой собакой и так далее. (Игнорируйте на данный момент тот факт, что такой шаблон, который подразумевает, что MakeFriends имеет виртуальную ковариацию для формальных типов параметров, не будет типобезопасным и, вероятно, тем самым нарушит принцип подстановки Лискова.) Эта концепция выражается в более высоких типах, но не в система типов C #. Иногда люди используют такой паттерн:

abstract class Animal<T> where T : Animal<T>
{
    public abstract void MakeFriends(T newFriend);
}
class Cat : Animal<Cat>
{
    public override void MakeFriends(Cat newFriend){ ... }
}

Однако на самом деле это не может обеспечить желаемое ограничение, потому что, конечно, ничто не мешает вам сказать:

class Dog : Animal<Cat>
{
    public override void MakeFriends(Cat newFriend){ ... }
}

И теперь Собака может дружить с Кошкой, нарушая замысел автора «Зверя». Система типов C # просто недостаточно мощна, чтобы представлять все виды ограничений, которые могут вам понадобиться. Вам придется использовать более высокие типы, чтобы это работало должным образом.

person Eric Lippert    schedule 10.12.2010
comment
+1 в целом, но: '... монада Maybe (в типах, допускающих значение NULL) ...' ‹- меня действительно раздражало. Nullable ‹T› - это такой шаг назад, и я действительно не понимаю, как он подходит выше. - person ; 10.12.2010
comment
@pst: Ну, а как бы вы добавить, возможно, типы к языку, который уже имеет ссылочные типы, допускающие значение NULL? Это очень хорошо и хорошо быть приверженцем Haskell в стиле башни из слоновой кости, но у нас есть установленная база в миллионы, и мы не можем произвольно вводить совершенно новые концепции в систему типов по своему желанию. Типы значений, допускающие значение NULL, далеки от совершенства; они не должны быть идеальными. Они предназначены для разумного компромисса с учетом существования предыдущей версии языка и среды выполнения. - person Eric Lippert; 10.12.2010
comment
@Eric Lippert См. Scala и Option. Scala поддерживает типы, допускающие значение NULL. Что мне не нравится в Nullable<T> (и почему я считаю, что он не заменяет Maybe), так это то, что T ограничен типом значения и не имеет соответствующей поддержки LINQ / расширения - это слишком специфично. Дихотомия не удаляется - только некоторый синтаксис для увеличения количества плавающих нулей. Ура. - person ; 10.12.2010
comment
@pst: Конечно, Scala - отличный язык. Но вы не ответили на мой вопрос: проблема, с которой мы столкнулись с Nullable ‹T›, заключалась не в том, как нам добавить типы, возможно, в систему типов? Как мы можем добавить типы значений в систему типов, когда уже существует установленная база из миллионов пользователей, которые знают, как использовать ссылочные типы, допускающие значение NULL, и хотят типы значений, допускающие значение NULL? Это трудная проблема. Вы можете утверждать, что это свидетельство того, что типы значений, допускающие значение NULL, и ссылочные типы, не допускающие значения NULL, должны были быть встроены в версию 1.0, но мы не можем изменить прошлое, мы можем только жить с ним. - person Eric Lippert; 10.12.2010
comment
@Eric Lippert: Maybe<T>, где T не имеет ограничений и имеет соответствующую поддержку LINQ - возможно, мне не хватает некоторых внутренних механизмов Nullable<T>, я не знаю. Option в Scala - это нормальный тип (никакой магии компилятора), производный от AnyRef. Это не защитит от кого-то, использующего null там, где ожидалось Option, и это не заботит. Тем не менее, он устраняет два недостатка, которые я обнаружил у Nullable<T>. Уловка, позволяющая Option быть полезной против NPE (например, полезной при использовании нулевых значений), заключается в [последовательном] использовании. Могут быть плагины или анализ, чтобы заполнить пробелы в пустых ›опциях. - person ; 10.12.2010
comment
Я не понимаю смысла говорить о .NET Generics, когда он спрашивает о C ++ и Haskell, не предполагайте, что, поскольку .net generics не поддерживают высокодородный полиморфизм, это применимо и к C ++. C ++ действительно поддерживает какой-то полиморфизм более высокого порядка с использованием параметров шаблон-шаблон (да, я сказал шаблон-шаблон), но это почти бесполезная функция в C ++, потому что она сильно отличается от обычных параметров типа шаблона и отсутствия вывода типа, почти не поддерживает типа удержание из них. - person snk_kid; 10.12.2010
comment
@snk_kid: прочтите первый абзац вопроса еще раз, включая бит в скобках. Вопрос касается как систем типов C ++, так и C #. Замечу, что я не делаю никаких предположений или заявлений о системе типов шаблонов C ++. - person Eric Lippert; 10.12.2010
comment
ОТЛИЧНЫЙ ответ, кстати, @Eric Lippert. Большое спасибо. - person Rafael S. Calsaverini; 11.12.2010

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

person fredoverflow    schedule 10.12.2010
comment
Кроме того, я хотел бы упомянуть, что понятия / классы типов - это просто система типов для типов. C ++ 0x без них почти такой же мощный. Без концепций некоторая проверка типов просто откладывается (что может вызвать печально известные сообщения об ошибках шаблона). - person sellibitze; 11.12.2010
comment
При использовании концептов намного легче проверить правильность шаблонов. какая-то проверка типов просто откладывается, это не совсем так. Это больше похоже на то, что некоторая проверка типов просто не выполняется. Например, vector< auto_ptr<U> > x; может работать незаметно и вызывать неопределенное поведение. Правильное концептуальное ограничение на параметр шаблона потребовало бы, чтобы auto_ptr<U> был CopyAssignable или что-то еще. - person Johannes Schaub - litb; 11.12.2010
comment
Что вы принципиально не можете сделать, так это проверить само определение шаблона на C ++ без каких-либо концепций. Типа, типичный пример, template<typename ForwardIterator> ForwardIterator find(ForwardIterator begin, ForwardIterator end) { ... begin + 1 ... }. Предположим, у вас есть такое тело. Если все пользователи передадут векторные итераторы в find (что им разрешено), вы никогда не заметите, что ваш шаблон неправильный. Но на самом деле это неверно, потому что ForwardIterators концептуально не имеют op+. - person Johannes Schaub - litb; 11.12.2010
comment
Просто заглянул, чтобы сказать - прошло 10 лет, и концепции уже здесь. - person wesanyer; 10.07.2021

От автора этого блога сообщение на Haskell и C ++:

Возможность выполнять вычисления во время компиляции в C ++ была обнаружена, а не встроена в язык.

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

person chrisaycock    schedule 10.12.2010
comment
Эта статья великолепна, у того же автора есть еще одна статья под названием понимание концепций C ++ через классы типов haskell - person Fabio Fracassi; 11.12.2010
comment
И вам также может понравиться C ++ Next, в котором показаны несколько реальных случаев использования, когда этот метод приносит реальную пользу. - person Fabio Fracassi; 11.12.2010

Несмотря на то, что в публикации были выделены C # и C ++, может быть интересно сравнить с Scala (2.8, цель - JVM) вместо этого. Scala очень похож на C #, поскольку это сильный / статический "объектно-ориентированный" язык с единственной диспетчеризацией, но он имеет более мощную систему типов, чем C # 3/4 (однако функции не полностью перекрываются).

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

С другой стороны, F #, «функциональный язык», имеет слабую (или вообще отсутствует? Опять же, не в моей области, но я считаю, что невозможно создать тип Monad в F #) поддержку конструкций классов типов. Определенно стоит использовать сильные стороны языка.

Удачного кодирования.

person Community    schedule 10.12.2010
comment
F # не имеет классов типов и не поддерживает полиморфизм более высокого порядка, как в случае с .NET в целом, однако это не означает, что вы не можете писать монады без них, это просто означает, что вы не можете создать абстракцию для всех видов монад. и, таким образом, напишите поверх них общий код. Однако вы можете писать определенные типы монад, и F # имеет для этого некоторую синтаксическую поддержку. - person snk_kid; 10.12.2010
comment
@snk_kid Спасибо за разъяснения. - person ; 13.12.2010

Возможно, мне здесь не хватает мета-уровня Haskell, но ваш реальный пример без объявлений будет выглядеть так, используя STL:

struct Test : public std::unary_function<Foo,SomeResult> {
    SomeResult operator()( const Foo& foo ) const;
};

std::vector<Foo> foos;
...

std::vector<SomeResult> results;
std::transform( foos.begin(), foos.end(), results.begin(), Test() );
....
person Frank Osterfeld    schedule 10.12.2010
comment
Как я уже упоминал в другом посте, Functor означает нечто совершенно иное, чем злоупотребленный термин, используемый в C ++, он должен быть функтором из теории категорий, а не Functors C ++. - person snk_kid; 10.12.2010