Подтвердить что ты не робот

Есть ли название для этой именования создания кортежа?

В Boost listinglist следующий хитроумный трюк для создания корневого объекта был недавно отправлен @LouisDionne:

#include <iostream>

auto list = [](auto ...xs) { 
    return [=](auto access) { return access(xs...); }; 
}; 

auto length = [](auto xs) { 
    return xs([](auto ...z) { return sizeof...(z); }); 
};

int main()
{
    std::cout << length(list(1, '2', "3")); // 3    
}

Пример Live.

Умность заключается в том, что list - это лямбда, который принимает переменный список параметров в качестве входных данных и возвращает лямбда в качестве вывода, на который будет поступать другая лямбда. Аналогично, length представляет собой лямбда, берущую список-подобный объект, к которому он будет поставлять переменный sizeof... оператор к исходным входным параметрам списка. Оператор sizeof... обернут внутри лямбда, так что его можно передать в list.

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

4b9b3361

Ответ 1

Я думаю, что это тонкая реализация подобной Монаде вещи, в частности, что-то в том же духе продолжения монады.

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

Монады имеют два первичных пиляра:

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

Википедия содержит очень хорошие примеры и объяснения о монадах.

Позвольте мне переписать данный код С++ 14:

auto list = []( auto... xs ) 
{ 
    return [=]( auto access ) { return access(xs...); };
};

Я думаю, что здесь мы идентифицируем функцию return монады: берет значение и возвращает его монадическим способом. В частности, это возвращение возвращает функтор (в математическом смысле, а не как функтор С++), который идет от категории "tuple" к категории вариационного пакета.

auto pack_size = [](auto... xs ) { return sizeof...(xs); };

pack_size - это просто нормальная функция. Он будет использоваться в конвейере для выполнения некоторой работы.

auto bind = []( auto xs , auto op ) 
{
    return xs(op);
};

И length - это всего лишь неосновная версия чего-то рядом с оператором monad bind, оператор, который берет монадическое значение с предыдущего шага конвейера и обходит его до указанной функции (функция, которая действительно делает работа). Эта функция - это функциональность, выполняемая этим этапом вычисления.

Наконец, ваш звонок можно переписать как:

auto result = bind(list(1,'2',"3"), pack_size);

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

Изменить: больше удовольствия

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

Ответ 2

Я бы назвал этот idiom tuple-continuator или более общим, monadic-continuator. Это, безусловно, пример продолжения монады. Отличное введение для продолжения монады для программистов на С++ здесь. По существу, list lambda above принимает значение (вариационный параметр-пакет) и возвращает простой "продолжитель" (внутреннее закрытие). Этот продолжатель, когда ему присваивается вызываемый (называемый access), передает пакет параметров в него и возвращает все возвращаемые возвращаемые значения.

Заимствуя из блога блога FPComplete, продолжатель более или менее похож на следующий.

template<class R, class A>
struct Continuator {
    virtual ~Continuator() {}
    virtual R andThen(function<R(A)> access) = 0;
};

Continuator выше - абстрактный - не обеспечивает реализацию. Итак, вот простой.

template<class R, class A>
struct SimpleContinuator : Continuator<R, A> {
    SimpleContinuator (A x) : _x(x) {}
    R andThen(function<R(A)> access) {
        return access(_x);
    }
    A _x;
};

SimpleContinuator принимает одно значение типа A и передает его на access, когда вызывается andThen. list лямбда выше по существу то же самое. Это более общий. Вместо одного значения внутреннее замыкание захватывает пакет параметров и передает его функции access. Ухоженная!

Надеюсь, это объяснит, что значит быть продолжателем. но что значит быть монадой? Вот хорошее представление с использованием изображений.

Я думаю, что лямбда list также является монадой списка, которая реализована как монада продолжения. Обратите внимание, что монада продолжения является матерью всех монадов. I.e., вы можете реализовать любую монаду с продолжением монады. Конечно, монада-список не вне досягаемости.

Как пакет параметров, естественно, представляет собой "список" (часто гетерогенных типов), имеет смысл работать как монада списка/последовательности. list lambda above - очень интересный способ преобразования пакетов параметров С++ в монадическую структуру. Поэтому операции могут быть связаны друг с другом цепями.

Тем не менее, length lambda, однако, немного разочаровывает, потому что он ломает монаду, а вложенная лямбда внутри просто возвращает целое число. Возможно, лучший способ записать длину "getter", как показано ниже.

---- ---- Функтор

Прежде чем мы сможем сказать, что список лямбда является монадой, мы должны показать, что это функтор. I.e., fmap должен быть записан для списка.

Список лямбда выше служит создателем функтора из пакета параметров - по существу он служит как return. Этот созданный функтор сохраняет пакет параметров с собой (захват) и позволяет ему "получить доступ", если вы даете вызываемый, который принимает переменное количество аргументов. Обратите внимание, что вызываемый называется ТОЧНО-ОДИН.

Позволяет написать fmap для такого функтора.

auto fmap = [](auto func) { 
    return [=](auto ...z) { return list(func(z)...); };
};

Тип func должен быть (a → b). I.e., в С++ говорят,

template <class a, class b>
b func(a);

Тип fmap fmap: (a -> b) -> list[a] -> list[b] I.e, в С++ говорит,

template <class a, class b, class Func>
list<b> fmap(Func, list<a>);

I.e., fmap просто отображает список-of-a в список-b.

Теперь вы можете сделать

auto twice = [](auto i) { return 2*i; };
auto print = [](auto i) { std::cout << i << " "; return i;};
list(1, 2, 3, 4)
    (fmap(twice))
    (fmap(print)); // prints 2 4 6 8 on clang (g++ in reverse)

Следовательно, это функтор.

---- ---- Монада

Теперь попробуйте написать flatmap (a.k.a. bind, selectmany)

Тип плоской карты flatmap: (a -> list[b]) -> list[a] -> list[b].

I.e., если задана функция, которая отображает a в список-b, а таблица flat-map из списка-of-a возвращает список-b. По сути, он берет каждый элемент из списка-of-a, вызывает func на нем, получает (потенциально пустой) список-b один за другим, затем объединяет весь список-b и, наконец, возвращает окончательный список -of-б.

Здесь реализована карта flatmap для списка.

auto concat = [](auto l1, auto l2) {
    auto access1 = [=](auto... p) {
      auto access2 = [=](auto... q) {
        return list(p..., q...);
      };
      return l2(access2);
    };
    return l1(access1);
};

template <class Func>
auto flatten(Func)
{
  return list(); 
}

template <class Func, class A>
auto flatten(Func f, A a)
{
  return f(a); 
}

template <class Func, class A, class... B>
auto flatten(Func f, A a, B... b)
{
  return concat(f(a), flatten(f, b...));
}

auto flatmap = [](auto func) {
  return [func](auto... a) { return flatten(func, a...); };
};

Теперь вы можете сделать много мощных вещей со списком. Например,

auto pair  = [](auto i) { return list(-i, i); };
auto count = [](auto... a) { return list(sizeof...(a)); };
list(10, 20, 30)
    (flatmap(pair))
    (count)
    (fmap(print)); // prints 6.

Функция count - операция monad-perserving, так как она возвращает список одного элемента. Если вы действительно хотите получить длину (не упакованную в список), вам нужно прекратить монадическую цепочку и получить значение следующим образом.

auto len = [](auto ...z) { return sizeof...(z); }; 
std::cout << list(10, 20, 30)
                 (flatmap(pair))
                 (len);

Если все сделано правильно, шаблон паттерна > (например, filter, reduce) теперь можно применить к пакетам параметров С++. Классно!

---- Законы Монады ----

Пусть убедитесь, что монада list удовлетворяет всем трем законам монады.

auto to_vector = [](auto... a) { return std::vector<int> { a... }; };

auto M = list(11);
std::cout << "Monad law (left identity)\n";
assert(M(flatmap(pair))(to_vector) == pair(11)(to_vector));

std::cout << "Monad law (right identity)\n";
assert(M(flatmap(list))(to_vector) == M(to_vector));

std::cout << "Monad law (associativity)\n";
assert(M(flatmap(pair))(flatmap(pair))(to_vector) == 
       M(flatmap([=](auto x) { return pair(x)(flatmap(pair)); }))(to_vector));

Все утверждения удовлетворяются.

---- Сборник трубопроводов ----

Несмотря на то, что вышеупомянутый "список" лямбда является предположительно монадой и разделяет характеристики пресловутого "списка-монады", это довольно неприятно. Тем более, что поведение общих компиляторов сборщика, например filter (a.k.a where), не отвечает общим ожиданиям.

Причина в том, как работают lambdas С++. Каждое лямбда-выражение создает объект функции уникального типа. Поэтому list(1,2,3) создает тип, который не имеет ничего общего с list(1) и пустым списком, который в этом случае был бы list().

Прямая реализация where завершает компиляцию, потому что в С++ функция не может возвращать два разных типа.

auto where_broken = [](auto func) {
  return flatmap([func](auto i) { 
      return func(i)? list(i) : list(); // broken :-(
  }); 
};

В приведенной выше реализации func возвращает логическое значение. Это предикат, который говорит true или false для каждого элемента. Оператор?: Не компилируется.

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

auto where_unpleasant = [](auto func) {
  return [=](auto... i) { 
      return list(std::make_pair(func(i), i)...);
  }; 
};

where_unpleasant выполняет свою работу, но неприятно...

Например, вы можете фильтровать отрицательные элементы.

auto positive = [](auto i) { return i >= 0; };
auto pair_print = [](auto pair) { 
  if(pair.first) 
     std::cout << pair.second << " "; 
  return pair; 
};
list(10, 20)
    (flatmap(pair))
    (where_unpleasant(positive))
    (fmap(pair_print)); // prints 10 and 20 in some order

---- Гетерогенные кортежи ----

До сих пор дискуссия шла о однородных кортежах. Теперь давайте обобщить его на верные кортежи. Тем не менее, fmap, flatmap, where принимают только одну обратную связь лямбда. Чтобы предоставить несколько ягнят, каждый из которых работает над одним типом, мы можем перегрузить их. Например,

template <class A, class... B>
struct overload : overload<A>, overload<B...> {
  overload(A a, B... b) 
      : overload<A>(a), overload<B...>(b...) 
  {}  
  using overload<A>::operator ();
  using overload<B...>::operator ();
};

template <class A>
struct overload<A> : A{
  overload(A a) 
      : A(a) {} 
  using A::operator();
};

template <class... F>
auto make_overload(F... f) {
  return overload<F...>(f...);   
}

auto test = 
   make_overload([](int i) { std::cout << "int = " << i << std::endl; },
                 [](double d) { std::cout << "double = " << d << std::endl; });
test(10); // int 
test(9.99); // double    

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

auto int_or_string = 
        make_overload([](int i) { return 5*i; },
                      [](std::string s) { return s+s; });
    list(10, "20")
        (fmap(int_or_string))
        (fmap(print)); // prints 2020 and 50 in some order

Наконец, Живой пример

Ответ 3

Это выглядит как форма стиль продолжения передачи.

Грубая идея CPS заключается в следующем: вместо того, чтобы иметь функцию (скажем f), возвращаем некоторое значение, вы предоставляете f еще один аргумент, который является функцией, называемой продолжением. Затем f вызывает это продолжение с возвращаемым значением вместо возврата. Возьмем пример:

int f (int x) { return x + 42; }

становится

void f (int x, auto cont) { cont (x + 42); }

Вызов является хвостовым вызовом и может быть оптимизирован в прыжок (поэтому TCO назначается на некоторых языках, например Scheme, чья семантика полагается на некоторую форму преобразования в CPS).

Другой пример:

void get_int (auto cont) { cont (10); }
void print_int (int x) { printf ("%d", x), }

Теперь вы можете сделать get_int (std::bind (f, _1, print_int)) для печати 54. Обратите внимание, что все вызовы продолжения всегда являются хвостовыми вызовами (вызов printf также является продолжением).

Известным примером являются асинхронные обратные вызовы (вызовы AJAX в javascript для примера): вы передаете продолжение в процедуру, выполняемую параллельно.

Можно создать континуумы ​​(и сформировать монаду, если вам интересно), как в приведенном выше примере. Фактически можно полностью преобразовать (функциональную) программу в CPS, чтобы каждый вызов был хвостом (а затем вам не нужен стек для запуска программы!).