design patterns in modern c++

Post on 12-Apr-2017

530 Views

Category:

Technology

47 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Design Patterns inModern C++, Part I

Dmitri Nesterukdmitrinеstеruk@gmаil.соm

@dnesteruk

What’s In This Talk?

• Examples of patterns and approaches in OOP design• Adapter• Composite• Specification pattern/OCP• Fluent and Groovy-style builders• Maybe monad

Adapter

STL String Complaints

• Making a string is easystring s{“hello world”};

• Getting its constituent parts is notvector<strings> words;boost::split(words, s, boost::is_any_of(“ “));

• Instead I would preferauto parts = s.split(“ “);

• It should work with “hello world”• Maybe some other goodies, e.g.

• Hide size()• Have length() as a property, not a function

Basic Adapter

class String {string s;

public:String(const string &s) : s{ s } { }

};

Implement Splitclass String {

string s;public:

String(const string &s) : s{ s } { }vector<string> split(string input){

vector<string> result;boost::split(result, s,

boost::is_any_of(input), boost::token_compress_on);return result;

}};

Length Proxying

class String {string s;

public:String(const string &s) : s{ s } { }vector<string> split(string input);size_t get_length() const { return s.length(); }

};

Length Proxying

class String {string s;

public:String(const string &s) : s{ s } { }vector<string> split(string input);size_t get_length() const { return s.length(); }

// non-standard!__declspec(property(get = get_length)) size_t length;

};

String Wrapper Usage

String s{ "hello world" };cout << "string has " << s.length << " characters" << endl;

auto words = s.split(" ");for (auto& word : words)cout << word << endl;

Adapter Summary

• Aggregate objects (or keep a reference)• Can aggregate more than one

• E.g., string and formatting

• Replicate the APIs you want (e.g., length)• Miss out on the APIs you don’t need• Add your own features :)

Composite

Scenario

• Neurons connect to other neurons

• Neuron layers are collections of neurons

• These two need to be connectable

Scenariostruct Neuron{

vector<Neuron*> in, out;unsigned int id;

Neuron(){

static int id = 1;this->id = id++;

}}

Scenario

struct NeuronLayer : vector<Neuron>{NeuronLayer(int count){while (count-- > 0)emplace_back(Neuron{});

}}

State Space Explosition

• void connect_to(Neuron& other){out.push_back(&other);other.in.push_back(this);

}• Unfortunately, we need 4 functions

• Neuron to Neuron• Neuron to NeuronLayer• NeuronLayer to Neuron• NeuronLayer to NeuronLayer

One Function Solution?

• Simple: treat Neuron as NeuronLayer of size 1• Not strictly correct• Does not take into account other concepts (e.g., NeuronRing)

• Better: expose a single Neuron in an iterable fashion• Other programming languages have interfaces for iteration

• E.g., C# IEnumerable<T>• yield keyword

• C++ does duck typing• Expects begin/end pair

• One function solution not possible, but…

Generic Connection Functionstruct Neuron{

...template <typename T> void connect_to(T& other){

for (Neuron& to : other)connect_to(to);

}template<> void connect_to<Neuron>(Neuron& other){

out.push_back(&other);other.in.push_back(this);

}};

Generic Connection Function

struct NeuronLayer : vector<Neuron>{…template <typename T> void connect_to(T& other){for (Neuron& from : *this)for (Neuron& to : other)from.connect_to(to);

}};

How to Iterate on a Single Value?

struct Neuron{…Neuron* begin() { return this; }Neuron* end() { return this + 1; }

};

API Usage

Neuron n, n2;NeuronLayer nl, nl2;

n.connect_to(n2);n.connect_to(nl);nl.connect_to(n);nl.connect_to(nl2);

Specification Pattern and the OCP

Open-Closed Principle

• Open for extension, closed for modification• Bad: jumping into old code to change a stable, functioning system• Good: making things generic enough to be externally extensible• Example: product filtering

Scenario

enum class Color { Red, Green, Blue };enum class Size { Small, Medium, Large };

struct Product{std::string name;Color color;Size size;

};

Filtering Productsstruct ProductFilter{

typedef std::vector<Product*> Items;static Items by_color(Items items, Color color){

Items result;for (auto& i : items)

if (i->color == color)result.push_back(i);

return result;}

}

Filtering Productsstruct ProductFilter{

typedef std::vector<Product*> Items;static Items by_color(Items items, Color color) { … }static Items by_size(Items items, Size size){

Items result;for (auto& i : items)

if (i->size == size)result.push_back(i);

return result;}

}

Filtering Productsstruct ProductFilter{

typedef std::vector<Product*> Items;static Items by_color(Items items, Color color) { … }static Items by_size(Items items, Size size) { … }static Items by_color_and_size(Items items, Size size, Color color){Items result;for (auto& i : items)

if (i->size == size && i->color == color)result.push_back(i);

return result;}

}

Violating OCP

• Keep having to rewrite existing code• Assumes it is even possible (i.e. you have access to source code)

• Not flexible enough (what about other criteria?)• Filtering by X or Y or X&Y requires 3 functions

• More complexity -> state space explosion

• Specification pattern to the rescue!

ISpecification and IFilter

template <typename T> struct ISpecification{virtual bool is_satisfied(T* item) = 0;

};template <typename T> struct IFilter{virtual std::vector<T*> filter(std::vector<T*> items, ISpecification<T>& spec) = 0;

};

A Better Filterstruct ProductFilter : IFilter<Product>{

typedef std::vector<Product*> Products;Products filter(

Products items, ISpecification<Product>& spec) override

{Products result;for (auto& p : items)

if (spec.is_satisfied(p))result.push_back(p);

return result;}

};

Making Specifications

struct ColorSpecification : ISpecification<Product>{Color color;explicit ColorSpecification(const Color color): color{color} { }

bool is_satisfied(Product* item) override {return item->color == color;

}}; // same for SizeSpecification

Improved Filter Usage

Product apple{ "Apple", Color::Green, Size::Small };Product tree { "Tree", Color::Green, Size::Large };Product house{ "House", Color::Blue, Size::Large };

std::vector<Product*> all{ &apple, &tree, &house };

ProductFilter pf;ColorSpecification green(Color::Green);

auto green_things = pf.filter(all, green);for (auto& x : green_things)std::cout << x->name << " is green" << std::endl;

Filtering on 2..N criteria

• How to filter by size and color?• We don’t want a SizeAndColorSpecification

• State space explosion

• Create combinators• A specification which combines two other specifications• E.g., AndSpecification

AndSpecification Combinatortemplate <typename T> struct AndSpecification : ISpecification<T>{

ISpecification<T>& first;ISpecification<T>& second;

AndSpecification(ISpecification<T>& first, ISpecification<T>& second)

: first{first}, second{second} { }

bool is_satisfied(T* item) override{

return first.is_satisfied(item) && second.is_satisfied(item);}

};

Filtering by Size AND Color

ProductFilter pf;ColorSpecification green(Color::Green);SizeSpecification big(Size::Large);AndSpecification<Product> green_and_big{ big, green };

auto big_green_things = pf.filter(all, green_and_big);for (auto& x : big_green_things)

std::cout << x->name << " is big and green" << std::endl;

Specification Summary

• Simple filtering solution is• Too difficult to maintain, violates OCP• Not flexible enough

• Abstract away the specification interface• bool is_satisfied_by(T something)

• Abstract away the idea of filtering• Input items + specification set of filtered items

• Create combinators (e.g., AndSpecification) for combining multiple specifications

Fluent and Groovy-Style Builders

Scenario

• Consider the construction of structured data• E.g., an HTML web page

• Stuctured and formalized• Rules (e.g., P cannot contain another P)• Can we provide an API for building these?

Building a Simple HTML List

// <ul><li>hello</li><li>world</li></ul>string words[] = { "hello", "world" };ostringstream oss;oss << "<ul>";for (auto w : words)

oss << " <li>" << w << "</li>";oss << "</ul>";printf(oss.str().c_str());

HtmlElement

struct HtmlElement{

string name;string text;vector<HtmlElement> elements;const size_t indent_size = 2;

string str(int indent = 0) const; // pretty-print}

Html Builder (non-fluent)

struct HtmlBuilder{

HtmlElement root;HtmlBuilder(string root_name) { root.name = root_name; }void add_child(string child_name, string child_text){

HtmlElement e{ child_name, child_text };root.elements.emplace_back(e);

}string str() { return root.str(); }

}

Html Builder (non-fluent)

HtmlBuilder builder{"ul"};builder.add_child("li", "hello")builder.add_child("li", "world");cout << builder.str() << endl;

Making It Fluent

struct HtmlBuilder{

HtmlElement root;HtmlBuilder(string root_name) { root.name = root_name; }HtmlBuilder& add_child(string child_name, string child_text){

HtmlElement e{ child_name, child_text };root.elements.emplace_back(e);return *this;

}string str() { return root.str(); }

}

Html Builder

HtmlBuilder builder{"ul"};builder.add_child("li", "hello").add_child("li", "world");cout << builder.str() << endl;

Associate Builder & Object Being Built

struct HtmlElement{

static HtmlBuilder build(string root_name){

return HtmlBuilder{root_name};}

};

// usage:HtmlElement::build("ul")

.add_child_2("li", "hello").add_child_2("li", "world");

Groovy-Style Builders

• Express the structure of the HTML in code• No visible function calls• UL {

LI {“hello”},LI {“world”}

}• Possible in C++ using uniform initialization

Tag (= HTML Element)

struct Tag{

string name;string text;vector<Tag> children;vector<pair<string, string>> attributes;

protected:Tag(const std::string& name, const std::string& text)

: name{name}, text{text} { }Tag(const std::string& name, const std::vector<Tag>& children)

: name{name}, children{children} { }}

Paragraph

struct P : Tag{

explicit P(const std::string& text): Tag{"p", text}

{}P(std::initializer_list<Tag> children): Tag("p", children)

{}

};

Image

struct IMG : Tag{

explicit IMG(const std::string& url): Tag{"img", ""}

{attributes.emplace_back(make_pair("src", url));

}};

Example Usage

std::cout <<

P {IMG {"http://pokemon.com/pikachu.png"}

}

<< std::endl;

Facet Builders

• An HTML element has different facets• Attributes, inner elements, CSS definitions, etc.

• A Person class might have different facets• Address• Employment information

• Thus, an object might necessitate several builders

Personal/Work Information

class Person{

// addressstd::string street_address, post_code, city;

// employmentstd::string company_name, position;int annual_income = 0;

Person() {} // private!}

Person Builder (Exposes Facet Builders)class PersonBuilder{

Person p;protected:

Person& person;explicit PersonBuilder(Person& person)

: person{ person } { }public:

PersonBuilder() : person{p} { }operator Person() { return std::move(person); }

// builder facetsPersonAddressBuilder lives();PersonJobBuilder works();

}

Person Builder (Exposes Facet Builders)class PersonBuilder{

Person p;protected:

Person& person;explicit PersonBuilder(Person& person)

: person{ person } { }public:

PersonBuilder() : person{p} { }operator Person() { return std::move(person); }

// builder facetsPersonAddressBuilder lives();PersonJobBuilder works();

}

Person Builder Facet Functions

PersonAddressBuilder PersonBuilder::lives(){

return PersonAddressBuilder{ person };}

PersonJobBuilder PersonBuilder::works(){

return PersonJobBuilder{ person };}

Person Address Builderclass PersonAddressBuilder : public PersonBuilder{

typedef PersonAddressBuilder Self;public:

explicit PersonAddressBuilder(Person& person) : PersonBuilder{ person } { }

Self& at(std::string street_address){

person.street_address = street_address;return *this;

}Self& with_postcode(std::string post_code);Self& in(std::string city);

};

Person Job Builder

class PersonJobBuilder : public PersonBuilder{

typedef PersonJobBuilder Self;public:

explicit PersonJobBuilder(Person& person) : PersonBuilder{ person } { }

Self& at(std::string company_name);Self& as_a(std::string position);Self& earning(int annual_income);

};

Back to Person

class Person{

// fieldspublic:

static PersonBuilder create();

friend class PersonBuilder;friend class PersonAddressBuilder;friend class PersonJobBuilder;

};

Final Person Builder Usage

Person p = Person::create().lives().at("123 London Road")

.with_postcode("SW1 1GB")

.in("London").works().at("PragmaSoft")

.as_a("Consultant")

.earning(10e6);

Maybe Monad

Presence or Absence

• Different ways of expressing absence of value• Default-initialized value

• string s; // there is no ‘null string’

• Null value• Address* address;

• Not-yet-initialized smart pointer• shared_ptr<Address> address

• Idiomatic• boost::optional

Monads

• Design patterns in functional programming• First-class function support• Related concepts

• Algebraic data types• Pattern matching

• Implementable to some degree in C++• Functional objects/lambdas

Scenario

struct Address{char* house_name; // why not string?

}

struct Person{Address* address;

}

Print House Name, If Any

void print_house_name(Person* p){if (p != nullptr &&

p->address != nullptr && p->address->house_name != nullptr)

{cout << p->address->house_name << endl;

}}

Maybe Monad

• Encapsulate the ‘drill down’ aspect of code• Construct a Maybe<T> which keeps context• Context: pointer to evaluated element

• person -> address -> name

• While context is non-null, we drill down• If context is nullptr, propagation does not happen• All instrumented using lambdas

Maybe<T>template <typename T>struct Maybe {

T* context;Maybe(T *context) : context(context) { }

};

// but, given Person* p, we cannot make a ‘new Maybe(p)’

template <typename T> Maybe<T> maybe(T* context){

return Maybe<T>(context);}

Usage So Far

void print_house_name(Person* p){maybe(p). // now drill down :)

}

Maybe::Withtemplate <typename T> struct Maybe{

...template <typename TFunc>auto With(TFunc evaluator){

if (context == nullptr)return ??? // cannot return maybe(nullptr) :(

return maybe(evaluator(context));};

}

What is ???

• In case of failure, we need to return Maybe<U>• But the type of U should be the return type of evaluator• But evaluator returns U* and we need U• Therefore…• return Maybe<

typename remove_pointer<decltype(evaluator(context))

>::type>(nullptr);

Maybe::With Finished

template <typename TFunc> auto With(TFunc evaluator){

if (context == nullptr)return Maybe<typename remove_pointer<

decltype(evaluator(context))>::type>(nullptr);return maybe(evaluator(context));

};

Usage So Far

void print_house_name(Person* p){maybe(p) // now drill down :).With([](auto x) { return x->address; }).With([](auto x) { return x->house_name; }). // print here (if context is not null)

}

Maybe::Do

template <typename TFunc>auto Do(TFunc action){// if context is OK, perform action on it if (context != nullptr) action(context);

// no context transition, so...return *this;

}

How It Works

• print_house_name(nullptr)• Context is null from the outset and continues to be null• Nothing happens in the entire evaluation chain

• Person p; print_house_name(&p);• Context is Person, but since Address is null, it becomes null henceforth

• Person p;p->Address = new Address;p->Address->HouseName = “My Castle”;print_house_name(&p);

• Everything works and we get our printout

Maybe Monad Summary

• Example is not specific to nullptr• E.g., replace pointers with boost::optional

• Default-initialized types are harder• If s.length() == 0, has it been initialized?

• Monads are difficult due to lack of functional support• [](auto x) { return f(x); } instead ofx => f(x) as in C#

• No implicits (e.g. Kotlin’s ‘it’)

That’s It!

• Questions?• Design Patterns in C++ courses on Pluralsight• dmitrinеsteruk /at/ gmail.com• @dnesteruk

Design Patterns in Modern C++, Part II

Dmitri Nesterukdmitrinеstеruk@gmаil.соm

@dnesteruk

What’s In This Talk?

• Part II of my Design Patterns talks• Part I of the talk available online in English and Russian

• Examples of design patterns implemented in C++• Disclaimer: C++ hasn’t internalized any patterns (yet)

• Patterns• Memento• Visitor• Observer• Interpreter

MementoHelping implement undo/redo

Bank Account

• Bank account has a balance• Balance changed via

• Withdrawals• Deposits

• Want to be able to undo an erroneous transaction

• Want to navigate to an arbitrary point in the account’s changeset

class BankAccount{

int balance{0};public:

void deposit(int x) {balance += x;

}void withdraw(int x) {

if (balance >= x)balance -= x;

}};

Memento (a.k.a. Token, Cookie)

class Memento{int balance;

public:Memento(int balance): balance(balance)

{}friend class BankAccount;

};

• Keeps the state of the balance at a particular point in time• State is typically private

• Can also keep reason for latest change, amount, etc.

• Returned during bank account changes

• Memento can be used to restore object to a particular state

Deposit and Restore

Memento deposit(int amount){balance += amount;return { balance };

}void restore(const Memento& m){balance = m.balance;

}

BankAccount ba{ 100 };auto m1 = ba.deposit(50); // 150auto m2 = ba.deposit(25); // 175// undo to m1ba.restore(m1); // 150// redoba.restore(m2); // 175

Storing Changes

class BankAccount {int balance{0}, current;vector<shared_ptr<Memento>> changes;

public:BankAccount(const int balance) : balance(balance){

changes.emplace_back(make_shared<Memento>(balance));current = 0;

}};

Undo and Redo

shared_ptr<Memento> deposit(intamount){balance += amount;auto m = make_shared<Memento>(balance);

changes.push_back(m);++current;return m;

}

shared_ptr<Memento> undo(){if (current > 0){--current;auto m = changes[current];balance = m->balance;return m;

}return{};

}

Undo/Redo Problems

• Storage excess• Need to store only changes, but still…

• Need to be able to overwrite the Redo step• Undo/redo is typically an aggregate operation

Cookie Monster!

• Why doesn’t push_back() return a reference?

• Well, it could, but…• As long as you’re not resizing due

to addition• How to refer to an element of

vector<int>?• Dangerous unless append-only• Change to vector< shared_ptr<int>>• Change to list<int>• Some range-tracking magic token

solution

• Return a magic_cookie that• Lets you access an element provided

it exists• Is safe to use if element has been

deleted• Keeps pointing to the correct element

even when container is reordered• Requires tracking all mutating

operations• Is it worth it?

ObserverThis has been done to death, but…

Simple Model

class Person{int age;

public:void set_age(int age){this->age = age;

}int get_age() const{return age;

}};

• Person has an age: private field with accessor/mutator

• We want to be informedwhenever age changes

• Need to modify the setter!

Person Listener

struct PersonListener{virtual ~PersonListener() = default;virtual void person_changed(Person& p,

const string& property_name, const any new_value) = 0;};

Person Implementation

class Person{vector<PersonListener*> listeners;

public:void subscribe(PersonListener* pl) {listeners.push_back(pl);

}void notify(const string& property_name, const any new_value){for (const auto listener : listeners)listener->person_changed(*this, property_name, new_value);

}};

Setter Change

void set_age(const int age){if (this->age == age) return;this->age = age;notify("age", this->age);

}

Consumption

struct ConsoleListener : PersonListener{void person_changed(Person& p,

const string& property_name, const any new_value) override

{cout << "person's " << property_name

<< " has been changed to ";if (property_name == "age"){

cout << any_cast<int>(new_value);}cout << "\n";

}};

Person p{14};ConsoleListener cl;p.subscribe(&cl);p.set_age(15);p.set_age(16);

Dependent Property

bool get_can_vote() const{return age >= 16;

}

• Where to notify?• How to detect that any

affecting property has changed?

• Can we generalize?

Notifying on Dependent Property

void set_age(const int age){if (this->age == age) return;

auto old_c_v = get_can_vote();

this->age = age;notify("age", this->age);

auto new_c_v = get_can_vote();if (old_c_v != new_c_v){

notify("can_vote", new_c_v);}

}

save old value

get new value compare and notify only if

changed

Observer Problems

• Multiple subscriptions by a single listener• Are they allowed? If not, use std::set

• Un-subscription• Is it supported?• Behavior if many subscriptions/one listener?

• Thread safety• Reentrancy

Thread Safetystatic mutex mtx;class Person {⋮void subscribe(PersonListener* pl){lock_guard<mutex> guard{mtx};⋮

}void unsubscribe(PersonListener* pl){lock_guard<mutex> guard{mtx};for (auto it : listeners){if (*it == pl) *it = nullptr;// erase-remove in notify()

}}

};

• Anything that touches the list of subscribers is locked• Reader-writer locks better (shared_lock for

reading, unique_lock for writing)

• Unsubscription simply nulls the listener• Must check for nullptr• Remove at the end of notify()

• Alternative: use concurrent_vector• Guaranteed thread-safe addition• No easy removal

Reentrancy

struct TrafficAdministration : Observer<Person>{void field_changed(Person& source,

const string& field_name){

if (field_name == "age"){if (source.get_age() < 17)

cout << "Not old enough to drive!\n";else{

// let's not monitor them anymorecout << "We no longer care!\n";source.unsubscribe(this);

}}}};

Age changes (1617):• notify() called• Lock taken

field_changed• unsubscribe()

unsubscribe()• Tries to take a lock• But it’s already taken

Observer Problems

• Move from mutex to recursive_mutex• Doesn’t solve all problems• See Thread-safe Observer Pattern – You’re doing it Wrong (Tony Van

Eerd)

Boost.Signals2• signal<T>

• A signal that can be sent to anyone willing to listen

• T is the type of the slot function• A slot is the function that receives the

signal• Ordinary function• Functor, std::function• Lambda

• Connection• signal<void()> s;

creates a signal• auto c = s.connect([](){

cout << “test” << endl;});connects the signal to the slot

• More than one slot can be connected to a signal

• Disconnection• c.disconnect();• Disconnects all slots

• Slots can be blocked• Temporarily disabled• Used to prevent infinite recursion• shared_connection_block(c)• Unblocked when block is destroyed, or

explicitly viablock.unblock();

INotifyPropertyChanged<T>

template <typename T>struct INotifyPropertyChanged{virtual ~INotifyPropertyChanged() = default;signal<void(T&, const string&)> property_changed;

};struct Person : INotifyPropertyChanged<Person>{void set_age(const int age){if (this->age == age) return;

this->age = age;property_changed(*this, "age");}

};

Consuming INPC Model

Person p{19};p.property_changed.connect(

[](Person&, const string& prop_name){

cout << prop_name << " has been changed" << endl;});

p.set_age(20);

InterpreterMake your own programming language!

Interpreter

• Interpret textual input• A branch of computer science

• Single item: atoi, lexical_cast, etc.• Custom file format: XML, CSV• Embedded DSL: regex• Own programming language

Interpreting Numeric Expressions

(13-4)-(12+1)

Lex: [(] [13] [-] [4] [)] [-] …

Parse: Op(-, Op(-, 13, 4), Op(+,12,1))

Token

struct Token{enum Type { integer, plus, minus, lparen, rparen

} type;

string text;

explicit Token(Type type, const string& text) :type{type}, text{text} {}

};

Lexingvector<Token> lex(const string& input){

vector<Token> result;for (int i = 0; i < input.size(); ++i){

switch (input[i]){case '+':

result.push_back(Token{ Token::plus, "+" });break;

case '-':result.push_back(Token{ Token::minus, "-" });break;

case '(':result.push_back(Token{ Token::lparen, "(" });break;

case ')':result.push_back(Token{ Token::rparen, ")" });break;

default:ostringstream buffer;buffer << input[i];for (int j = i + 1; j < input.size(); ++j){

if (isdigit(input[j])){buffer << input[j];++i;

}else{result.push_back(Token{ Token::integer, buffer.str() });

break;}

}…

Parsing Structuresstruct Element{virtual ~Element() = default;virtual int eval() const = 0;

};

struct Integer : Element{int value;explicit Integer(const int value): value(value)

{}int eval() const override { return value;

}};

struct BinaryOperation : Element{enum Type { addition, subtraction } type;shared_ptr<Element> lhs, rhs;

int eval() const override{if (type == addition) return lhs->eval() + rhs->eval();

return lhs->eval() - rhs->eval();}

};shared_ptr<Element> parse(const vector<Token>& tokens){⋮

}

Parsing Numeric Expressions

string input{ "(13-4)-(12+1)" };auto tokens = lex(input);try {auto parsed = parse(tokens);cout << input << " = " << parsed->eval() << endl;

} catch (const exception& e){cout << e.what() << endl;

}

Boost.Sprit

• A Boost library for interpreting text• Uses a de facto DSL for defining the parser• Defining a separate lexer not mandatory• Favors Boost.Variant for polymorphic types• Structurally matches definitions to OOP structures

Tlön Programming Language

• Proof-of-concept language transpiled into C++• Parser + pretty printer + notepadlike app• Some language features

• Shorter principal integral types• Primary constructors• Tuples• Non-keyboard characters (keyboard-unfriendly)

Visitor

Adding Side Functionality to Classes

• C++ Committee not liking UCS• Or any other “extension function” kind of deal• Possible extensions on hierarchies end up being intrusive

• Need to modify the entire hierarchy

That’s It!

• Design Patterns in C++ courses on Pluralsight• Leanpub book (work in progress!)• Tlön Programming Language• dmitrinеsteruk /at/ gmail.com• @dnesteruk

top related