Александр Гранин, "Декларативно-функциональный...
TRANSCRIPT
Функционально-декларативный дизайн на С++
Александр Гранин[email protected]
C++ User Group, Новосибирск
О себе
● C++ → Haskell
● Рассказывал на DevDay@2GIS о Haskell
● Рассказывал на TechTalks@NSU о ФП
● Статьи на Хабре о дизайне в ФП
● Работаю в “Лаборатории Касперского”
struct Presentation{
Описание задачиФункциональная комбинаторикаПродвинутый дизайнТестированиеПроблемы и особенности
};
О задаче
● Игра “Амбер” (По мотивам “Хроник Амбера” Роджера Желязны)
● C++11 (Qt C++ 5.2.1, gcc 4.8.2)
● Функционально-декларативный дизайн
● Максимум смысла, минимум кода
● https://github.com/graninas/Amber
Ограничения● Нет классов, только struct (POD)
● Списки инициализации!
● Нет циклов for(;;), есть for_each()
● Всяческие eDSLs
● Иммутабельность
● Лямбды, функции, замыкания
Координаты тени, игрока
Полюс Амбера
Амбер
Бергма
Кашфа
Авалон
G(90) W(10) A(100) S(70)
G(30) W(70) A(80) S(90)
Амбер:
90 Земля (G)10 Вода (W)100 Воздух (A)70 Небо (S)
100 Флора100 Фауна0 Расстояние до Амбера100 Расстояние до Хаоса
Коородинаты тени, игрока
namespace Element {enum ElementType { Air, Sky, Water, Ground};
}
Амбер
Бергма
Кашфа
Авалон
Игрок:
90 Земля10 Вода100 Воздух70 Небо
typedef std::map<Element::ElementType, int> ShadowStructure;
Декларативное задание координатShadowStructure::value_type Air(int air) { return ShadowStructure::value_type(Element::Air, air);}
ShadowStructure amberShadowStructure() { return { { Element::Ground, 90 } , { Element::Water, 10 } , Air(100) , Sky(70) };}
Определение тениtypedef std::function<ShadowStructure(ShadowStructure, Direction::DirectionType)> ShadowVariator;
struct Shadow { ShadowName name; ShadowVariator variator; ShadowStructure structure; double influence;};
typedef std::function<ShadowStructure(ShadowStructure, Direction::DirectionType)> ShadowVariator;
const ShadowVariator identityVariator = [](const ShadowStructure& structure, Direction::DirectionType){ return structure;};
ShadowVariator координат игрока
NGround
SWater
ESky
WAir
ShadowVariator координат игрокаconst ShadowVariator bergmaVariator = [](const ShadowStructure& structure, Direction::DirectionType dir) -> ShadowStructure {
switch (dir) { case Direction::North: return changeElements({ element::Water(-2) , element::Ground(2) } , structure); // and so on for the different directions... }};
AmberTask - задачи на лямбдахtypedef std::function<Amber (const Amber&)> AmberTask;
const AmberTask goNorth = [](const Amber& amber) { const ShadowVariator variator = // somehow get variator
Amber newAmber = amber; newAmber.playerPos = variator(amber.playerPos, Direction::North);
return newAmber; };
Список задачtypedef std::function<Amber (const Amber&)> AmberTask;typedef std::list<AmberTask> AmberTaskList;
Amber goNorth(const Amber& amber) { AmberTaskList tasks = { goNorth, inflateShadowStorms, tickWorldTime }; return evaluate(amber, tasks);}
Выполнение списка задачtypedef std::function<Amber (const Amber&)> AmberTask;typedef std::list<AmberTask> AmberTaskList;
Amber evaluate(const Amber& amber, const AmberTaskList& tasks) { Amber currentAmber = amber; std::for_each(tasks.begin(), tasks.end(), [¤tAmber](const AmberTask& task) { currentAmber = task(currentAmber); }); return currentAmber;}
Boilerplate
const AmberTask goNorth = [](const Amber& amber) { // Bla-bla with Direction::North};
const AmberTask goSouth = [](const Amber& amber) { // The same Bla-bla with Direction::South};
// And so on...
Конструирование лямбдAmberTask goDirection(Direction::DirectionType dir) { return [dir](const Amber& amber) { // Bla-bla with dir };}
AmberTaskList tasks = { goDirection(Direction::North), inflateShadowStorms, tickWorldTime };
Небезопасный код
const AmberTask inflateShadowStorms = [](const Amber& amber){ throw std::runtime_error("Shit happened! :)");};
Данные + результат onSuccess Task
Amber 1Result 1
Amber 2Result 2onFail Task
if (Result 1== Success)
+
-
safeEvalTask
safeEvalTask
Комбинаторный eDSLconst AmberTask tickOneAmberHour = [](const Amber& amber) { auto action1Res = anyway (inflateShadowStorms, wrap(amber)); auto action2Res = onSuccess (affectShadowStorms, action1Res); auto action3Res = onFail (shadowStabilization, action2Res); auto action4Res = anyway (tickWorldTime, action3Res); return action4Res.amber;};
AmberTask goNorthTask = [](const Amber& amber) { auto action1Res = anyway (goNorth, wrap(amber)); auto action2Res = anyway (tickOneAmberHour, action1Res); return action2Res.amber;};
Комбинаторыstruct Artifact { Amber amber; bool success;};
Artifact wrap(const Amber& amber){ Artifact artifact { amber, true, {} }; return artifact;}
Artifact onSuccess(const AmberTask& task, const Artifact& artifact) { // eval() when artifact.success == true. // otherwise, return an old artifact.}
Artifact anyway(const AmberTask& task, const Artifact& artifact) { // no check of previous task success. // just safely eval() a new task.}
Как может выглядеть eval()Artifact eval(const AmberTask& task, const Artifact& artifact) { Artifact newArtifact = artifact; try { Amber newAmber = task(artifact.amber); newArtifact.amber = newAmber; newArtifact.success = true; } catch (const std::exception& e) { // Do something with e newArtifact = artifact; newArtifact.success = false; } return newArtifact;}
Обобщение безопасного типа// Было:struct Artifact { Amber amber; bool success;};
AmberTask goNorthTask = [](const Amber& amber) { Artifact action1Res = onSuccess (amberTask1, wrap(amber)); Artifact action2Res = onSuccess (amberTask2, action1Res); Artifact action3Res = onSuccess (amberTask3, action2Res); return action3Res.amber;};
Обобщение безопасного типа// Было:struct Artifact { Amber amber; bool success;};
// Стало:enum MaybeValue { Just, Nothing};
template <typename Data>struct Maybe { Data data; MaybeValue mValue;};
Обобщение безопасного типа// Было:Artifact wrap(const Amber& amber);Artifact onSuccess(const AmberTask& task, const Artifact& artifact);
// Стало:template <typename Input> Maybe<Input> just(const Input& input);
template <typename Input, typename Output> Maybe<Output> bind(const Maybe<Input>& input, const std::function<Maybe<Output>(Input)>& action);
Maybe - это монадаtemplate <typename Input, typename Output> Maybe<Output> bind(const Maybe<Input>& input, const std::function<Maybe<Output>(Input)>& action) {
if (input.mValue == Nothing) { return nothing<Output>(); }
return action(input.data);}
Монадические функции в Maybeconst std::function<Maybe<ShadowVariator>(Amber)> lookupVariator = [](const Amber& amber) { return ...; // retrieve the nearest shadow's variator};
std::function<Maybe<Amber>(ShadowVariator)> applyVariator(const Amber& amber, Direction::DirectionType dir) { return [&amber, dir](const ShadowVariator& variator) { // apply variator to passed amber, using dir };}
Использование Maybe
MaybeAmber goDirectionBinded(const Amber& amber, Direction::DirectionType dir) { MaybeAmber mbAmber1 = just(amber); MaybeShadowVariator mbVariator = bind(mbAmber1, lookupVariator); MaybeAmber mbAmber2 = bind(mbVariator, applyVariator(amber, dir)); MaybeAmber mbAmber3 = bind(mbAmber2, updateNearestPlace); return mbAmber3;}
Использование Maybe с auto
MaybeAmber goDirectionBinded(const Amber& amber, Direction::DirectionType dir){ auto m1 = just(amber); auto m2 = bind(m1, lookupVariator); auto m3 = bind(m2, applyVariator(amber, dir)); auto m4 = bind(m3, updateNearestPlace); return m4;}
Связывание многих MaybeMaybeAmber goDirectionStacked(const Amber& amber, Direction::DirectionType dir) {
MaybeActionStack<Amber, ShadowVariator, Amber, Amber> stack = bindMany(lookupVariator, applyVariator(amber, dir), updateNearestPlace);
MaybeAmber mbAmber = evalMaybes(just(amber), stack); return mbAmber;}
Простая реализация MaybeActionStack
template <typename M1, typename M2, typename M3, typename M4>struct MaybeActionStack{ std::function<Maybe<M2>(M1)> action1; std::function<Maybe<M3>(M2)> action2; std::function<Maybe<M4>(M3)> action3;};
Простая реализация bindManytemplate <typename M1, typename M2, typename M3, typename M4> MaybeActionStack<M1, M2, M3, M4> bindMany(const std::function<Maybe<M2>(M1)> action1, const std::function<Maybe<M3>(M2)> action2, const std::function<Maybe<M4>(M3)> action3){ MaybeActionStack<M1, M2, M3, M4> stack; stack.action1 = action1; stack.action2 = action2; stack.action3 = action3; return stack;}
Простая реализация evalMaybes
template <typename M1, typename M2, typename M3, typename M4>Maybe<M4> evalMaybes(const Maybe<M1>& m1, const MaybeActionStack<M1, M2, M3, M4>& stack){ Maybe<M2> m2 = bind<M1, M2>(m1, stack.action1); Maybe<M3> m3 = bind<M2, M3>(m2, stack.action2); Maybe<M4> m4 = bind<M3, M4>(m3, stack.action3); return m4;}
Тестированиеvoid Testing::changeElementTest() {
amber::ShadowStructure structure = { { amber::Element::Ground, 90 , { amber::Element::Water, 10 } }; amber::ShadowStructure expected = { { amber::Element::Ground, 100 } , { amber::Element::Water, 10 } }; auto newStructure = changeElement(structure, amber::Element::Ground, 10);
ASSERT_EQ(expected, newStructure);}
Положительные моменты
● Лямбды - универсальный инструмент дизайна● Краткость функционального кода● Высокая модульность● Прекрасная тестируемость● Редуцирована структурная сложность ПО● Многие задачи решаются проще, понятнее● Больше внимания задаче, а не борьбе с
языком
Проблемы и особенности
● Массированное копирование данных● Большее потребление памяти● Меньшая производительность● Опасные замыкания в лямбдах● Чистота функций не контролируется● Нет алгебраических типов данных● Нет каррирования, заменители плохи
Проблема: глубокие структуры
struct C { int intC; std::string stringC;};
int intC; std::string stringC;
CB
A
struct B { C c;};
struct A { B b;};
// Not Ok: a mutable codevoid changeC(A& a) { a.b.c.intC += 20; a.b.c.stringC = "Hello, World!";}
Проблема: глубокие структуры
int intC; std::string stringC;
CB
A
// Immutable code, but still not Ok: too deep structure divingA changeC(const A& oldA) { C newC = oldA.b.c; newC.intC = oldA.b.c.intC + 20; newC.stringC = "Hello, world!";
B newB = oldA.b; newB.c = newC;
A newA = oldA; newA.b = newB; return newA;}
Встречайте: линзы!
A changeC(const A &oldA) { LensStack<A, B, C> stack = zoom(aToBLens(), bToCLens()); A newA = evalLens(stack, oldA, modifyC); return newA;}
std::function<C(C)> modifyC = [](const C& c){ return C { c.intC + 20, "Hello, World!" };};
Линза - это геттер + сеттер
Lens<A, B> aToBLens() { return lens<A, B> ( GETTER(A, b) , SETTER(A, B, b));}
Lens<B, C> bToCLens() { return lens<B, C> ( GETTER(B, c) , SETTER(B, C, c));}
Геттер и сеттер - это лямбды
Lens<A, B> aToBLensDesugared() { return lens<A, B> ( [](const A& a) { return a.b; }
, [](const A& a, const B& b) { A newA = a; newA.b = b; return newA; });}
LensStack - та же идея, что и MaybeActionStack
template <typename Zoomed1, typename Zoomed2, typename Zoomed3 = Identity, typename Zoomed4 = Identity>struct LensStack{ Lens<Zoomed1, Zoomed2> lens1; Lens<Zoomed2, Zoomed3> lens2; Lens<Zoomed3, Zoomed4> lens3;};
Зуммирование и фокусировкаtemplate <typename Zoomed1, typename Zoomed2,
typename Zoomed3>LensStack<Zoomed1, Zoomed2, Zoomed3, Identity> zoom(Lens<Zoomed1, Zoomed2> l1 , Lens<Zoomed2, Zoomed3> l2) { LensStack<Zoomed1, Zoomed2, Zoomed3, Identity> ls; ls.lens1 = l1; ls.lens2 = l2; ls.lens3 = idL<Zoomed3>(); return ls;}
На этот раздействительно все!
Вопросы?
Александр Гранин[email protected]
C++ User Group, Новосибирск