C ++のレイジーリスト



Scalaには興味深いコレクション-Streamがあります。 コンテナ。最初のアクセス時に要素が計算される(そしてその後保存される)リストです:



クラスStreamは、必要な場合にのみ要素が評価される遅延リストを実装します。


C ++で似たようなものを実装したかった。



目的



標準のアルゴリズムで使用できるコンテナを取得して、アクセス時にその要素を生成できるようにします。



将来を見据えて、使用例を示します。
typedef lazy::list<int> list_type; list_type fibonacci(int n1, int n2) { int next = n1 + n2; std::cout << "fibonacci(" << n1 << ", " << n2 << ") -> " << n1 << std::endl; return list_type(n1, std::bind(&fibonacci, n2, next)); } int main() { list_type list(fibonacci(0, 1)); auto res3 = std::find_if(list.begin(), list.end(), [](int v){return v > 3;}); std::cout << "first number greater 3 is " << *res3 << std::endl; std::cout << std::endl; auto res10 = std::find_if(list.begin(), list.end(), [](int v){return v > 10;}); std::cout << "first number greater 10 is " << *res10 << std::endl; return 0; }
      
      





出力は次のようになります。



 fibonacci(0, 1) -> 0 fibonacci(1, 1) -> 1 fibonacci(1, 2) -> 1 fibonacci(2, 3) -> 2 fibonacci(3, 5) -> 3 fibonacci(5, 8) -> 5 first number greater 3 is 5 fibonacci(8, 13) -> 8 fibonacci(13, 21) -> 13 first number greater 10 is 13
      
      







例からわかるように、要素生成関数は各要素に対して1回だけ呼び出され、受け取った値はコンテナに保存され、再計算されません。



制限事項



次のようなことをしたいとします:



 auto iter = --list.end();
      
      





終了前の要素を取得するには、関数によって生成されたすべての要素をバイパスする必要があり、これは無限です(上記の例の場合)。 したがって、遅延リストの反復子は単方向-ForwardIteratorになります。 同様の状況は、リスト内の要素の数を取得するとき、および最後の要素(pop_back)を削除するときです。 したがって、コンテナにはこれらのメソッドはありません。



簡単にするために、私は任意の場所への挿入と任意の要素の削除を実装していません。 何らかの機能によって要素のシーケンスを生成でき、挿入/削除時にこのシーケンスに違反するという理由だけのためです。 同じ理由で、要素は変更できません。 しかし、これは慣例です。



どうしたの?



結果は、遅延リストを生成する要素とファンクターの両方を追加できるリストになり、そのリストに要素とファンクターを含めることができます。 最初の要素(pop_front)またはすべての要素(clear)を削除できます。 要素はリストの最初または最後に挿入されます。



リストイテレータは単方向であり、リストアイテムの変更は許可されません。



 template< typename T, typename Allocator = std::allocator<T> > class list;
      
      





リスト定義
 template< typename T, typename Allocator = std::allocator<T> > class list { public: typedef list<T, Allocator> self_type; typedef T value_type; typedef std::function<self_type ()> func_type; typedef __details_lazy_list::const_iterator<self_type> iterator; typedef __details_lazy_list::const_iterator<self_type> const_iterator; friend __details_lazy_list::const_iterator<self_type>; list(); list(const self_type& that); list(self_type&& that); template<typename ... Args> explicit list(Args&&... args) { push_others(std::forward<Args>(args)...); } void push_back(const value_type& value); void push_back(value_type&& value); void push_back(const func_type& func); void push_back(func_type&& func); void push_back(const self_type& that); void push_back(self_type&& that); void push_front(const value_type& value); void push_front(value_type&& value); void push_front(const func_type& func); void push_front(func_type&& func); void push_front(const self_type& that); void push_front(self_type&& that); void clear(); bool empty() const; const_iterator begin() const; const_iterator end() const; private: typedef std::list<value_type, Allocator> container_type; typedef typename container_type::iterator inner_iterator; typedef value_type const * funcs_map_key_type; typedef std::pair<funcs_map_key_type, func_type> funcs_map_value_type; typedef std::map<funcs_map_key_type, func_type> funcs_map_type; void forward(const_iterator& iter) const; void insert(inner_iterator pos, self_type&& that) const; template<typename Arg, typename ...Args> void push_others(Arg&& arg, Args&&... args) { push_back(std::forward<Arg>(arg)); push_others(std::forward<Args>(args)...); } template<typename Arg> void push_others(Arg&& arg) { push_back(std::forward<Arg>(arg)); } void push_others() {} mutable container_type _cont; mutable funcs_map_type _funcs; };
      
      







イテレータ定義
 template< typename lazy_list_type > class const_iterator: public std::iterator<std::input_iterator_tag, typename lazy_list_type::value_type> { friend lazy_list_type; public: typedef std::iterator<std::input_iterator_tag, typename lazy_list_type::value_type> base_type; const_iterator(); typename base_type::reference const operator* () const; typename base_type::pointer const operator-> () const; const_iterator& operator++(); const_iterator operator++(int); bool operator== (const const_iterator& that); bool operator!= (const const_iterator& that); private: typedef typename lazy_list_type::inner_iterator inner_iterator; const_iterator(const lazy_list_type* owner, inner_iterator iter); const lazy_list_type* _owner; inner_iterator _iter; };
      
      







記事の最後にリポジトリへのリンクがあり、ユニットテストで実装を取得できます。



テンプレートパラメータ。



T-保管アイテムのタイプ



アロケーター-要素を配置するために使用されるアロケーター。



内部タイプ



種類 説明
value_type T
self_type 独自のタイプ
func_type アイテムの生成に使用されるファンクターのタイプ。 ファンクターはself_typeオブジェクトを返します。
イテレータ 定数順反復子
const_iterator 定数順反復子


方法



方法 説明
push_front 先頭への挿入
push_back 最後に挿入
空っぽ コンテナが空かどうかを確認する
クリア すべてのアイテムを削除
pop_front 最初の要素を削除


push_frontおよびpush_backメソッドは、要素、保存された要素の値、またはself_type型の別のオブジェクトを生成するファンクターを受け入れます。



コンストラクター



署名 説明
list();



空のコンテナを作成します
template<typename ... Args> explicit list(Args&&... args)



コンテナを作成し、転送された要素をそれに入れます。

次のタイプの値を転送できます。

value_type





func_type





self_type





仕組み



内部では、2つの標準コンテナが使用されます。std::値を格納するリストとstd ::ファンクタを格納するマップです。 ファンクターは遅延リスト、つまり self_type。 これにより、必要に応じて一度に複数の要素を一度に計算でき、次に、次の値がない場合を心配する必要がありません-シーケンスが終了しました。この場合、空のコンテナを返すことができます。



新しい要素を追加すると、すべてが簡単になります-すぐに内部リストに追加されます。



ファンクターを追加するとき、追加された要素に関連付けられたファンクターがあるかどうかがチェックされます(push_back)。 ファンクターがない場合、転送されたファンクターがマップに追加され、前の要素へのポインターがキーとして使用されます。 最初、空のコンテナ、または既に関連付けられたファンクターがある要素の後に追加する場合、operator()ファンクタメソッドが単に呼び出され、結果のコンテナからの値が適切な場所(先頭または末尾)に挿入され、新しいファンクタがマップに追加されます。それらは返されたコンテナにあります。



リストに「値-ファンクター」のペアを格納することは可能ですが、作業の過程でファンクターの数は計算された要素の数よりもかなり少なくなり、ペアを格納する場合のメモリコストは高くなります。

繰り返しますが、ファンクターの数はそれほど多くないと想定しているため、mapを使用するかunordered_mapを使用するかに大きな違いはありません。 唯一のことは、マップを使用すると、メモリコストがわずかに少なくなるということです。



反復子がインクリメントされると、現在の要素のファンクターがチェックされます(チェックされている場合)。それによって返される値がコンテナーに追加され、ファンクターが削除されます。 その後、反復子は内部リストにインクリメントされます。 ファンクターが存在しないか、空のコンテナーを返した場合は、内部リストの次の要素に移動します。



なんでこんなこと?



Scalaの言語講義で発表されたWater Pouringタスクは、そのようなリストを実装するように促しました。 一番下の行は次のとおりです。一定の容積のグラスがいくつかあり、そこからグラスを満たすことができるタップ(グラスを完全に満たすことができます)、グラスの内容物を注ぐことができるシンクがあります。 あるグラスから別のグラスに水を入れ、空にし、注ぐには、グラスの1つに一定量の水を入れる必要があります。 解決策は、そのような状態を取得するための一連のアクションです。



たとえば、3リットルと5リットルのグラスが2枚あり、4リットルを取得したいとします。





各グラスの現在の水の量を条件として考慮します。 各状態から、可能な操作のいずれかを適用することにより、可能な状態の新しいセットを取得できます:fill、pour、pour。 状態の各セットから、新しいセットを取得できます。 循環しないように、受信した状態を個別に保存し、新しい状態のセットを受信すると破棄します。





状態の各セットで、目的の状態(目的の水位を持つグラス)があるかどうかを確認します。



現在の状態に影響を与えるための可能なオプションが必要です。 各水移動オプションは、imoveインターフェイスを継承します。



 class imove { public: virtual state operator()(const state& cur) const = 0; virtual std::unique_ptr<imove> clone() const = 0; virtual std::string to_string() const = 0; virtual ~imove() {} };
      
      





to_string



メソッドは、画面に情報を表示するためにto_string



必要です。



このタスクでは、次のタイプの移動が可能です。



グラスを埋める-塗りつぶし
 class fill: public imove { public: fill(unsigned glass, unsigned capacity); state operator()(const state& cur) const override; std::unique_ptr<imove> clone() const override; std::string to_string() const override; protected: const unsigned _glass; const unsigned _capacity; }; fill::fill(unsigned glass, unsigned capacity) : _glass(glass), _capacity(capacity) {} state fill::operator()(const state& cur) const { assert(cur.size() > _glass); state next(cur); next[_glass] = _capacity; return next; } std::unique_ptr<imove> fill::clone() const { return std::unique_ptr<imove>(new fill(_glass, _capacity)); } std::string fill::to_string() const { return "fill(" + std::to_string(_glass) + ")"; }
      
      







コップから水を注ぐ-空
 class empty: public fill { public: empty(unsigned glass); std::unique_ptr<imove> clone() const override; std::string to_string() const override; }; empty::empty(unsigned glass) : fill(glass, 0) {} std::unique_ptr<imove> empty::clone() const { return std::unique_ptr<imove>(new empty(_glass)); } std::string empty::to_string() const { return "empty(" + std::to_string(_glass) + ")"; }
      
      







あるグラスから別のグラスに注ぐ-注ぐ
 class pour: public imove { public: pour(unsigned from, unsigned to, unsigned capacity_to); state operator()(const state& cur) const override; std::unique_ptr<imove> clone() const override; std::string to_string() const override; protected: const unsigned _from; const unsigned _to; const unsigned _capacity_to; }; pour::pour(unsigned from, unsigned to, unsigned capacity_to) : _from(from), _to(to), _capacity_to(capacity_to) {} state pour::operator()(const state& cur) const { assert((cur.size() > _from) && (cur.size() > _to)); assert(_capacity_to >= cur[_to]); unsigned amount = std::min(cur[_from], _capacity_to - cur[_to]); state next(cur); next[_from] -= amount; next[_to] += amount; return next; } std::unique_ptr<imove> pour::clone() const { return std::unique_ptr<imove>(new pour(_from, _to, _capacity_to)); } std::string pour::to_string() const { return "pour(" + std::to_string(_from) + ", " + std::to_string(_to) + ")"; }
      
      







また、新しい状態に関する情報、つまり状態とそれに至る一連の動きを保存する必要があります。 path



クラスがこれを担当します。



 class path { public: path(const state& initial_state); path(const path& that); void extend(imove_ptr move); const state& end_state() const; std::string to_string() const; bool empty() const; private: std::list<imove_ptr> _history; state _end_state; };
      
      





そして実際には、遅延リストと上記のヘルパークラスを使用して解決策を見つけるクラス:



 typedef std::list<path> paths_list; class water_pouring { public: water_pouring(std::initializer_list<unsigned> capacities); path solve(unsigned target); private: typedef lazy::list<paths_list> list_of_paths_type; list_of_paths_type extend(const paths_list& paths); const std::vector<unsigned> _capacities; const std::vector<imove_ptr> _posible_moves; const state _initial; std::set<state> _explored_states; list_of_paths_type _paths; };
      
      





このクラスには2つのパブリックメソッドがあります-メガネの容量を取得するコンストラクターと、目的の状態を達成するためのパスを返すメソッドです。



プライベート拡張メソッドは、遅延リストアイテムを生成するために使用されます。



メガネの容量、可能な動きのセット、初期状態、すでに「見つかった」状態、および実際に怠withな状態のリストとそれらの受信履歴を保存します。



Create_moves関数は、可能な動きを取得するために使用されます
 std::vector<imove_ptr> create_moves(const std::vector<unsigned>& capacities) { std::vector<imove_ptr> moves; for (size_t i = 0; i < capacities.size(); ++i) { moves.emplace_back(new empty(i)); moves.emplace_back(new fill(i, capacities[i])); for (size_t j = 0; j < capacities.size(); ++j) { if (i != j) moves.emplace_back(new pour(i, j, capacities[j])); } } return moves; }
      
      







water_pouring::extend



メソッド:



 water_pouring::list_of_paths_type water_pouring::extend(const paths_list& paths) { paths_list next; for (auto& cur_path: paths) { for (auto move: _posible_moves) { state next_state = (*move)(cur_path.end_state()); if (_explored_states.find(next_state) == _explored_states.end()) { path new_path(cur_path); new_path.extend(move); next.push_back(new_path); _explored_states.insert(next_state); } } } if (next.empty()) return list_of_paths_type(); return list_of_paths_type(next, std::bind(&water_pouring::extend, this, next)); }
      
      





water_pouring::solve







 path water_pouring::solve(unsigned target) { paths_list::const_iterator solution; auto it = std::find_if( _paths.begin(), _paths.end(), [target, &solution](const paths_list& paths) -> bool { solution = std::find_if( paths.begin(), paths.end(), [target](const path& p) -> bool { auto it = std::find( p.end_state().begin(), p.end_state().end(), target); return it != p.end_state().end(); }); return solution != paths.end(); }); if (it != _paths.end()) return *solution; return path(state({0})); }
      
      





実際に、解決策を見つけるには、std :: find_if関数を使用し、述語として、必要な状態へのパスを調べるラムダ関数を使用します。 ラムダは、リンクを介してソリューションをキャプチャし、ソリューションが見つかった場合に反復子が指すソリューションのリストを通過しないようにします。



その結果、プログラムは次のソリューションを表示します。



 fill(1) pour(1, 0) empty(0) pour(1, 0) fill(1) pour(1, 0) --> (3, 4)
      
      





「怠lazな」リストが役立つ別のタスクを思い付くことができませんでした。 誰かがこのアイデアを気に入ってくれるといいのですが。



参照資料






All Articles