haskell — Эквивалент экзистенциального количественного определения в C ++?

Чтобы помочь себе в изучении C ++, я работаю над реализацией красно-черного дерева. Приходящий из
Хаскелл, я подумал, что было бы интересно посмотреть, смогу ли я свойства
красно-черное дерево

статически в системе типов C ++:

  1. Узел красный или черный.
  2. Корень черный […]
  3. Все листья (ноль) черные.
  4. Если узел красный, то оба его потомка черные.
  5. Каждый путь от данного узла к любому из его дочерних узлов NIL содержит
    такое же количество черных узлов. […]

Я выяснил, как создать типы для узлов дерева, чтобы удовлетворить ограничения 1,
3, 4 и 5 с использованием шаблонов:

template <typename Key, typename Value>
class RedBlackTree {
private:
enum class color { Black, Red };

// [1. A node is either red or black]
template <color Color, size_t Height>
struct Node {};

// [3. All leaves are black]
struct Leaf : public Node<color::Black, 0> {};

template <color Left, color Right, size_t ChildHeight>
struct Branch {
public:
template <color ChildColor>
using Child = unique_ptr<Node<ChildColor, ChildHeight>>;

Key key;
Value value;
Child<Left> left;
Child<Right> right;

Branch(Key&& key, Value&& value, Child<Left> left, Child<Right> right) :
key { key }, value { value }, left { left }, right { right } {}
};

// [4. If a node is red, then both its children are black.]
// [5. Every path from a given node to any of its descendant NIL nodes contains
//     the same number of black nodes.]
template <size_t Height>
struct RedBranch: public Node<color::Red, Height>
, public Branch<color::Black, color::Black, Height> {
public:
using RedBlackTree::Branch;
};

// [5. Every path from a given node to any of its descendant NIL nodes contains
//     the same number of black nodes.]
template <size_t Height, color Left, color Right>
struct BlackBranch: public Node<color::Black, Height>
, public Branch<Left, Right, Height-1> {
public:
using RedBlackTree::Branch;
};

// ...
};

Где я в тупике дает root указатель, который будет храниться в RedBlackTree
экземпляр типа, который удовлетворяет свойству 2, но все еще полезен.

Что я хочу, это что-то вроде:

template <typename Key, typename Value>
class RedBlackTree {
//...
unique_ptr<Node<color::Black,?>> root = std::make_unique<Leaf>();
//...
}

(заимствовать синтаксис из Java), так что я могу подстановочный знак по высоте дерева. Это из
Конечно, не работает.

Я мог бы получить свой код для компиляции, если бы я сделал

template <typename Key, typename Value, size_t TreeHeight>
class RedBlackTree {
//...
unique_ptr<Node<color::Black,TreeHeight>> root = std::make_unique<Leaf>();
//...
}

Но это не тот тип дерева, который мне нужен — мне не нужен тип самого дерева.
отражать его высоту, в противном случае тип моего дерева может измениться, когда я вставлю
пара ключ-значение. Я хочу иметь возможность обновить мой root содержать указатель на черный
Node из любой рост.

Вернувшись в Хаскель-Лэнд, я бы решил эту проблему, используя экзистенциальный
квантование
:

data Color = Black | Red

data Node (color :: Color) (height :: Nat) key value where
Leaf :: Node 'Black 0 key value
BlackBranch :: Branch left right height key value -> Node 'Black (height+1) key value
RedBranch :: Branch 'Black 'Black height key value -> Node 'Red height key value

data Branch (left :: Color) (right :: Color) (childHeight :: Nat) key value = Branch
{ left  :: Node left childHeight key value
, right :: Node right childHeight key value
, key   :: key
, value :: value
}

data RedBlackTree key value where
RedBlackTree :: { root :: Node 'Black height key value } -> RedBlackTree key value

Есть ли эквивалентная концепция в C ++ 14 (или, может быть, C ++ 17), или альтернативный способ, которым я мог бы написать свой struct определения, чтобы быть в состоянии дать root полезный и правильный тип?

12

Решение

template<class K, class T>
struct NodeView {
virtual NodeView const* left() const = 0;
virtual NodeView const* right() const = 0;
virtual K const& key() const = 0;
virtual T const& value() const = 0;
private:
~NodeView() {} // no deleting it!
};

это интерфейс.

Пусть ваши узлы дерева реализуют этот интерфейс. Им разрешено и рекомендуется возвращаться nullptr когда у них нет ребенка на одной стороне или другой.

В базовой структуре возьмите корневой узел в качестве параметра шаблона. Убедитесь, что он черный, используя шаблон tomfoolery.

использование make_shared хранить его в std::shared_ptr

auto tree = std::make_shared<std::decay_t<decltype(tree)>>(decltype(tree)(tree));
std::shared_ptr<NodeView const> m_tree = std::move(tree);

Где m_tree участник является членом вашей корневой структуры управления.

Теперь у вас есть доступ только для чтения к общему дереву. Код, который хранит его, гарантированно будет сбалансированным красно-черным деревом во время компиляции. Во время выполнения это просто гарантированно будет дерево.

В интерфейс, который я написал выше, вы могли бы вставить больше информации о гарантии, но это могло бы загромождать интерфейс за пределы того, что обычно должен знать читатель. (вроде бы, есть разные Red а также Black тип узла интерфейса).

Теперь, если тело одной короткой функции слишком много, чтобы доверять, и вы предпочитаете доверять стенке кода шаблона, мы можем сделать это:

template<template<class...>class Test, class T>
struct restricted_shared_ptr {
template<class U,
std::enable_if_t< Test<U>{}, int> = 0
>
restricted_shared_ptr( std::shared_ptr<U> pin ):ptr(std::move(pin)) {}
restricted_shared_ptr(restricted_shared_ptr const&)=default;
restricted_shared_ptr(restricted_shared_ptr &&)=default;
restricted_shared_ptr& operator=(restricted_shared_ptr const&)=default;
restricted_shared_ptr& operator=(restricted_shared_ptr &&)=default;
restricted_shared_ptr() = default;
T* get() const { return ptr.get(); }
explicit operator bool() const { return (bool)ptr; }
T& operator*() const { return *ptr.get(); }
T* operator->() const { return ptr.get(); }
private:
std::shared_ptr<T> ptr;
};

Теперь мы просто пишем произвольную проверку шаблона, которая говорит, что «это достаточно хорошо, чтобы удовлетворить меня».

И хранить restricted_shared_ptr< MyCheck, NodeView<K,T> const >, Нет способа сохранить тип в этом общем указателе, который не проходит MyCheck без неопределенного поведения.

Здесь нужно доверять MyCheckконструктор, чтобы делать то, что он говорит, что делает.

template<class T>
struct IsBlackNode:std::false_type{};

template<class K, class V, std::size_t Height, class Left, class Right>
struct IsBlackNode< BlackNode<K, V, Height, Left, Right> >:std::true_type{};

это требование, которое только BlackNodeс может пройти.

Так что restricted_shared_ptr< IsBlackNode, NodeView<K, T> const > является общим указателем на то, что проходит IsBlackNode тестовое задание а также реализует NodeView<K,T> интерфейс.

4

Другие решения

Заметка

Ответ Якка более идиоматичен для C ++ — этот ответ показывает, как написать (или, по крайней мере, начать писать) что-то более похожее на версию на Haskell.

Когда вы увидите, сколько C ++ требуется для эмуляции Haskell, вы можете вместо этого использовать нативные идиомы.

ТЛ; др

большинство ваших инвариантов (и свойств) Haskell на самом деле вообще не выражаются статически в типе, а в (времени выполнения) кода различных конструкторов. Система типов помогает, гарантируя, что один из этих конструкторов действительно работал, и отслеживая, какой он был для диспетчеризации сопоставления с образцом.


IIUC, твой Хаскель Node Тип не имеет четырех параметров типа, но два:

data Node (color :: Color) (height :: Nat) key value where

исправляет типы цвета и высоты — только ключевые и значения типов не определены. Все четыре являются записями, но две из этих записей имеют фиксированные типы.

Итак, ближайший простой перевод будет

template <typename Key, typename Value>
struct Node {
Color color_;
size_t height_;
Key key_;
Value val_;
};

Сложность в том, что нет прямой поддержки ваших разных конструкторов — это информация о времени выполнения, которую Haskell отслеживает для вас.

Итак, Leaf это Node, чей конструктор инициализировал поля цвета и высоты для вас, но используемый конструктор также отслеживается как часть созданного объекта.

Ближайшим эквивалентом этого и сопоставлением с шаблоном, который он дает, будет тип варианта, подобный Boost.Variant, давая вам что-то вроде:

// underlying record storage
template <typename Key, typename Value>
struct NodeBase {
Color color_;
size_t height_;
Key key_;
Value val_;
};

template <typename Key, typename Value>
struct Leaf: public NodeBase<Key,Value> {
Leaf(Key k, Value v) : NodeBase{Color::Black, 0, k, v} {}
// default other ctors here
};
// similarly for your BlackBranch and RedBranch constructors

template <typename Key, typename Value>
using Node = boost::variant<Leaf<Key,Value>,
RedBranch<Key,Value>,
BlackBranch<Key,Value>>;

еще раз обратите внимание, что у вашего типа Branch есть записи для leftColor, rightColor, childHeight, и что только ключ и значение создают параметры типа.

Наконец, когда вы будете использовать сопоставление с образцом для написания функции на ваших различных конструкторах Node в Haskell, вы будете использовать

template <typename Key, typename Value>
struct myNodeFunction {
void operator() (Leaf &l) {
// use l.key_, l.value_, confirm l.height_==0, etc.
}
void operator() (RedBranch &rb) {
// use rb.key_, rb.value_, confirm rb.color_==Color::Red, etc.
}
void operator() (BlackBranch &bb) {
// you get the idea
}
};

и примените это как:

boost::apply_visitor(myNodeFunction<K,V>(), myNode);

или, если вы часто используете этот шаблон, вы можете обернуть его как

template <typename Key, typename Value,
template Visitor<typename,typename> >
void apply(Node<Key,Value> &node)
{
boost::apply_visitor(Visitor<Key,Value>{}, node);
}
4

По вопросам рекламы [email protected]