47
Функционально-декларативный дизайн на С++ Александр Гранин [email protected] C++ User Group, Новосибирск

Функционально декларативный дизайн на C++

Embed Size (px)

Citation preview

Page 1: Функционально декларативный дизайн на C++

Функционально-декларативный дизайн на С++

Александр Гранин[email protected]

C++ User Group, Новосибирск

Page 2: Функционально декларативный дизайн на C++

О себе

● C++ → Haskell

● Рассказывал на DevDay@2GIS о Haskell

● Рассказывал на TechTalks@NSU о ФП

● Статьи на Хабре о дизайне в ФП

● Работаю в “Лаборатории Касперского”

Page 3: Функционально декларативный дизайн на C++

struct Presentation{

Описание задачиФункциональная комбинаторикаПродвинутый дизайнТестированиеПроблемы и особенности

};

Page 4: Функционально декларативный дизайн на C++

О задаче

● Игра “Амбер” (По мотивам “Хроник Амбера” Роджера Желязны)

● C++11 (Qt C++ 5.2.1, gcc 4.8.2)

● Функционально-декларативный дизайн

● Максимум смысла, минимум кода

● https://github.com/graninas/Amber

Page 5: Функционально декларативный дизайн на C++

Ограничения● Нет классов, только struct (POD)

● Списки инициализации!

● Нет циклов for(;;), есть for_each()

● Всяческие eDSLs

● Иммутабельность

● Лямбды, функции, замыкания

Page 6: Функционально декларативный дизайн на C++

Структура Амбера

Полюс Амбера Средние миры

Полюс Хаоса

Амбер

Бергма

Кашфа

Авалон

Земля

ДворыХаоса

Page 7: Функционально декларативный дизайн на C++

Координаты тени, игрока

Полюс Амбера

Амбер

Бергма

Кашфа

Авалон

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 Расстояние до Хаоса

Page 8: Функционально декларативный дизайн на C++

Коородинаты тени, игрока

namespace Element {enum ElementType { Air, Sky, Water, Ground};

}

Амбер

Бергма

Кашфа

Авалон

Игрок:

90 Земля10 Вода100 Воздух70 Небо

typedef std::map<Element::ElementType, int> ShadowStructure;

Page 9: Функционально декларативный дизайн на C++

Декларативное задание координат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) };}

Page 10: Функционально декларативный дизайн на C++

Определение тениtypedef std::function<ShadowStructure(ShadowStructure, Direction::DirectionType)> ShadowVariator;

struct Shadow { ShadowName name; ShadowVariator variator; ShadowStructure structure; double influence;};

Page 11: Функционально декларативный дизайн на C++

typedef std::function<ShadowStructure(ShadowStructure, Direction::DirectionType)> ShadowVariator;

const ShadowVariator identityVariator = [](const ShadowStructure& structure, Direction::DirectionType){ return structure;};

ShadowVariator координат игрока

NGround

SWater

ESky

WAir

Page 12: Функционально декларативный дизайн на C++

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... }};

Page 13: Функционально декларативный дизайн на C++

Игровой процесс

AmberTaskEvaluator

Amber 1

AmberTaskAmberTaskAmberTask Amber 2

Input Output

Page 14: Функционально декларативный дизайн на C++

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; };

Page 15: Функционально декларативный дизайн на C++

Список задач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);}

Page 16: Функционально декларативный дизайн на C++

Выполнение списка задач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(), [&currentAmber](const AmberTask& task) { currentAmber = task(currentAmber); }); return currentAmber;}

Page 17: Функционально декларативный дизайн на C++

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...

Page 18: Функционально декларативный дизайн на C++

Конструирование лямбдAmberTask goDirection(Direction::DirectionType dir) { return [dir](const Amber& amber) { // Bla-bla with dir };}

AmberTaskList tasks = { goDirection(Direction::North), inflateShadowStorms, tickWorldTime };

Page 19: Функционально декларативный дизайн на C++

Небезопасный код

const AmberTask inflateShadowStorms = [](const Amber& amber){ throw std::runtime_error("Shit happened! :)");};

Page 20: Функционально декларативный дизайн на C++

Данные + результат onSuccess Task

Amber 1Result 1

Amber 2Result 2onFail Task

if (Result 1== Success)

+

-

safeEvalTask

safeEvalTask

Page 21: Функционально декларативный дизайн на C++

Комбинаторный 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;};

Page 22: Функционально декларативный дизайн на C++

Комбинаторы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.}

Page 23: Функционально декларативный дизайн на C++

Как может выглядеть 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;}

Page 24: Функционально декларативный дизайн на C++

Обобщение безопасного типа// Было: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;};

Page 25: Функционально декларативный дизайн на C++

Обобщение безопасного типа// Было:struct Artifact { Amber amber; bool success;};

// Стало:enum MaybeValue { Just, Nothing};

template <typename Data>struct Maybe { Data data; MaybeValue mValue;};

Page 26: Функционально декларативный дизайн на C++

Обобщение безопасного типа// Было: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);

Page 27: Функционально декларативный дизайн на C++

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);}

Page 28: Функционально декларативный дизайн на C++

Монадические функции в 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 };}

Page 29: Функционально декларативный дизайн на C++

Использование 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;}

Page 30: Функционально декларативный дизайн на C++

Использование 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;}

Page 31: Функционально декларативный дизайн на C++

Связывание многих 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;}

Page 32: Функционально декларативный дизайн на C++

Простая реализация 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;};

Page 33: Функционально декларативный дизайн на C++

Простая реализация 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;}

Page 34: Функционально декларативный дизайн на C++

Простая реализация 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;}

Page 35: Функционально декларативный дизайн на C++

Тестирование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);}

Page 36: Функционально декларативный дизайн на C++

Положительные моменты

● Лямбды - универсальный инструмент дизайна● Краткость функционального кода● Высокая модульность● Прекрасная тестируемость● Редуцирована структурная сложность ПО● Многие задачи решаются проще, понятнее● Больше внимания задаче, а не борьбе с

языком

Page 37: Функционально декларативный дизайн на C++

Проблемы и особенности

● Массированное копирование данных● Большее потребление памяти● Меньшая производительность● Опасные замыкания в лямбдах● Чистота функций не контролируется● Нет алгебраических типов данных● Нет каррирования, заменители плохи

Page 38: Функционально декларативный дизайн на C++

Мы это сделали,всем спасибо! :)

Александр Гранин[email protected]

C++ User Group, Новосибирск

Page 39: Функционально декларативный дизайн на C++

А теперь - бонус!

Page 40: Функционально декларативный дизайн на C++

Проблема: глубокие структуры

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!";}

Page 41: Функционально декларативный дизайн на C++

Проблема: глубокие структуры

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;}

Page 42: Функционально декларативный дизайн на C++

Встречайте: линзы!

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!" };};

Page 43: Функционально декларативный дизайн на C++

Линза - это геттер + сеттер

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));}

Page 44: Функционально декларативный дизайн на 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; });}

Page 45: Функционально декларативный дизайн на C++

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;};

Page 46: Функционально декларативный дизайн на C++

Зуммирование и фокусировка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;}

Page 47: Функционально декларативный дизайн на C++

На этот раздействительно все!

Вопросы?

Александр Гранин[email protected]

C++ User Group, Новосибирск