Functional “Life”:
parallel cellular automata and comonads
Alexander Granin
graninas@gmail.com
C++ Russia, Saint Petersburg
Who I am?
● C++, Haskell, C#
● C++ User Group Novosibirsk, 2014
“Functional Declarative Design in C++”
● C++ Siberia Novosibirsk, 2015
“Functional Microscope: Lenses in C++”
● Talks, articles, research on FP in general, FP in C++
struct Presentation
{
Functional programming in С++
Functionally designed cellular automation
Parallel computation of cellular automation
};
4
Functional programming in C++
C++ FP Enthusiasts
● Range v3 by Eric Niebler - proposal for C++ Standard Lib
● FTL (Functional Template Library) by Bjorn Aili
● Cat by Nicola Bonelli - Category Theory elements
● Bartosz Milewski
● John Carmack
● …
● <Place your name here>
С++ User Group Novosibirsk, 2014
“Functional Declarative Design in C++”
С++ Siberia Novosibirsk, 2015
“Functional Microscope: Lenses in C++”
auto lens = personL() to addressL() to houseL();
Account account1 = {...};
Account account2 = set(lens, account1, 20);
// account2.person.address.house == 20
std::function<int(int)> modifier =
[](int old) { return old + 6; };
Account account3 = over(lens, account2, modifier);
// account3.person.address.house == 26
Lens 2 Lens 3Lens 1
FP elements in C++
● Lambdas, closures, functions (almost pure)
● std::function<>
● Immutability, POD-types
● Templates - pure functional language
● for_each(), recursion
● C++ Concepts: coming soon...
9
Simple 1-dimensional 3-state CA
● 1 dimension
● 3 states: A (“Alive”), P (“Pregnant”), D/space (“Dead”),
A A
A A
A A
P
P
A A A
1 gen P
2 gen A A A
3 gen A A A A
4 gen A P A
5 gen A A A
6 gen A A A A
7 gen A P A
template <typename T>
struct Universe {
std::vector<T> field;
int position;
};
typedef char Cell;
const Cell Pregnant = 2;
const Cell Alive = 1;
const Cell Dead = 0;
Universe<Cell>
A A A A
Universe<T>: Pointed array
Universe<Cell> u;
u.field = {D, A, A, D, A, A, D};
u.position = 3;
Immutable shift
A A A A
Universe<Cell> left (const Universe<Cell>& u) {
Universe<Cell> newU
{ u.field, u.position - 1 };
if (u.position == 0)
newU.position = u.size() - 1;
return newU;
}
Universe<Cell> right (const Universe<Cell>& u);
A A A A
A A A A
shift to right
shift to left
Observing: shift and extract
A A A A
Cell extract(const Universe<Cell>& u) {
return u.field[u.position];
}
Universe<Cell> u = {...};
Cell cur = extract (u);
Cell r = extract (right (u));
Cell rr = extract (right (right (u)));
Cell l = extract (left (u));
Cell ll = extract (left (left (u)));
D
A A A A
shift to left
shift to left
extract
Rule: observe and reduce
A A A A
P
Cell rule(const Universe<Cell>& row) {
// extract l, ll, cur, r, rr here
if (isA(l) && isA(r) && !isAorP(cur))
return Pregnant;
// ... more rules here
return Dead;
}
Applying rule: extend, extract
Cell rule (Universe<Cell> row);
Universe<Cell> extend (
std::function<Cell(Universe<Cell>)> f,
const Universe<Cell>& u);
Cell extract(const Universe<Cell>& u);
A A A A
P
P
Step: duplicate, (for_each: extend, extract)
A A A A
A A A A
A A A A
A A A A
A A A A
A A A A
A A A A A
P
A
A
P
A
Cell rule (Universe<Cell> row);
Universe<Cell> extend (
std::function<Cell(Universe<Cell>)> f,
const Universe<Cell>& u);
Universe<Universe<Cell>>
duplicate (const Universe<Cell>& u);
Universe<Cell> left (const Universe<Cell>& u);
Universe<Cell> right (const Universe<Cell>& u);
Universe<Cell> r1;
r1.position = 0;
r1.field = {D, D, D, P, D, D, D};
Universe<Cell> r2 = stepWith(rule(), r1);
Universe<Cell> r3 = stepWith(rule(), r2);
Universe<Cell> (
std::function<Cell(Universe<Cell>)> f,
const Universe<Cell>& u) {
return extend(f, ut);
}
Step
17
Generic functional approach
#define UT Universe<T>
#define UUT Universe<Universe<T>>
template <typename T> T rule (const UT& u)
template <typename T> UT left (const UT& u)
template <typename T> UT right (const UT& u)
Generic extract
template <typename T> T extract(const UT& u)
{
return u.field[u.position];
}
Generic extend
template <typename T> UT extend (
const func<T(UT)>& f,
const UT& u)
{
UUT duplicated = duplicate (u);
return { map(f, duplicated.field), u.position };
}
Generic map
template<typename A, typename B, template <class ...> class Container>
Container<B> map (
const std::function<B(A)>& f,
const Container<A>& va)
{
Container<B> vb;
std::transform(va.begin(), va.end(), std::back_inserter(vb), f);
return vb;
}
Generic duplicate
const std::function<UT(UT)> leftCreator = [](const UT& u) {return left(u); };
const std::function<UT(UT)> rightCreator = [](const UT& u) {return right(u); };
template <typename T> UUT duplicate (const UT& u)
{
return makeUniverse (leftCreator, rightCreator, u);
}
Generic makeUniverse
template <typename T> UUT makeUniverse (
const std::function<UT(UT)>& leftCreator,
const std::function<UT(UT)>& rightCreator,
const UT& u) {
std::vector<UT> lefts = tailOfGen(u.position, leftCreator, u);
std::vector<UT> rights = tailOfGen(u.size() - u.position - 1, rightCreator, u);
std::vector<UT> all;
all.swap(lefts);
all.push_back(u);
all.insert(all.end(), rights.begin(), rights.end());
return { std::move(all), u.position };
}
extract + duplicate + extend = comonad
template <typename T> T extract (const UT& u)
template <typename T> UT extend (
const func<T(UT)>& f,
const UT& u)
template <typename T> UUT duplicate (const UT& u)
24
Parallel computations in FP
Container<B> map (
const std::function<B(A)>& f,
const Container<A>& va);
Container<B> mapPar (
const std::function<B(A)>& f,
const Container<A>& va);
mapPar
template <typename A, typename B, template <class ...> class Container>
Container<B> mapPar (
const std::function<B(A)>& f,
const Container<A>& va)
{
Container<std::future<B>> pars = map(par(f), va);
std::future<Container<B>> pRes = joinPars(pars);
return pRes.get();
}
template <typename A, typename B> std::function<std::future<B>(A)> par(
const std::function<B(A)>& f)
{
return [=](const A& a)
{
return std::async(std::launch::async, [=]() { return f(a); } );
};
}
par
template <typename B> std::future<std::vector<B>> joinPars(
std::vector<std::future<B>>& pars)
{
return std::async(std::launch::async, [&]() {
std::vector<B> bs;
bs.reserve(pars.size());
for (auto& it : pars)
bs.push_back(it.get());
return bs;
});
}
joinPars
28
Parallel Game of Life benchmark
● 2 dimensions
● 2 states: A (“Alive”), D/space (“Dead”),
// Pointed array of pointed arrays
typedef Universe<Cell> LifeRow;
typedef Universe<LifeRow> LifeField;
A little bit harder...
#define UT Universe<T>
#define UUT Universe<Universe<T>>
#define UUUT Universe<Universe<Universe<T>>>
#define UUUUT Universe<Universe<Universe<Universe<T>>>>
template <typename T> UUUUT duplicate2 (const UUT& u)
template <typename T> UUT extend2 (
const func<T(UUT)>& f,
const UUT& u)
template <typename T> T extract2 (const UUT& u)
extend vs extend2
template <typename T> UT extend (
const func<T(UT)>& f,
const UT& u) {
UUT duplicated = duplicate (u);
return { map (f, duplicated.field), u.position }; // == fmap (f, duplicated.field)
}
template <typename T> UUT extend2 (
const func<T(UUT)>& f,
const UUT& uut) {
UUUUT duplicated = duplicate2 (uut);
return fmap2 (f, duplicated);
}
fmap2
template <typename T> UUT fmap2 (
const func<T(UUT)>& f,
const UUUUT& uuut) {
const func<UT(UUUT)> f2 = [=](const UUUT& uuut2) {
UT newUt;
newUt.position = uuut2.position;
newUt.field = map (f, uuut2.field);
return newUt;
};
return { map (f2, uuut.field), uuut.position }; // parallelization: map -> mapPar
}
Game of Life benchmark
Field side Sequential Parallel
(milliseconds)
50 484 283
100 3900 2291
150 12669 8005
200 30278 19415
auto l1 = gliderLifeHuge();
QBENCHMARK {
auto l2 = stepWith(rule, l1);
QVERIFY(l2.size() == HugeSize);
}
Game of Life on comonads, C++
● Highly experimental
● Sequential, async and parallel GoL
● Simple 1D 3-state CA
● Functional design
● https://github.com/graninas/CMLife
● Клеточные автоматы и комонады, by Hithroc Mehatoko
Thank you!
Alexander Granin
graninas@gmail.com
Questions?
C++ Russia, Saint Petersburg

Functional "Life": parallel cellular automata and comonads

  • 1.
    Functional “Life”: parallel cellularautomata and comonads Alexander Granin graninas@gmail.com C++ Russia, Saint Petersburg
  • 2.
    Who I am? ●C++, Haskell, C# ● C++ User Group Novosibirsk, 2014 “Functional Declarative Design in C++” ● C++ Siberia Novosibirsk, 2015 “Functional Microscope: Lenses in C++” ● Talks, articles, research on FP in general, FP in C++
  • 3.
    struct Presentation { Functional programmingin С++ Functionally designed cellular automation Parallel computation of cellular automation };
  • 4.
  • 5.
    C++ FP Enthusiasts ●Range v3 by Eric Niebler - proposal for C++ Standard Lib ● FTL (Functional Template Library) by Bjorn Aili ● Cat by Nicola Bonelli - Category Theory elements ● Bartosz Milewski ● John Carmack ● … ● <Place your name here>
  • 6.
    С++ User GroupNovosibirsk, 2014 “Functional Declarative Design in C++”
  • 7.
    С++ Siberia Novosibirsk,2015 “Functional Microscope: Lenses in C++” auto lens = personL() to addressL() to houseL(); Account account1 = {...}; Account account2 = set(lens, account1, 20); // account2.person.address.house == 20 std::function<int(int)> modifier = [](int old) { return old + 6; }; Account account3 = over(lens, account2, modifier); // account3.person.address.house == 26 Lens 2 Lens 3Lens 1
  • 8.
    FP elements inC++ ● Lambdas, closures, functions (almost pure) ● std::function<> ● Immutability, POD-types ● Templates - pure functional language ● for_each(), recursion ● C++ Concepts: coming soon...
  • 9.
    9 Simple 1-dimensional 3-stateCA ● 1 dimension ● 3 states: A (“Alive”), P (“Pregnant”), D/space (“Dead”), A A A A A A P P A A A 1 gen P 2 gen A A A 3 gen A A A A 4 gen A P A 5 gen A A A 6 gen A A A A 7 gen A P A
  • 10.
    template <typename T> structUniverse { std::vector<T> field; int position; }; typedef char Cell; const Cell Pregnant = 2; const Cell Alive = 1; const Cell Dead = 0; Universe<Cell> A A A A Universe<T>: Pointed array Universe<Cell> u; u.field = {D, A, A, D, A, A, D}; u.position = 3;
  • 11.
    Immutable shift A AA A Universe<Cell> left (const Universe<Cell>& u) { Universe<Cell> newU { u.field, u.position - 1 }; if (u.position == 0) newU.position = u.size() - 1; return newU; } Universe<Cell> right (const Universe<Cell>& u); A A A A A A A A shift to right shift to left
  • 12.
    Observing: shift andextract A A A A Cell extract(const Universe<Cell>& u) { return u.field[u.position]; } Universe<Cell> u = {...}; Cell cur = extract (u); Cell r = extract (right (u)); Cell rr = extract (right (right (u))); Cell l = extract (left (u)); Cell ll = extract (left (left (u))); D A A A A shift to left shift to left extract
  • 13.
    Rule: observe andreduce A A A A P Cell rule(const Universe<Cell>& row) { // extract l, ll, cur, r, rr here if (isA(l) && isA(r) && !isAorP(cur)) return Pregnant; // ... more rules here return Dead; }
  • 14.
    Applying rule: extend,extract Cell rule (Universe<Cell> row); Universe<Cell> extend ( std::function<Cell(Universe<Cell>)> f, const Universe<Cell>& u); Cell extract(const Universe<Cell>& u); A A A A P P
  • 15.
    Step: duplicate, (for_each:extend, extract) A A A A A A A A A A A A A A A A A A A A A A A A A A A A A P A A P A Cell rule (Universe<Cell> row); Universe<Cell> extend ( std::function<Cell(Universe<Cell>)> f, const Universe<Cell>& u); Universe<Universe<Cell>> duplicate (const Universe<Cell>& u); Universe<Cell> left (const Universe<Cell>& u); Universe<Cell> right (const Universe<Cell>& u);
  • 16.
    Universe<Cell> r1; r1.position =0; r1.field = {D, D, D, P, D, D, D}; Universe<Cell> r2 = stepWith(rule(), r1); Universe<Cell> r3 = stepWith(rule(), r2); Universe<Cell> ( std::function<Cell(Universe<Cell>)> f, const Universe<Cell>& u) { return extend(f, ut); } Step
  • 17.
    17 Generic functional approach #defineUT Universe<T> #define UUT Universe<Universe<T>> template <typename T> T rule (const UT& u) template <typename T> UT left (const UT& u) template <typename T> UT right (const UT& u)
  • 18.
    Generic extract template <typenameT> T extract(const UT& u) { return u.field[u.position]; }
  • 19.
    Generic extend template <typenameT> UT extend ( const func<T(UT)>& f, const UT& u) { UUT duplicated = duplicate (u); return { map(f, duplicated.field), u.position }; }
  • 20.
    Generic map template<typename A,typename B, template <class ...> class Container> Container<B> map ( const std::function<B(A)>& f, const Container<A>& va) { Container<B> vb; std::transform(va.begin(), va.end(), std::back_inserter(vb), f); return vb; }
  • 21.
    Generic duplicate const std::function<UT(UT)>leftCreator = [](const UT& u) {return left(u); }; const std::function<UT(UT)> rightCreator = [](const UT& u) {return right(u); }; template <typename T> UUT duplicate (const UT& u) { return makeUniverse (leftCreator, rightCreator, u); }
  • 22.
    Generic makeUniverse template <typenameT> UUT makeUniverse ( const std::function<UT(UT)>& leftCreator, const std::function<UT(UT)>& rightCreator, const UT& u) { std::vector<UT> lefts = tailOfGen(u.position, leftCreator, u); std::vector<UT> rights = tailOfGen(u.size() - u.position - 1, rightCreator, u); std::vector<UT> all; all.swap(lefts); all.push_back(u); all.insert(all.end(), rights.begin(), rights.end()); return { std::move(all), u.position }; }
  • 23.
    extract + duplicate+ extend = comonad template <typename T> T extract (const UT& u) template <typename T> UT extend ( const func<T(UT)>& f, const UT& u) template <typename T> UUT duplicate (const UT& u)
  • 24.
    24 Parallel computations inFP Container<B> map ( const std::function<B(A)>& f, const Container<A>& va); Container<B> mapPar ( const std::function<B(A)>& f, const Container<A>& va);
  • 25.
    mapPar template <typename A,typename B, template <class ...> class Container> Container<B> mapPar ( const std::function<B(A)>& f, const Container<A>& va) { Container<std::future<B>> pars = map(par(f), va); std::future<Container<B>> pRes = joinPars(pars); return pRes.get(); }
  • 26.
    template <typename A,typename B> std::function<std::future<B>(A)> par( const std::function<B(A)>& f) { return [=](const A& a) { return std::async(std::launch::async, [=]() { return f(a); } ); }; } par
  • 27.
    template <typename B>std::future<std::vector<B>> joinPars( std::vector<std::future<B>>& pars) { return std::async(std::launch::async, [&]() { std::vector<B> bs; bs.reserve(pars.size()); for (auto& it : pars) bs.push_back(it.get()); return bs; }); } joinPars
  • 28.
    28 Parallel Game ofLife benchmark ● 2 dimensions ● 2 states: A (“Alive”), D/space (“Dead”), // Pointed array of pointed arrays typedef Universe<Cell> LifeRow; typedef Universe<LifeRow> LifeField;
  • 29.
    A little bitharder... #define UT Universe<T> #define UUT Universe<Universe<T>> #define UUUT Universe<Universe<Universe<T>>> #define UUUUT Universe<Universe<Universe<Universe<T>>>> template <typename T> UUUUT duplicate2 (const UUT& u) template <typename T> UUT extend2 ( const func<T(UUT)>& f, const UUT& u) template <typename T> T extract2 (const UUT& u)
  • 30.
    extend vs extend2 template<typename T> UT extend ( const func<T(UT)>& f, const UT& u) { UUT duplicated = duplicate (u); return { map (f, duplicated.field), u.position }; // == fmap (f, duplicated.field) } template <typename T> UUT extend2 ( const func<T(UUT)>& f, const UUT& uut) { UUUUT duplicated = duplicate2 (uut); return fmap2 (f, duplicated); }
  • 31.
    fmap2 template <typename T>UUT fmap2 ( const func<T(UUT)>& f, const UUUUT& uuut) { const func<UT(UUUT)> f2 = [=](const UUUT& uuut2) { UT newUt; newUt.position = uuut2.position; newUt.field = map (f, uuut2.field); return newUt; }; return { map (f2, uuut.field), uuut.position }; // parallelization: map -> mapPar }
  • 32.
    Game of Lifebenchmark Field side Sequential Parallel (milliseconds) 50 484 283 100 3900 2291 150 12669 8005 200 30278 19415 auto l1 = gliderLifeHuge(); QBENCHMARK { auto l2 = stepWith(rule, l1); QVERIFY(l2.size() == HugeSize); }
  • 33.
    Game of Lifeon comonads, C++ ● Highly experimental ● Sequential, async and parallel GoL ● Simple 1D 3-state CA ● Functional design ● https://github.com/graninas/CMLife ● Клеточные автоматы и комонады, by Hithroc Mehatoko
  • 34.