それは少し主観的ですが、オペレーターが鈍感で難しいオペレーターを明確にする要素をより明確に理解したいと思っています。私は最近言語設計を検討してきましたが、私がいつも一周する問題の1つは、言語での主要な操作を演算子にする場合と、キーワードまたは関数を使用する場合です。
カスタムオペレーターは簡単に作成でき、多くの場合、新しいデータ型にはいくつかのオペレーターがパッケージ化されて使用されるため、Haskellはこのことで悪名高くなっています。たとえば、Parsecライブラリには、_>.
_や_.>
_などのジェムを使用して、パーサーを組み合わせるための多数の演算子が付属しています。それらが実際に何を意味するかを覚えてしまえば、非常に扱いやすくなります。 leftCompose(parser1, parser2)
などの関数呼び出しの方が優れているでしょうか?確かにより冗長ですが、いくつかの点でより明確です。
Cのような言語での演算子のオーバーロードも同様の問題ですが、_+
_などの使い慣れた演算子の意味を通常とは異なる新しい意味でオーバーロードするという追加の問題が発生します。
新しい言語では、これはかなり難しい問題のように思えます。たとえば、F#では、C#スタイルのキャスト構文や冗長なVBスタイルではなく、数学的に派生した型キャスト演算子を使用してキャストします。C#:_(int32) x
_ VB:CType(x, int32)
F#:_x :> int32
_
理論的には、新しい言語はほとんどの組み込み機能の演算子を持つことができます。変数宣言でdef
またはdec
またはvar
の代わりに、_! name
_または_@ name
_または同様のものを使用しないでください。それは確かに宣言を短縮し、その後にバインドします:_@x := 5
_または_declare x = 5
_の代わりに_let x = 5
_ほとんどのコードは多くの変数定義を必要とするので、なぜでしょうか?
オペレーターはいつ明確で便利ですか、いつわかりませんか?
一般的な言語設計の観点からは、関数と演算子の間に違いはありません。関数は、任意の(または可変の)アリティを持つプレフィックス操作として説明できます。また、キーワードは予約された名前を持つ関数または演算子と見なすことができます(これは文法の設計に役立ちます)。
これらの決定はすべて、最終的には記法をどのように読んでほしいか、そして主観的であるかどうかに帰着します。誰もが知っているように、数学には通常の中置演算子を使用します
最後に、ダイクストラは 数学的表記の興味深い正当化 を使用しました。これには、中置表記と前置表記のトレードオフの良い議論が含まれています
私にとって、オペレーターは、コードの行を大声で読み上げたり頭の中で読んだりできなくなり、意味をなさなくなると、役に立たなくなります。
たとえば、declare x = 5
は"declare x equals 5"
として読み取るか、let x = 5
は"let x equal 5"
として読み取ることができます。これは、声を出して読むと非常に理解できますが、@x := 5
は次のように読み取られます"at x colon equals 5"
(または、数学者の場合は"at x is defined to be 5"
)。これはまったく意味がありません。
したがって、私の意見では、コードを大声で読み上げて、言語に精通していない適度にインテリジェントな人が理解できる場合は演算子を使用しますが、そうでない場合は関数名を使用します。
オペレータは、慣れているとわかりやすく便利です。そしておそらくそれは、選択された演算子が既に確立された慣行として十分に近い場合(形式、優先順位、結合性など)にのみ、演算子のオーバーロードを行うべきであることを意味します。
しかし、もう少し掘り下げると、2つの側面があります。
構文(関数呼び出し構文のプレフィックスではなく、演算子のインフィックス)と命名。
構文については、接頭辞よりも接頭辞が十分に明確であるという別のケースがあり、慣れ親しむ努力が必要になる場合があります。
_a * b + c * d + e * f
_を+(+(*(a, b), *(c, d)), *(e, f))
または_+ + * a b * c d * e f
_と比較します(どちらもinfixバージョンと同じ条件で解析可能です)。 _+
_は、用語の1つより前に来るのではなく、用語を分離するという事実により、私の見やすくなります(ただし、プレフィックス構文には不要な優先規則を覚えておく必要があります)。このように物事を複合する必要がある場合、長期的な利益の方法は学習コストに見合う価値があります。また、記号を使用する代わりに文字でオペレーターに名前を付けても、この利点は維持されます。
演算子の命名に関しては、確立されたシンボルまたは明確な名前以外のものを使用することにはいくつかの利点があると思います。はい、それらは短いかもしれませんが、それらは本当に不可解であり、すでにそれらを知っている正当な理由がない場合、すぐに忘れられます。
言語設計のPOVを使用する場合の3番目の側面は、演算子のセットを開くか閉じるか、つまり演算子を追加できるかどうか、および優先順位と結合性をユーザーが指定できるかどうかです。私の最初のテイクは注意深く、ユーザーが指定できる優先度と結合性を提供しないことですが、オープンセットを使用することでよりオープンになります(演算子のオーバーロードを使用するためのプッシュは増加しますが、悪いものを使用するためのプッシュは減少します)有用なインフィックス構文から利益を得るためです)。
演算子は、関数が使い慣れた数学的目的を厳密に反映している場合、はるかに読みやすくなると思います。たとえば、+や-などの標準演算子は、加算や減算を実行するときに、加算や減算よりも読みやすくなっています。オペレーターの行動は明確に定義する必要があります。たとえば、リスト連結の+の意味のオーバーロードを許容できます。理想的には、操作には副作用がなく、変更の代わりに値を返します。
ただし、foldなどの関数のショートカットオペレーターに悩んでいます。明らかに彼らとのより多くの経験がこれを楽にするでしょうが、私はfoldRight
が/:
より読みやすいと思います。
記号としての演算子は、直感的に理解できる場合に役立ちます。たとえば、+
と-
は明らかに加算と減算です。そのため、ほとんどすべての言語でこれらが演算子として使用されています。比較と同じ(<
および>
は小学校で教えられており、<=
および>=
は、下線付きの比較キーがないことを理解すると、基本的な比較を直感的に拡張したものです。標準キーボード)。
*
と/
はすぐにわかりにくくなりますが、普遍的に使用され、+
および-
キーの横にあるテンキー上の位置は、コンテキストを提供するのに役立ちます。 Cの<<
と>>
は同じカテゴリに属します。左シフトと右シフトが何であるかを理解すると、矢印の後ろのニーモニックを理解することはそれほど難しくありません。
次に、Cコードを読み取り不可能なオペレータースープに変える可能性のある、本当に奇妙なものに行きます。 (Cを選択するのは、Cまたは子孫言語のいずれかを使用して、構文がほとんどの人に馴染みのある、非常に演算子が多い例であるためです。)たとえば、%
演算子。誰もがそれが何であるか知っています:それはパーセント記号です... Cを除いて、100による除算とは関係ありません。それはモジュラス演算子です。それはニーモニックにゼロの意味を成しますが、それはあります!
さらに悪いのはブール演算子です。 &
はandとして意味がありますが、2つの異なる&
演算子があり、2つの異なる処理を実行します。また、いずれかが有効な場合はどちらも構文的に有効です。 、それらのうちの1つだけが実際に意味がある場合でも、どのような場合でも意味があります。これは単なる混乱のレシピです。 or、xorおよびnot演算子は、ニーモニックに役立つ記号を使用せず、一貫性もないため、さらに悪いものです。 (double -xor演算子は使用せず、論理およびビットごとのnotは、1つのシンボルの代わりに2つの異なるシンボルを使用し、2つめのバージョンを使用します。)
さらに悪いことに、&
演算子と*
演算子は完全に異なるものに再利用されるため、人とコンパイラの両方が構文解析することが難しくなります。 (A * B
の意味?コンテキストに完全に依存します:A
は型ですか、変数ですか?)
確かに、私はデニス・リッチーと話したことがなく、彼が言語で行ったデザインの決定について尋ねましたが、オペレーターの背後にある哲学は「シンボルのためのシンボル」であったように感じられます。
これをCと同じ演算子を持つPascalと比較してください。ただし、それらを表す際の哲学は異なります。演算子は直感的で読みやすいものでなければなりません。演算子は同じです。モジュラス演算子はWord mod
です。キーボードには明らかに「モジュラス」を意味する記号がないためです。論理演算子はand
、or
、xor
およびnot
であり、それぞれ1つしかありません。コンパイラーは、ブール演算と数値演算のどちらを実行しているかに基づいて、ブール版とビット版のどちらが必要かを認識し、エラーのクラス全体を排除します。これにより、PascalコードfarがCコードよりも理解しやすくなります。特に、構文が強調表示されているため、演算子とキーワードが識別子と視覚的に区別されている最新のエディターではわかりやすくなっています。
演算子の問題は、代わりに使用できる意味のある(!)メソッド名の数と比較すると、演算子の数が少ないことです。副次的な影響として、ユーザー定義可能な演算子は多くのオーバーロードを引き起こす傾向があります。
確かに、_C = A * x + b * c
_という行はC = A.multiplyVector(x).addVector(b.multiplyScalar(c))
よりも読み書きが簡単です。
あるいは、これらすべての演算子のオーバーロードされたバージョンがすべて頭にある場合は、書く方が確かに簡単です。そして、最後から2番目のバグを修正しながら、後で読むことができます。
さて、実行されるほとんどのコードについては、それで問題ありません。 「プロジェクトはすべてのテストに合格し、実行されます」-私たちが望むすべてのソフトウェアの多くにとって。
しかし、重要な領域に入るソフトウェアを実行している場合、状況は異なります。安全上重要なもの。セキュリティ。飛行機を空中に保つ、または核施設を動かし続けるソフトウェア。または、機密性の高いメールを暗号化するソフトウェア。
監査コードを専門とするセキュリティ専門家にとって、これは悪夢になることがあります。豊富なユーザー定義演算子と演算子のオーバーロードが混在していると、最終的にどのコードが実行されるかを理解するのが難しいという不愉快な状況に置かれる可能性があります。
したがって、元の質問で述べたように、これは非常に主観的な主題です。また、演算子の使用方法に関する多くの提案は完全に意味を成しているように聞こえるかもしれませんが、それらのいくつかだけを組み合わせると、長期的には多くの問題を引き起こす可能性があります。
最も極端な例はAPLだと思います。次のプログラムは「人生のゲーム」です。
life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
そして、もしあなたがリファレンスマニュアルで数日を費やすことなくそれが何を意味するかを理解することができるなら、あなたに幸運を!
問題は、ほぼ全員が直感的に理解できる一連の記号があることです:+,-,*,/,%,=,==,&
残りは、理解する前に説明が必要であり、一般的に特定の言語に固有のものです。たとえば、必要な配列のメンバーを指定する明確な記号はありません。[]、()、さらには "。"です。が使用されていますが、同様に簡単に使用できる明白なキーワードがないため、演算子の賢明な使用のために作成される場合があります。しかし、それらの多くはありません。