Каррируем на C++

Привет, хабр.Сидел я как-то вечером, ждал, пока соберется свежая ревизия clang, и смотрел на код одного своего проекта, в котором встречались не очень красивые вещи вроде

std: transform (someContainer.begin (), someContainer.end (), std: back_inserter (otherContainer), [this] (const SomeUglyAndLongType& item) { return HandleItem (item); }); Зачем создавать целую лямбду, чтобы у функции двух аргументов (если, как пишут классики, this считать неявным нулевым аргументом) зафиксировать один из них? На каком-нибудь псевдохаскеле можно было бы просто написать что-то вроде map (handleItem this) someContainer Мапы, функторы и прочие монады сделаем как-нибудь в следующий раз, а вот вещи, напоминающие (handleItem this) можно попробовать научиться писать.Итак, что хочется сделать? Хочется научиться каррировать произвольные функции, то есть, вызывать их по «кусочкам» — передали первый аргумент, получили какую-то новую функцию одного аргумента, передали второй, получили новую функцию, и так далее, пока все аргументы для нашей исходной функции не будут переданы. Когда аргументы кончились, переходим на личности и вызываем нашу исходную функцию со всеми указанными аргументами.

Или, если вдруг кому так понятнее будет, нужно из функции N аргументов T1 × T2 ×… × TN → R сделать последовательность функций T1 → (T2 → (… → (TN → R))). Хотя, кому так понятнее, тот и наверняка и без меня знает, что такое каррирование.

Сразу предупреждаю, что решение получится не production-quality по ряду причин, которые мы обсудим чуть позже.

Что нам для этого понадобится? Понадобится свежий компилятор с поддержкой C++14, например, clang 3.4 и новее. Нужна и стандартная библиотека из C++14, на некоторых линуксах с этим могут быть проблемы, так что с кодом можно играться и на онлайн-сервисах вроде этого.

Как должен выглядеть результат наших попыток сделать из C++ хаскель на, собственно, плюсах? Ну, например, как-нибудь так:

auto test (int t1, int t2, double t3, const std: string& str) { return t1 * t2 * t3 * str.size (); } // … std: cout << curry (test) (1) (2) (3.0) ("four") << std::endl; Естественно, свободными функциями curry ограничиваться не должно.Что нам нужно сделать? Да всего-ничего, написать функцию curry. Ну так давайте и напишем!

Пусть curry (f) возвращает какой-нибудь объект (назовём его CurryImpl), которому, очевидно, нужна будет функция f, которую мы передали в curry, а ещё у него должен быть перегружен operator (), и по условию задачи он должен принимать один аргумент непонятно какого типа, и непонятно что возвращать. Естественно, CurryImpl шаблонный, чтобы уметь запоминать любые функции:

template class CurryImpl { const F m_f; // наша функция, переданная в curry public: CurryImpl (F f) : m_f { f } { }

template auto operator () (const T& arg) const { // осталось тут что-то написать } };

template auto curry (F f) { return CurryImpl { f }; } Давайте теперь посмотрим на оператор-круглые-скобки. Итак, этот оператор принимает один аргумент arg и либо вызывает искомую функцию m_f, если это последний аргумент в цепочке вызовов, либо возвращает другой объект, который запомнил этот arg.Сначала разберёмся с тем, как запоминать аргументы.

Ну, тут на самом деле все довольно тривиально: нужно их просто брать и хранить в CurryImpl. Про эти аргументы мы ничего не знаем, равно как и про их типы, поэтому придётся добавить ещё один аргумент шаблона, да ещё и с вариадик. Непосредственно хранить значения аргументов можно, например, в std: tuple. Сообщать нашему CurryImpl про эти аргументы можно непосредственно в конструкторе. Итого, объявление класса модифицируется как-то так:

template class CurryImpl { const F m_f; const std: tuple m_prevArgs; public: CurryImpl (F f, const std: tuple& prev) : m_f { f } , m_prevArgs { prev } { }

//, а дальше все как раньше }; Так, хорошо, с хранением разобрались, теперь осталось вызывать функцию. Тут всё становится немного интереснее:, а как, собственно, определить, когда стоит прекратить накапливать аргументы и, собственно, вызвать m_f? А просто взять и вызвать, если получится — всё отлично! А поможет нам в этом одно замечательное правило C++ — SFINAE, или Substitution Failure Is Not An Error. Если вкратце, [в данном контексте] суть в том, что, если компилятор при выборе кандидатов на вызов функции среди ряда перегруженных функций видит некое некорректное выражение в объявлении функции, то он её отбрасывает и смотрит дальше вместо error в консоли и слишком рано завершённой компиляции. Собственно, все эти std: enable_if и компания основаны именно на SFINAE.

Итак, SFINAE. Напишем функцию, которая будет вызывать нашу исходную m_f только тогда, когда такой вызов будет well-formed. В этом нам поможет замечательная шаблонная функция std: result_of. std: result_of определяет вложенный тип type, равный типу, возвращаемому объектом F, если он вызван с аргументами типа T1, T2, … Подробнее она описана, например, здесь. Собственно, ключевые слова по ссылке, для C++14:

Only defined if F can be called with the arguments ArgTypes… in unevaluated context.

Для C++11 формулировка слегка другая, но это несущественно.

Кстати, в C++14 можно пользоваться удобным синонимом std: result_of_t<...> вместо typename std: result_of<...>:: type.

Итак, пишем нашу функцию:

template std: result_of_t invoke (const T& arg) const // PrevArgs — аргумент CurryImpl, как мы помним { // тут как-то вызываем нашу m_f и возвращаем её результат } Как это работает? Если m_f, которая типа F, можно вызвать со всеми предыдущими аргументами и текущим, то всё хорошо, функция «существует», если нельзя — компилятор её и не рассматривает.

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

template auto invoke (const T& arg) const { return CurryImpl { m_f, std: tuple_cat (m_prevArgs, std: tuple { arg }) }; } Что мы тут сделали? Вернули новый CurryImpl, который сохраняет все предыдущие аргументы плюс один новый. На уровне типов это отражается в записи PrevArgs…, T, если хотите, добавляющей T в variadic-список типов, а на уровне значений — просто соединяем два кортежа, старый m_prevArgs и новый одноэлементный кортеж.

Посмотрим снова на наш оператор-круглые-скобки. Теперь понятно, что мы должны в нём вызвать одну из наших двух перегрузок invoke, по возможности первую, которая вызывает функцию, а то так можно никогда не перестать копить аргументы. Как это сделать? Тут есть куча вариантов, мой любимый в таких случаях — использовать тот факт, что любая перегрузка лучше, чем перегрузка с эллипсисом (aka многоточие, aka вариадики C-стайл). То есть, добавляем ещё один параметр, который даже не будем использовать, например, int в первую функцию и … во вторую, и получаем что-то подобное:

template auto operator () (const T& arg) const { return invoke (arg, 0); } private: template std: result_of_t invoke (const T& arg, int) const { // тут как-то вызываем нашу m_f и возвращаем её результат } template auto invoke (const T& arg, …) const { return CurryImpl { m_f, std: tuple_cat (m_prevArgs, std: tuple { arg }) }; } То есть, если первая перегрузка доступна, всегда будет выбираться она: int компилятору нравится куда больше, чем …

Осталось разобраться с вызовом функции из первой перегрузки, и всё будет хорошо.

Что нужно, чтобы вызвать функцию, аргументы которой (с точностью до arg) хранятся в кортеже? Надо как-нибудь распаковать тот кортеж и передать результаты распаковки в функцию как обычные аргументы. Проблема в том, что в точке вызова мы не знаем на этапе написания кода, сколько у нас аргументов, поэтому просто взять и руками подёргать std: get мы не можем. Ну и славно, негоже в 2014 году работу за компилятор делать. Вот был бы у нас какой-нибудь способ сделать вариадик из чисел от 0 до N-1, мы могли бы написать что-то такое:

// в Is лежат числа от 0 до (количество элементов в m_prevArgs)-1 template auto invokeIndexed (const T& arg /*, тут ещё что-то, чтобы Is вывелся компилятором сам*/) const { return m_f (std: get (m_prevArgs)…, arg); } Здесь по правилам разворачивания variadic-параметров выражение std: get (m_prevArgs)… развернётся компилятором в std: get (m_prevArgs), std: get (m_prevArgs), и так далее для всех индексов Is.А, стоп, так в точности такой вариант можно сделать на тех же вариадиках в C++11, а в C++14 его даже добавили в STL! Замечательно, всё встаёт на свои места. Идём по ссылке, видим замечательный метод std: index_sequence_for, который нам как раз нужен (у нас ведь на руках есть PrevArgs…), и пишем в теле нашей первой invoke вызов invokeIndexed:

return invokeIndexed (arg, std: index_sequence_for {}); А invokeIndexed тогда вторым параметром должна принимать std: index_sequence, которая как раз и отвечает за хранение списка чисел: template auto invokeIndexed (const T& arg, std: index_sequence) const { return m_f (std: get (m_prevArgs)…, arg); } Отлично! Всё работает! На радостях пишем то, с чего начинался пост, вроде такого:

struct Foo { auto doFoo (int baz, int qux) { return (m_bar + baz) / qux; } }; // … Foo someFoo; const auto fooRes = Curry (&Foo: doFoo) (&someFoo) (2) (4); И сразу же компилятор возвращает нас в жестокую реальность: выражение m_f (arguments) не является well-formed, если m_f — указатель на функцию-член класса.Впрочем, как учили нас классики, любая проблема решается добавлением ещё одного уровня абстракции, поэтому добавим ещё один уровень, который будет ответственен непосредственно за вызов функции с аргументами. Уровень будет представляться в виде шаблонной структуры, параметризуемой типом нашей m_f, и будет иметь шаблонный оператор-круглые-скобки. Опишем сначала общий случай, когда мы просто берём и вызываем нашу функцию:

template struct Invoke { template auto operator () (IF fr, IArgs… args) { return fr (args…); } }; Перепишем invokeIndexed: template auto invokeIndexed (const T& arg, std: index_sequence) const { return Invoke {} (m_f, std: get (m_prevArgs)…, arg); } К счастью, для частных случаев указателей на функции-члены никаких SFINAE не нужно, можно просто специализировать наш Invoke, да и выбирать из списка аргументов Args… первый — по соглашению он и будет нашим объектом, на котором нужно вызвать функцию. При этом ещё не забудем, что первый аргумент может быть как указателем на класс, так и ссылкой, и хочется поддерживать оба варианта вызова: template struct Invoke { auto operator () (R (C::*ptr) (Args…), C c, Args… rest) { return (c.*ptr) (rest…); } auto operator () (R (C::*ptr) (Args…), C *c, Args… rest) { return (c→*ptr) (rest…); } }; Кстати, между ними есть принципиальная разница, очевидная, конечно, но проговорить стоит: в первом случае изменения затронут локальную копию c, а во втором они будут видны и извне, на объекте, который в один из операторов-круглые-скобочки мы передали. То есть: struct Foo { int m_state = 42;

auto doFoo (int bar) { m_state += bar; return m_state; } };

Foo foo; curry (&Foo: doFoo) (foo) (1); // foo.m_state всё ещё 42 curry (&Foo: doFoo) (&foo) (1); // foo.m_state теперь 43 Вот, в общем-то, и всё.

Всё вместе #include #include #include #include #include

template class CurryImpl { const F m_f; const std: tuple m_prevArgs; public: CurryImpl (F f, const std: tuple& prev) : m_f { f } , m_prevArgs { prev } { } template auto operator () (const T& arg) const { return invoke (arg, 0); } private: template std: result_of_t invoke (const T& arg, int) const { return invokeIndexed (arg, std: index_sequence_for {}); } template struct Invoke { template auto operator () (IF fr, IArgs… args) { return fr (args…); } };

template struct Invoke { auto operator () (R (C::*ptr) (Args…), C c, Args… rest) { return (c.*ptr) (rest…); } auto operator () (R (C::*ptr) (Args…), C *c, Args… rest) { return (c→*ptr) (rest…); } };

template auto invokeIndexed (const T& arg, std: index_sequence) const { return Invoke {} (m_f, std: get (m_prevArgs)…, arg); } template auto invoke (const T& arg, …) const { return CurryImpl { m_f, std: tuple_cat (m_prevArgs, std: tuple { arg }) }; } };

template auto curry (F f) { return CurryImpl { f, {} }; }

auto test (int t1, int t2, double t3, const std: string& str) { return t1 * t2 * t3 * str.size (); }

struct Foo { int m_bar;

auto doFoo (int baz, int qux) { auto result = (m_bar + baz) / qux; ++m_bar; return result; } };

int main () { const auto res = curry (test) (1) (2) (3.0) («four»); std: cout << res << std::endl;

Foo someFoo { 42 }; const auto fooRes = curry (&Foo: doFoo) (&someFoo) (2) (4); std: cout << fooRes << " " << someFoo.m_bar << std::endl; someFoo.m_bar = 42; auto lambda = [someFoo] (int bar, int baz) mutable { return someFoo.doFoo (bar, baz); }; const auto lambdaRes = curry (lambda) (4) (2); std::cout << lambdaRes << std::endl; } Ещё всё вместе есть онлайн.Теперь ответы на некоторые предполагаемые вопросы:

Зачем параметризовать CurryImpl типом функции, почему бы просто не использовать std: function?

В std: function внутри сидит type erasure, который далеко не всегда девиртуализируется и инлайнится компилятором. Впрочем, это тема для отдельной статьи Скучно. А std: tuple тебе не скучно? Нет, тем более, что собственная реализация была бы точно такая же, от тупла тут никакого оверхеда.Зачем тут C++14, разве C++11 не хватит? Неа, не хватит. C++14 тут полезен, в частности:

чтобы писать auto вместо возвращаемых значений функций, а не выписывать адские рекурсивные decltype или ещё что похуже; чтобы не переизобретать compile-time-последовательность чисел (std: index_sequence_for, это вот всё). Хотя, конечно, без всего этого вполне можно жить, самое главное — вариадики и вывод типов хотя бы в виде decltype.Получается, что с функциями с аргументами по умолчанию передать можно будет только лишь аргументы без значений по умолчанию? Верно.

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

Почему оно не production-ready? Потому что коллеги вас растерзают за такой код, а сисадмин и менеджер не захотят переходить на C++14 там наверняка где-то внутри бегает пара проблем, связанных с обработкой ссылок.

© Habrahabr.ru