スタックの実装...呼び出しスタック

あるアイデアが思い浮かびました。コールスタックとデータ構造としてのスタックがあります。 呼び出しスタックを使用してデータを保存することはできますか?

少し考えて、それが可能であることに気付きましたが、制限があります:まず、コールバックを使用して値を処理する必要があります(結局、値がスタック上にある間、保存した呼び出しを残すことはできません)。 第二に、アクセスは厳密にLIFOです。

実装-アンダーカット。



言語として、私はNemerle-機能的なスタイルの使い慣れた.NET +利便性を使用することにしました。



実装自体は外見的に簡単です。

variant StackAction[T]{ | Push{ value : T; next : void -> StackAction[T]; } | Pop { next : T -> StackAction[T]; } } module StackStack{ public Enter[T](current : StackAction[T].Push) : StackAction[T]{ def processNext(next : StackAction[T]): StackAction[T]{ match(next){ | push is StackAction[T].Push => processNext(Enter(push)); | pop is StackAction[T].Pop => pop.next(current.value); } } processNext(current.next()); } }
      
      







Nemerle構文に不慣れな人向け:

バリアントStackAction [T]は、変数がプッシュまたはポップのいずれかを取ることができるバリアントジェネリック型を記述します。 さらに、値がプッシュの場合、値フィールド(スタックに置く実際の値)があり、次は次のStackActionを返すパラメーターの空のリストを持つコールバックです。 また、Popの場合は、スタックの先頭から値を引数として受け取り、次のStackActionを返すコールバックが必要です。



Enter関数は、スタックを操作するロジックそのものを実装します。 その内部で、ローカルprocessNext関数が宣言され、クロージャーとしてcurrentを受け取ります。 processNextの本体は、1つの一致ブロックで構成されます(パターンマッチング)。 pushとpopはnextの同義語であり、実際にどの型が値をとるかによって異なります。



その結果、Enter:操作のロジックは、current.nextを呼び出し、戻り値をprocessNextに渡し、戻り値をprocessNextから返します。 (nemerleにはreturnステートメントがなく 、関数は最後の式から値を返し、実行されたブランチから一致します)

processNextは渡された値をチェックします。 プッシュの場合、この値でEnterを呼び出し、Enterの結果で自分自身を呼び出します。 したがって、ループが取得されます-コールバックがPopを返すまで、Enterの現在の呼び出しは終了しません。 nextの値がPopの場合、current.valueの現在の値がクロージャーからコールバックに渡されます(processNext自体はすでに何度も再帰的に呼び出されます)。



その結果、別の欠点があります。Popがスタックから最後の値を取得し、スタックが空の場合、クライアントコードで呼び出されたEnterは、最後のPopが返したものを返します。 したがって、スタックの低い値で作業するには、別のサイクルを実行する必要があります。



使用例として、 逆ポーランド記法で式を計算します。



 def items = "7 2 3 * -".Split(array[' ']); mutable currentPosition = 0; def processNextToken(){ def action(operation : double * double -> double){ StackAction.Pop(fun(y){ StackAction.Pop(fun(x){ StackAction.Push(operation(x, y), processNextToken); }); }); } if(currentPosition >= items.Length){ StackAction.Pop(fun(x){ StackAction.Push(x, fun(){throw Exception("Bad input, not enough operations.")}); }); }else{ currentPosition++; mutable value : double; match(items[currentPosition-1]){ | strVal when (Double.TryParse(strVal, out value)) => StackAction.Push(value, processNextToken); | "+" => action(_ + _); | "-" => action(_ - _); | "/" => action(_ / _); | "*" => action(_ * _); | token => throw Exception($"bad token $token"); } } } def calc(current : StackAction[double]){ match(current){ | StackAction.Push (_, next) when (next == processNextToken) => calc(StackStack.Enter(current :> StackAction[double].Push)); | StackAction.Push (res, _) => WriteLine($"Result = $res"); | StackAction.Pop => throw Exception("Bad input, not enough arguments."); } } calc(processNextToken());
      
      







最後から簡単な説明:

calcは、スタックの一番下の要素を操作するロジックを実装します。プッシュがprocessNextTokenコールバックで失敗した場合、プッシュが別のコールバック(fun(){throw Exception( "Bad input、not enough operations。"))で失敗した場合、レコード全体が処理されますそして、関数は最終結果を返しました。 ポップが脱落した場合、最後のアクションには十分な引数がありませんでした。



processNextTokenは、トークンを順番に処理します。 レコードの最後に達した場合、スタックから最後の値を取得し、calcに返します。 スタックに複数の値がある場合、匿名関数fun(){例外をスロー(「不正な入力、十分な操作がありません。」)}が呼び出されます。 トークンがさらにある場合は、現在のトークンを取得します。 アクションと呼ばれるアクションのために、数値リテラルをスタックに配置します。 レコード_ + _-特別なマジックネメル-部分実行。 この場合、算術演算子を2つの引数を持つ関数に変換します。



actionはスタックから2つの値を受け取り、渡された関数を実行して、結果をスタックに配置します。



かなり混乱しますか? 値を保存するスタックを別のスレッドに転送すると、通常のスタックインターフェイスでクラスを作成できます。



 enum Action{ | Push | Pop } public class StackEmptyException : Exception{ public this(message : string){ base(message); } } public class ThreadStack[T] : IDisposable{ class Resident{ public mutable volatile action : Action; public mutable volatile value : T; public mutable volatile exception : Exception; public syncIn = AutoResetEvent(false); public syncOut = AutoResetEvent(false); public start() : void{ exception = null; try{ mutable current = next(); while(true){ match(current){ | act is StackAction[T].Push => current = StackStack.Enter(act : StackAction[T].Push); | StackAction.Pop => throw StackEmptyException("Stack is empty"); } } }catch{ | e => {exception = e; _ = syncOut.Set();} } } next() : StackAction[T]{ _ = syncOut.Set(); _ = syncIn.WaitOne(); match(action){ | Push => StackAction.Push(value, next); | Pop => StackAction.Pop(fun(x){ value = x; next();}); } } } private resident : Resident = Resident(); private thread : Thread; public this(){ thread = Thread(ThreadStart(resident.start)); thread.Start(); } public Dispose() : void implements IDisposable.Dispose { try{ thread.Abort(); _ = resident.syncIn.Set(); thread.Join(); (resident.syncIn : IDisposable).Dispose(); (resident.syncOut : IDisposable).Dispose(); }finally{} } private checkException() : void{ when(resident.exception != null) { throw resident.exception; } } private exec() : void{ _ = resident.syncIn.Set(); _ = resident.syncOut.WaitOne(); checkException(); } public Push(val : T) : void{ resident.value = val; resident.action = Action.Push; exec(); } public Pop() : T{ resident.action = Action.Pop; exec(); resident.value; } }
      
      







さらに多くのコードがありますが、C#を知っている人にとってはすでに明確になっているはずです。 唯一のもの: "_ ="構文は、戻り値を無視することをコンパイラーに伝えます。



そして、これは同じ逆ポーランド記法です:

 def items = "7 2 3 * -".Split(array[' ']); mutable currentPosition = 0; mutable stack : ThreadStack[double]; try{ stack = ThreadStack(); mutable onStack = 0; def execOperation(operation : double * double -> double){ def y = stack.Pop(); def x = stack.Pop(); stack.Push(operation(x, y)); onStack--; } currentPosition = 0; while(currentPosition < items.Length){ currentPosition++; mutable value : double; match(items[currentPosition-1]){ | strVal when (Double.TryParse(strVal, out value)) => { onStack++; stack.Push(value);} | "+" => execOperation(_ + _); | "-" => execOperation(_ - _); | "/" => execOperation(_ / _); | "*" => execOperation(_ * _); | token => throw Exception($"bad token $token"); } } when(onStack > 1){ throw Exception("Bad input, not enough operations."); } WriteLine("Result: " + stack.Pop()); }catch{ | e is StackEmptyException => throw Exception("Bad input, not enough arguments."); }finally{ stack.Dispose(); }
      
      







そしてもちろん、この記事には結論が必要です。関数型言語のそのような歪みでさえ、非常に容量があり理解しやすいものです(もしそれらを扱うスキルがあれば)。 命令型のスタイルは冗長ですが、それでも準備のできていない読者にとっては明確です。



All Articles