web-dev-qa-db-ja.com

ユーザー定義の演算子が一般的でないのはなぜですか?

関数型言語に欠けている機能の1つは、演算子は単なる関数であるという考えです。そのため、カスタム演算子の追加は、関数の追加と同じくらい簡単です。多くの手続き型言語では演算子のオーバーロードが許可されているため、一部の意味では演算子は引き続き関数です(これは [〜#〜] d [〜#〜] で非常に当てはまります)演算子はテンプレートの文字列として渡されますパラメータ)。

オペレーターのオーバーロードが許可されている場合、カスタムオペレーターを追加することは簡単です。私は このブログ投稿 を見つけました。これは、優先規則のためにカスタム演算子が中置記法でうまく機能しないことを主張していますが、著者はこの問題に対していくつかの解決策を示しています。

私は周りを見回しましたが、その言語でカスタム演算子をサポートする手続き型言語を見つけることができませんでした。ハック(C++のマクロなど)はありますが、言語サポートとほとんど同じではありません。

この機能を実装するのはかなり簡単なので、なぜもっと一般的ではないのですか?

見苦しいコードにつながる可能性があることは理解していますが、これにより、過去の言語設計者が、悪用されやすい便利な機能(マクロ、3項演算子、安全でないポインタ)を追加できなくなったことはありません。

実際の使用例:

  • 欠落している演算子を実装します(たとえば、Luaにはビット単位の演算子がありません)
  • Dを模倣する~(配列連結)
  • DSL
  • 使用する | Unixパイプスタイルの構文糖(コルーチン/ジェネレータを使用)

doがカスタム演算子を許可する言語にも興味がありますが、why除外されています。ユーザー定義の演算子を追加するためにスクリプト言語をフォークすることを考えましたが、どこにも見たことがないことに気づいたときは自分でやめました。そのため、言語デザイナーが私より賢いので、それを許可しなかったのには十分な理由があります。

98
beatgammit

プログラミング言語の設計には、正反対の2つの考え方があります。 1つは、プログラマーがより少ない制限でより優れたコードを書くことであり、もう1つは、より多くの制限を備えたより良いコードを書くことです。私の意見では、優れた経験豊富なプログラマーは少ない制限で繁栄するというのが現実ですが、その制限は初心者のコード品質に利益をもたらす可能性があります。

ユーザー定義のオペレーターは、経験豊富な手で非常に洗練されたコードを作成でき、初心者はまったくひどいコードを作成できます。したがって、言語にそれらが含まれているかどうかは、言語デザイナーの考え方に依存します。

136
Karl Bielefeldt

〜または "myArray.Concat(secondArray)"を使用して配列を連結するかどうかを選択できるので、おそらく後者を使用します。どうして? 〜は、それが書かれた特定のプロジェクトで指定された、配列の連結という意味を持つ、完全に無意味な文字だからです。

基本的に、あなたが言ったように、演算子はメソッドと違いはありません。しかし、メソッドには、コードフローの理解を深めるために、読みやすくてわかりやすい名前を付けることができますが、演算子は不透明で状況に応じたものです。

これが、PHPの.演算子(文字列連結)やHaskellやOCamlのほとんどの演算子も好きではない理由です。

83

この機能を実装するのはかなり簡単なので、なぜもっと一般的ではないのですか?

あなたの前提は間違っています。 ではありません「実装するのはかなり簡単」ではありません。実際、それは問題の袋をもたらします。

投稿で提案されている「解決策」を見てみましょう。

  • 優先順位なし。著者自身は、「優先ルールを使用しないことは単に選択肢ではない」と述べています。
  • セマンティック対応の解析。記事が言うように、これはコンパイラーが多くの意味論的知識を持っていることを必要とします。この記事は実際にはこれに対する解決策を提供していないので、これは簡単なことではありません。コンパイラーは、電力と複雑さの間のトレードオフとして設計されています。特に、著者は関連情報を収集するための事前解析ステップについて言及していますが、事前解析は非効率的であり、コンパイラーは解析パスを最小限に抑えるように非常に努力しています。
  • カスタム中置演算子なし。まあ、それは解決策ではありません。
  • ハイブリッド解。このソリューションは、セマンティック対応の解析の多く(すべてではない)の欠点を持っています。特に、コンパイラは不明なトークンをカスタム演算子を表す可能性があるものとして扱う必要があるため、意味のあるエラーメッセージを生成できないことがよくあります。また、解析を続行するには(タイプ情報などを収集するために)上記のオペレーターの定義が必要になる場合があり、もう一度追加の解析パスが必要になります。

全体として、これはパーサーの複雑さの点でもパフォーマンスの点でも実装するのにコストがかかる機能であり、多くの利点をもたらすかどうかは明らかではありません。確かに、新しい演算子を定義する機能にはいくらかメリットがありますが、それでも議論の余地があります(新しい演算子を使用するのはよくないことを主張する他の回答をご覧ください)。

71
Konrad Rudolph

とりあえず、「オペレーターが読みやすさに害を及ぼすために悪用される」という議論全体を無視し、言語設計の影響に焦点を当てましょう。

中置演算子には、単純な優先規則よりも多くの問題があります(率直に言っても、参照するリンクはその設計上の決定の影響を軽視します)。 1つは競合解決です。a.operator+(b)b.operator+(a)を定義するとどうなりますか?どちらか一方を優先すると、その演算子の予想される可換性が損なわれます。エラーをスローすると、正常に機能しないモジュールが1つにまとめて破損する可能性があります。派生型をミックスに投入し始めるとどうなりますか?

問題は、演算子は単なる関数ではないということです。関数はスタンドアロンであるか、そのクラスによって所有されています。これにより、どのパラメータ(存在する場合)が多態性ディスパッチを所有するかが明確に設定されます。

そして、それはオペレーターから生じる様々なパッケージングと解決の問題を無視します。言語設計者が(概して)中置演算子の定義を制限する理由は、議論の余地のある利点を提供しながら、言語の問題の山を作成するためです。

そして率直に言って、それらはnotを実装するのが簡単であるためです。

25
Telastyn

演算子のオーバーロード実装されている がどのくらいの頻度であるかは驚くでしょう。しかし、それらは多くのコミュニティで一般的に使用されていません。

〜を使用して配列に連結するのはなぜですか?どうして <を使うRubyする を使うのか)==あなたが扱うプログラマーはおそらくRubyプログラマーではない。あるいはDプログラマーだ。彼らがあなたのコードに遭遇したとき、彼らは何をしますか?彼らは行き、シンボルの意味を調べなければなりません。

私はかつて、関数型言語にも精通している非常に優れたC#開発者と一緒に働いていました。突然、彼は拡張メソッドと標準のモナド用語を使用してC#に monads を導入し始めました。コードの意味がわかったら、彼のコードの一部が簡潔で読みやすくなることについて誰も異議を唱えることはできませんでしたが、誰もがコードを理解する前にモナドの用語を学ぶ必要があったことを意味します

十分公正だと思いますか?ほんの小さなチームでした。個人的には同意しません。すべての新しい開発者は、この用語に混乱する運命にありました。新しいドメインを学習するのに十分な問題はありませんか?

一方、私は the ?? operator C#では他のC#開発者にそれが何であるかを知っていると期待していますが、デフォルトでサポートしていない言語にそれをオーバーロードしません。

19
pdr

私はいくつかの理由を考えることができます:

  • それら実装するのは簡単ではありません-特にユーザー定義の優先順位、固定性、アリティルールを許可する場合、任意のカスタムオペレーターを許可すると、コンパイラーがより複雑になる可能性があります。単純さが美徳である場合、演算子のオーバーロードは優れた言語設計からあなたを連れ去っています。
  • それらは乱用されます-演算子を再定義し、あらゆる種類のカスタムクラス用にそれらを再定義し始めることが「クール」であると考えるコーダーによって主に行われます。やがて、オペレーターは従来のよく理解された規則に従っていないため、コードはカスタマイズされたシンボルでいっぱいになり、他の誰も読むことも理解することもできません。あなたのDSLがたまたま数学のサブセットでない限り、私は「DSL」の議論を買いません:-)
  • それら可読性と保守性を損なう-オペレーターが定期的にオーバーライドされている場合、この機能が使用されているときにそれを見つけるのが難しくなる可能性があり、プログラマーはオペレーターが何をしているのか継続的に自問する必要があります。意味のある関数名を付ける方がずっと良いです。いくつかの余分な文字を入力することは安価であり、長期的なメンテナンスの問題は高価です。
  • 彼らは暗黙のパフォーマンス期待を破るできます。たとえば、私は通常、配列内の要素のルックアップがO(1)であることを期待します。ただし、演​​算子のオーバーロードを使用すると、インデックス演算子の実装によっては、_someobject[i]_がO(n)操作になる可能性があります。

実際には、通常の関数を使用する場合と比較して、演算子のオーバーロードが正当な用途を持っているケースはほとんどありません。正当な例としては、数学者が複素数に対して定義されているよく知られている方法を理解している数学者が使用する複素数クラスの設計があります。しかし、これはそれほど一般的なケースではありません。

考慮すべきいくつかの興味深いケース:

  • Lisps:一般に、演算子と関数をまったく区別しません-_+_は通常の関数です。演算子を含め、好きなように関数を定義できます(通常、組み込み_+_との競合を回避するために、別々の名前空間で関数を定義する方法があります)。しかし、意味のある関数名を使用する文化的な傾向があるので、これはあまり乱用されません。また、LISPでは、プレフィックス表記は独占的に使用される傾向があるため、演算子のオーバーロードが提供する「構文上の砂糖」にはあまり価値がありません。
  • Java-演算子のオーバーロードを許可しません。これは時々煩わしいです(複素数の場合など)が、平均してJavaは、単純で汎用的なOOP langauge。Javaコードは、この単純さの結果として、中低レベルの開発者が実際に保守するのは非常に簡単です。
  • C++には、非常に高度な演算子のオーバーロードがあります。時々これは悪用されます(_cout << "Hello World!"_誰ですか?)しかし、このアプローチは、C++が高度なプログラミングを可能にすると同時にパフォーマンスのために金属に非常に近づくことができる複雑な言語としての位置付けを考えると理にかなっています。パフォーマンスを犠牲にすることなく、希望どおりに動作するComplex数クラスを記述します。自分を足で撃つことはあなた自身の責任であると理解されています。
11
mikera

この機能を実装するのはかなり簡単なので、なぜもっと一般的ではないのですか?

それは実装するのは簡単ではありません(簡単に実装しない限り)。また、理想的に実装されたとしても、あまり効果がありません。簡潔さによる可読性の向上は、不慣れや不透明性による可読性の低下によって相殺されます。要するに、それは通常開発者やユーザーの時間に値するものではないため、一般的ではありません。

とは言え、私はそれを行う3つの言語を考えることができ、それらはさまざまな方法で行います。

  • S-expression-yがすべてではない場合のスキームであるラケットは、拡張する構文のパーサーに相当するものを記述することを許可および期待します(これを扱いやすくするための便利なフックを提供します)。
  • 純粋に関数型のプログラミング言語であるHaskellを使用すると、句読点のみで構成される演算子を定義でき、固定レベル(10が利用可能)と結合性を提供できます。三項演算子などは、二項演算子と高次関数から作成できます。
  • Agdaは依存型プログラミング言語であり、演算子(paper here )を使用して非常に柔軟であり、if-thenとif-then-elseの両方を同じプログラムの演算子として定義できますが、そのレクサー、パーサー、および評価者はすべて結果として強く結合されています。
8
Alex R

カスタムオペレーターが推奨されない主な理由の1つは、どのオペレーターも何でもできる/できるということです。

たとえば、cstreamの左シフトのオーバーロードは非常に批判的です。

言語がオペレーターのオーバーロードを許可している場合、混乱を避けるために、オペレーターの動作を基本の動作と同様に保つことが一般的に推奨されます。

また、ユーザー定義演算子を使用すると、特にカスタム設定ルールもある場合に、解析がはるかに困難になります。

7
ratchet freak

ユーザー定義の単語を使用しないのと同じ理由で、ユーザー定義の演算子は使用しません。だれもその関数を「sworp」とは呼びません。あなたの考えを他の人に伝える唯一の方法は、共有言語を使用することです。そして、それはあなたがあなたのコードを書いている社会にとって言葉と記号(演算子)の両方が知られていなければならないことを意味します。

したがって、プログラミング言語で使用されている演算子は、私たちが学校で教えてきた演算子(算術)、またはブール演算子など、プログラミングコミュニティで確立された演算子です。

4
Vagif Verdi

このようなオーバーロードをサポートする言語については、Scalaは、実際にはよりクリーンでより良い方法でC++を実行できます。ほとんどの文字を関数名で使用できるため、!+ *のような演算子を定義できます。 = ++必要に応じて。インフィックスの組み込みサポートがあります(1つの引数を取るすべての関数に対して)。このような関数の結合性も定義できます。ただし、優先順位を定義することはできません(醜いトリックは、 ここ を参照してください。

4
kutschkem

まだ言及されていないことの1つは、Smalltalkの場合で、すべて(オペレーターを含む)がメッセージ送信です。 _+_、_|_などの「演算子」は実際には単項メソッドです。

すべてのメソッドをオーバーライドできるため、_a + b_は、abが両方とも整数の場合は整数の加算を意味し、両方がOrderedCollectionsの場合はベクトルの加算を意味します。

これらは単なるメソッド呼び出しであるため、優先順位の規則はありません。これは、標準の数学的表記法に重要な意味を持っています。_3 + 4 * 5_は、3 + (4 * 5)ではなく_(3 + 4) * 5_を意味します。

(これは、Smalltalk初心者にとって大きな障害です。数学の規則に違反すると、特殊なケースが削除されるため、すべてのコード評価が左から右に均一に進行し、言語がはるかに単純になります。)

4
Frank Shearar

ここで2つのことと戦っています。

  1. そもそもなぜ演算子が言語で存在するのですか?
  2. 関数/メソッドに対する演算子の利点は何ですか?

ほとんどの言語では、演算子は実際には単純な関数として実装されていません。それらには関数の足場が含まれている可能性がありますが、コンパイラー/ランタイムはそれらのセマンティックな意味とそれらを効率的にマシンコードに変換する方法を明確に認識しています。これは、組み込み関数と比較してもはるかに当てはまります(そのため、ほとんどの実装では、実装にすべての関数呼び出しオーバーヘッドが含まれていません)。ほとんどの演算子は、CPUにあるプリミティブ命令の上位レベルの抽象化です(そのため、ほとんどの演算子は算術、ブール、またはビット単位です。それらを「特別な」関数(「プリミティブ」、「ビルトイン」、「ネイティブ」などと呼びます)としてモデル化できますが、これを行うには、そのような特別な関数を定義するための非常に堅牢なセマンティクスのセットが一般的に必要です。別の方法として、意味的にはユーザー定義の演算子のように見えますが、それ以外の場合はコンパイラー内の特別なパスを呼び出す組み込み演算子を使用できます。それは2番目の質問の答えに反します...

上で述べた機械翻訳の問題は別として、構文レベルでは、演算子は関数と実際には違いません。これらの特徴は、簡潔で象徴的であるという特徴がある傾向があり、有用である必要がある重要な追加の特徴を示唆しています。開発者にとって広く理解された意味/セマンティクスを持っている必要があります。短いシンボルは、すでに理解されているセマンティクスのセットの省略形でない限り、あまり意味を伝えません。そのため、ユーザー定義の演算子は本質的に役に立たなくなります。その性質上、演算子はあまり広く理解されていないためです。それらは、1文字または2文字の関数名と同じくらい意味があります。

C++のオペレーターオーバーロードは、これを調べるための肥沃な土台を提供します。ほとんどの演算子のオーバーロード「乱用」は、広く理解されているセマンティックコントラクトの一部を破壊するオーバーロードの形で発生します(典型的な例は、a + b!= b + aなどのoperator +のオーバーロード、または+がそのいずれかを変更する場合ですオペランド)。

演算子のオーバーロードandユーザー定義演算子を許可するSmalltalkを見ると、言語がどのようにそれを実行するのか、そしてそれがどれほど有用であるかがわかります。 Smalltalkでは、演算子は構文プロパティが異なるメソッドにすぎません(つまり、演算子はインフィックスバイナリとしてエンコードされます)。この言語は、特別な加速演算子とメソッドに「プリミティブメソッド」を使用します。ユーザー定義のオペレーターが作成されたとしてもごくわずかであり、作成された場合、作成者がおそらく使用することを意図したほどには使用されない傾向があります。新しい関数をメソッドではなく演算子として定義するのはほとんど正味の損失であり、後者は関数のセマンティクスの表現を可能にするため、演算子のオーバーロードに相当することもまれです。

3

C++のオペレーターオーバーロードは、単一の開発者チームにとって便利なショートカットであることが常にわかりましたが、これは、メソッド呼び出しが簡単ではない方法で "非表示"にされているために、長期的にはあらゆる種類の混乱を引き起こしますdoxygenのようなツールがばらばらになり、イディオムを適切に使用するために人々はそれらを理解する必要があります。

想像するよりも、意味を理解するのが難しい場合もあります。むかしむかし、大規模なクロスプラットフォームC++プロジェクトで、FilePathオブジェクト(JavaのFileと同様)を作成してパスが構築される方法を正規化することをお勧めしますオブジェクト)、それはその上に別のパス部分を連結するためにoperator /を使用することになります(つまり、File::getHomeDir()/"foo"/"bar"のようなことを行うことができ、サポートされているすべてのプラットフォームで正しく動作します)。それを見た人は誰でも、基本的には「一体何なんだ?弦の分割は……ああ、それはかわいいけど、正しいことをするとは信じない」と言うでしょう。

同様に、グラフィックスプログラミングやその他の領域では、Matrix * Matrix、Vector * Vector(ドット)、Vector%Vector(クロス)、Matrix * Vector(マトリックス変換)、Matrix ^ Vector(均質な座標を無視する特殊なケースのマトリックス変換-表面法線に役立ちます)などですが、ベクトル数学ライブラリを作成した人の解析時間を少し節約しますが、終了するだけです他の人のために問題をさらに混乱させる。それだけの価値はありません。

1
fluffy

演算子のオーバーロードは、メソッドのオーバーロードが悪い考えと同じ理由で悪い考えです。画面上の同じシンボルは、その周りにあるものに応じて異なる意味を持ちます。これにより、カジュアルな読書が難しくなります。

可読性は保守性の重要な側面であるため、常に過負荷を回避する必要があります(非常に特殊な場合を除きます)。各記号(演算子または英数字の識別子)は、それ自体に基づく固有の意味を持つ方がはるかに優れています。

例として:知らないコードを読んでいるときに、知らない新しいalphanum識別子に遭遇した場合、少なくとも知らないことがわかっているという利点があります。その後、それを調べに行くことができます。ただし、意味がわかっている共通の識別子または演算子が表示された場合、完全に異なる意味を持つために実際に過負荷になっていることに気付く可能性ははるかに低くなります。 (オーバーロードが広く使用されているコードベースで)どの演算子がオーバーロードされているかを知るには、コードのごく一部だけを読みたい場合でも、コード全体の実用的な知識が必要です。これは、新しい開発者をそのコードに追いつくのを難しくし、小さな仕事のために人々を連れてくることを不可能にします。これはプログラマーの仕事のセキュリティにとっては良いかもしれませんが、コードベースの成功に責任があるなら、絶対にこのやり方を避けるべきです。

演算子はサイズが小さいため、演算子をオーバーロードするとコードの密度を高めることができますが、コードを高密度にすることは実際の利点ではありません。ロジックが2倍の行を読み取るには、2倍の時間がかかります。コンパイラは気にしません。唯一の問題は人間の可読性です。コードをコンパクトにしても読みやすさは向上しないため、コンパクトにすることによる実質的なメリットはありません。先に進み、スペースを取り、一意の操作に一意の識別子を与えれば、コードは長期的にはより成功します。

0
AgilePro