ウィキペディアで説明されているように、私は Shunting-yardアルゴリズム を使用しています。
演算子を処理するときのアルゴリズムの説明は次のとおりです。
トークンが演算子o1の場合:
演算子スタックの最上位に演算子トークンo2があり、
o1 is left-associative and its precedence is less than or equal to that of o2, or o1 is right associative, and has precedence less than that of o2,
次に、演算子スタックから出力キューにo2をポップします。
O1をオペレータースタックにプッシュします。
ただし、次の例を示します。
入力:
sin max 2 3 / 3 * 3.1415
アルゴリズムが/
トークンに到達すると、次のようになります。
Token | Action | Output (in RPN) | Operator Stack
...
/ | Pop token to output | 2 3 max | / sin
...
彼らは関数トークンmax
をstack
からポップしてqueue
に入れています。彼らのアルゴリズムによれば、これは関数トークンが両方とも演算子であり、/
演算子よりも優先順位が低いことを意味しているようです。
これが事実であるかどうかについての説明はありません。では、Shunting-yard
アルゴリズムの場合、関数の優先順位は何ですか?機能は右または左に関連付けられていますか?それともウィキペディアは不完全/不正確なだけですか?
直接的な答えは、関数は演算子ではないということです。リンクしたページから:
トークンが関数トークンの場合、それをスタックにプッシュします。
関数のケース(接頭辞から接尾辞へ)は演算子のケース(接頭辞から接尾辞へ)よりもはるかに単純であるため、これは言う必要があるすべてです。
フォローアップの質問の場合:優先順位と結合性の概念が必要なのは、複数の中置演算子を持つ式の継承のあいまいさのためです。関数トークンは既にプレフィックス表記を使用しているため、問題はありません。最初にsin
を評価する必要があることを理解するために、max
またはmax
が「優先順位が高い」かどうかを知る必要はありません。トークンの順序からはすでに明らかです。これが、コンピューターが最初に前置/後置記法を好む理由であり、中置を前置/後置に変換するためのこのアルゴリズムがあるのはこのためです。
括弧が存在しない場合に関数の引数がどこで開始および終了するかについて、何らかのルールが必要です。そのため、関数は演算子よりも「優先される」、またはその逆であると言えます。しかし、中置演算子とは異なり、すべての関数に対して単一の一貫した規則があれば、それらの構成を完全に明確にすることができます。
言語構文に応じて、考慮すべき2つの異なるケースがあります。言語が括弧を使用して関数の適用を示す場合(例:f(2+1)
)、優先順位は関係ありません。関数はスタックにプッシュされ、後にポップアウトされます(上記の例では、結果は2 1 + f
です)。または、関数を値として扱い、すぐに出力し、右括弧の後に関数呼び出し操作を出力することもできます(それ以外の場合は、他の括弧と同じように処理する必要があります)。たとえば、f 2 1 + $
、ここで$
は関数呼び出し操作です。
ただし、言語が括弧を使用して関数の呼び出しを示していない場合は、Wikipediaの例のように、特別な句読点なしで関数の直後に引数を配置します(例:f 2 + 1
)。これは、もう少し複雑。ここで例として示した式はあいまいです:is fが2と1に結果に追加されるか、2と1を一緒に追加してからfを呼び出すか=結果とは?
この場合も、2つの方法があります。関数に遭遇したときに演算子スタックにプッシュするだけで、必要な優先順位を割り当てることができます。これは最も単純なアプローチであり、引用された例が明らかに行ったことです。ただし、実際的な問題があります。まず、関数をどのように特定しますか?有限のセットがある場合は簡単ですが、ユーザー定義関数がある場合、これはパーサーが環境にフィードバックしすぎる必要があることを意味します。また、複数の引数を持つ関数をどのように処理しますか?
この構文のスタイルでは、関数アプリケーションの演算子で便利な値として関数を使用する方がはるかに理にかなっていると感じています。次に、値を読み取るたびにアプリケーションオペレーターを挿入するだけで、最後に読み取ったものが値でもあったため、どの識別子が関数であるかを通知する特別な方法は必要ありません。関数を返す式を操作することもできます(操作としての関数スタイルでは困難または不可能)。これは、カリーを使用して複数の引数関数を処理できることを意味します。これは、それらを直接処理することを大幅に簡略化したものです。
次に決定する必要があるのは、関数適用の優先順位だけです。選択はあなた次第ですが、このように機能する私が使用したすべての言語で、これはその言語で最も強力な結合演算子であり、正しい連想演算子でした。 (唯一の興味深いバリエーションはHaskellであり、強い結合バージョンが記述されているだけでなく、言語の中で最も弱い結合演算子である記号$
と同じ意味であり、f 2 + 1
はfを2に適用し、f $ 2 + 1
は残りの式全体に適用します)
Dijkstraの元の考え(ALGOL 60コンパイラペーパーの7〜11ページ https://ir.cwi.nl/pub/9251 )を読んだ後、要求された「シャントヤードの機能」を実装しました。堅牢なソリューションが必要なため、次のことを行いました。
解析:
後置から後置(回避ヤード):
堅牢なテストや複雑なシナリオで完全に機能します。私のアプリケーション(コマンドライン引数を含む式エキスパンダー)では、複数引数関数とコンマ "、"トークンをサポートしてそれらを分離し、これらがプロセス全体を流れます。
例は "sqrt(3 ^ 2 + 4 ^ 2)"のようになり、これは "3 2 ^ 4 2 ^ + sqrt"になり、最終的に "5"はプログラムが引数と見なします。 bignumなので、 "" binomial(64、32)/ gcd(binomial(64、32)、binomial(63、31))) "==> big things ==>" 2 "が役に立ちます。" 123456 ^ 789 "は40,173桁で、タイミングは私のMacBookProで「評価= 0.000390秒」と非常に速く表示されます。
また、これを使用してテーブルのデータを展開し、それが便利だと思います。とにかく、これは、関数呼び出し、複数の引数、およびダイクストラのシャントヤードコンテキストでの深い入れ子を慎重に処理するための私のヒントです。今日は独立した考えからそれをしました。より良い方法があるかどうかわからない。