Александр Гранин, "Декларативно-функциональный...

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

Upload: platonov-sergey

Post on 28-Jul-2015

738 views

Category:

Technology


2 download

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

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

AmberTaskEvaluator

Amber 1

AmberTaskAmberTaskAmberTask Amber 2

Input Output

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(), [&currentAmber](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);}

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

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

языком

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

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

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

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

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

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

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

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, Новосибирск