web-dev-qa-db-ja.com

ほとんどのプログラミング言語に、関数を宣言するための特別なキーワードまたは構文があるのはなぜですか?

ほとんどのプログラミング言語(動的に型付けされた言語と静的に型付けされた言語の両方)には、関数を宣言するための変数の宣言とは大きく異なる特別なキーワードや構文があります。関数は、別の名前付きエンティティを宣言するのと同じように見えます。

たとえばPythonの場合:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

何故なの:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

同様に、Javaのような言語では:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

何故なの:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

この構文は、一部の言語ではdeffunctionのように何かを宣言するより自然な方法のように見えます(関数または変数)。そして、IMOはより一貫性があり(変数または関数のタイプを理解するために同じ場所を調べます)、おそらくパーサー/文法を少し簡単に記述できます。

このアイデアを使用している言語(CoffeeScript、Haskell)はごくわずかですが、ほとんどの一般的な言語には、関数(Java、C++、Python、JavaScript、C#、PHP、Ruby)のための特別な構文があります。

両方の方法をサポートする(そして型推論を行う)Scalaでも、次のように書くのが一般的です。

def addOne(x: Int) = x + 1

のではなく:

val addOne = (x: Int) => x + 1

IMO、少なくともScalaでは、これはおそらく最も簡単に理解できるバージョンですが、このイディオムがほとんどフォローされていません。

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

私は自分のおもちゃの言語に取り組んでいて、そのような方法で私の言語を設計する場合に落とし穴があるかどうか、そしてこのパターンが広く守られていない歴史的または技術的な理由があるかどうか疑問に思っていますか?

39
pathikrit

その理由は、ほとんどの人気のある言語は、関数型言語とそのルーツであるラムダ計算ではなく、C言語ファミリーの言語に由来するか、影響を受けたためです。

これらの言語では、関数はnotが別の値です:

  • C++、C#、およびJavaでは、関数をオーバーロードできます。同じ名前でシグネチャが異なる2つの関数を使用できます。
  • C、C++、C#、およびJavaでは、関数を表す値を使用できますが、関数ポインター、ファンクター、デリゲート、および関数インターフェイスはすべて、関数自体とは異なります。その理由の一部は、それらのほとんどが実際には単なる関数ではなく、何らかの(可変)状態を備えた関数であることです。
  • 変数はデフォルトで変更可能です(変更を禁止するにはconstreadonlyまたはfinalを使用する必要があります)が、関数を再割り当てすることはできません。
  • より技術的な観点から見ると、コード(関数で構成される)とデータは分離されています。通常、それらはメモリのさまざまな部分を占有し、それらへのアクセス方法は異なります。コードは一度ロードされてから実行されるだけです(ただし、読み取りまたは書き込みは行われません)。一方、データは多くの場合、常に割り当ておよび割り当て解除され、書き込みおよび読み取りが行われますが、実行されることはありません。

    そしてCは「金属に近い」ことを意図していたので、言語の構文にもこの違いを反映することは理にかなっています。

  • C++、C#、およびJavaでのラムダの導入の遅れから明らかなように、関数型プログラミングの基礎を形成する「関数は単なる値」アプローチは、ごく最近になって一般的な言語で勢力を得ました。 (2011、2007、2014)。

48
svick

hum​​ansは、関数が単なる「別の名前付きエンティティ」ではないことを認識することが重要であるためです。それらをそのように操作することが理にかなっている場合もありますが、それでも一目で認識できます。

理解できない文字の塊はマシンが解釈するのに適しているため、コンピューターが構文をどう考えているかは問題ではありませんが、人間が理解して維持することはほとんど不可能です。

これらすべてが最終的に比較とジャンプの命令に煮詰められても、whileとforループ、switchとif elseがあるなどの理由と同じです。その理由は、コードを保守および理解する人間の利益のために存在するからです。

関数を「別の名前付きエンティティ」として提案する方法で使用すると、コードが見にくくなり、理解しにくくなります。

59
whatsisname

先史時代にさかのぼって、ALGOL 68と呼ばれる言語が、あなたが提案するものに近い構文を使用したことを知りたいと思うかもしれません。関数識別子が他の識別子と同じように値にバインドされていることを認識すると、その言語で構文を使用して関数(定数)を宣言できます

関数タイプ名前 =(パラメータリスト結果タイプ本体 ;

具体的にはあなたの例は

PROC (INT)INT add one = (INT n) INT: n+1;

最初の型が宣言のRHSから読み取れるという冗長性を認識し、関数型であることは常にPROCで始まり、これは(通常は)

PROC add one = (INT n) INT: n+1;

ただし、=には引き続きbeforeパラメータリストが含まれています。また、関数variable(同じ関数タイプの別の値を後で割り当てることができる)が必要な場合は、=:=に置き換えて、の一つ

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

ただし、この場合、どちらの形式も実際には省略形です。識別子func varはローカルで生成された関数への参照を指定するため、完全に展開された形式は次のようになります。

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

この特定の構文形式は使い慣れていますが、他のプログラミング言語ではそれほど大きな支持を得ていませんでした。 Haskellのような関数型プログラミング言語でさえ、f n = n+1=のスタイルを好んでいます以下パラメータリスト。その理由は主に心理的なものだと思います。結局のところ、数学者でさえ私が好むように、しばしば好まないf = nn + fよりも1 =(n)= n + 1。

ちなみに、上記の説明では、変数と関数の重要な違いが1つ強調されています。関数定義は通常、名前を特定の関数値にバインドしますが、後で変更することはできませんが、変数定義は通常、initial値ですが、後で変更される可能性があります。 (これは絶対的な規則ではありません。関数変数と非関数定数はほとんどの言語で発生します。)さらに、コンパイルされた言語では、関数定義にバインドされた値は通常コンパイル時定数なので、関数の呼び出しは次のようになります。コード内の固定アドレスを使用してコンパイルされます。 C/C++では、これも要件です。 ALGOL 68に相当

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

関数ポインタを導入せずにC++で記述することはできません。この種の特定の制限は、関数定義に異なる構文を使用することを正当化します。しかし、それらは言語のセマンティクスに依存しており、正当化はすべての言語に適用されるわけではありません。

10

JavaおよびScalaの例として言及しました。しかし、これらは関数ではなく、メソッドです。メソッドと関数は- 基本的に異なる関数はオブジェクトであり、メソッドはオブジェクトに属します。

関数とメソッドの両方を備えたScalaでは、メソッドと関数の間に以下の違いがあります。

  • メソッドはジェネリックにできますが、関数はできません
  • メソッドにはパラメータリストを1つも含めなくてもかまいません。関数には常に1つのパラメータリストしかありません。
  • メソッドは暗黙的なパラメータリストを持つことができますが、関数はできません
  • メソッドはデフォルト引数を持つオプションのパラメーターを持つことができ、関数はできません
  • メソッドはパラメータを繰り返すことができますが、関数はできません
  • メソッドは名前によるパラメータを持つことができ、関数はできません
  • メソッドは名前付き引数で呼び出すことができますが、関数はできません
  • 関数はオブジェクトですが、メソッドはそうではありません

したがって、提案された置換は、少なくともそれらのケースでは、単に機能しません。

8
Jörg W Mittag

私が考えることができる理由は次のとおりです。

  • コンパイラーが宣言内容を理解しやすくなります。
  • これが関数なのか変数なのかを(簡単な方法で)知ることが重要です。関数は通常ブラックボックスであり、内部実装については気にしません。 Scalaでの戻り値の型の型推論は嫌いです。なぜなら、戻り値の型を持つ関数を使用するほうが簡単だと思うからです。提供されるドキュメントは多くの場合、それだけです。
  • そして最も重要なのは、プログラミング言語の設計で使用される群集を追う戦略です。 C++はCプログラマを盗むために作成され、JavaはC++プログラマを怖がらせないように設計されており、C#はJavaプログラマを引き付けるように設計されています。C#でさえ、素晴らしいチームが背後にいる非常に現代的な言語だと思いますが、JavaまたはCからさえいくつかの間違いをコピーしました。
3
Sleiman Jneidi

質問を逆にすると、RAMの制約が非常に大きいマシンでソースコードを編集したり、フロッピーディスクからソースコードを読み取る時間を最小限にしたりすることに興味がない場合、キーワードを使用することの何が問題になっていますか?

確かに_x=y+z_よりも_store the value of y plus z into x_の方が読みやすいですが、句読文字がキーワードよりも本質的に「優れている」という意味ではありません。変数ij、およびkIntegerで、xRealの場合、次の行を検討してください。パスカルで:

_k := i div j;
x := i/j;
_

1行目は切り捨て整数除算を実行し、2行目は実数除算を実行します。 Pascalは、切り捨て整数除算演算子としてdivを使用しているため、別の目的(実数除算)がすでにある句読点を使用するのではなく、区別がうまくいきます。

関数定義を簡潔にすることが役立つコンテキストがいくつかありますが(たとえば、別の式の一部として使用されるラムダ)、関数は一般的に目立ち、関数として視覚的に簡単に認識できるはずです。区別をはるかに細かくして句読点文字のみを使用することは可能かもしれませんが、ポイントは何でしょうか? Function Foo(A,B: Integer; C: Real): Stringと言うことで、関数の名前が何であるか、関数が期待するパラメーター、そして何が返されるかが明確になります。 Functionをいくつかの句読文字に置き換えることで、6文字または7文字短縮できるかもしれませんが、何が得られるでしょうか。

もう1つの注意点は、ほとんどのフレームワークでは、名前を常に特定のメソッドまたは特定の仮想バインディングに関連付ける宣言と、特定のメソッドまたはバインディングを最初に識別する変数を作成する宣言との間に基本的な違いがあることです。変更可能実行時別のものを識別するため。これらはほとんどの手続き型フレームワークでは意味的に非常に異なる概念であるため、異なる構文を使用する必要があります。

2
supercat

ええと、その理由は、そういった言語が十分に機能していないためです。つまり、関数を定義することはめったにありません。したがって、余分なキーワードの使用は許容されます。

MLまたはミランダの遺産であるOTOHの言語では、ほとんどの場合、関数を定義します。たとえば、いくつかのHaskellコードを見てください。それは文字通りほとんどが関数定義のシーケンスであり、それらの多くはローカル関数とそれらのローカル関数のローカル関数を持っています。したがって、Haskellのfunキーワードは、assignで始まる命令型言語の割り当てステートメントを要求するのと同じくらい大きな間違いです。原因の割り当ては、おそらく最も頻度の高い単一のステートメントです。

2
Ingo

個人的には、あなたの考えに致命的な欠陥は見当たりません。新しい構文を使用して特定のことを表現するのが予想よりも難しい場合や、(さまざまな特殊なケースや他の機能を追加するなどして)修正する必要がある場合がありますが、自分で見つけることはできません。アイデアを完全に放棄する必要があります。

あなたが提案した構文は多かれ少なかれ数学の関数や関数のタイプを表現するために時々使用されるいくつかの表記スタイルの変形のように見えます。これは、他のすべての文法と同様に、おそらく他のプログラマーよりも一部のプログラマーにとって魅力的なものになるでしょう。 (数学者として、私はたまたま好きです。)

ただし、ほとんどの言語では、defスタイルの構文(つまり、従来の構文)doesの動作が標準の変数割り当てとは異なることに注意してください。

  • CおよびC++ファミリー、関数は一般に「オブジェクト」として扱われません。つまり、コピーされてスタックに置かれる型付きデータのチャンクなどは扱われません。 (はい、関数ポインターを持つことはできますが、それらは通常の意味での「データ」ではなく、実行可能コードを指します。)
  • ほとんどのOO言語では、メソッド(つまり、メンバー関数)の特別な処理があります。つまり、それらはクラス定義のスコープ内で宣言された関数だけではありません。最も重要な違いは、メソッドが呼び出されているオブジェクトは通常、暗黙の最初のパラメーターとしてメソッドに渡されます。Pythonは、これをselfで明示的にします(ちなみに、実際にはキーワード;任意の有効な識別子をメソッドの最初の引数にすることができます)。

新しい構文がコンパイラーまたはインタープリターが実際に行っていることを正確に(そしてできれば直感的に)表すかどうかを検討する必要があります。たとえば、ラムダとRubyのメソッドの違いを読むのに役立つかもしれません。これにより、関数(データのみ)のパラダイムが典型的なOO /手続き型パラダイムとどのように異なるかがわかります。

1
Kyle Strand

一部の言語では、関数は値ではありません。そんな言葉で

val f = << i : int  ... >> ;

は関数定義ですが、

val a = 1 ;

は定数を宣言しますが、1つの構文を使用して2つのことを意味しているため、混乱を招きます。

ML、Haskell、Schemeなどの他の言語では、関数を第1クラスの値として扱いますが、関数の値の定数を宣言するための特別な構文をユーザーに提供します。つまりコンストラクトが一般的で詳細な場合は、ユーザーに省略形を与える必要があります。まったく同じことを意味する2つの異なる構文をユーザーに提供するのは不合理です。時にはエレガンスを実用性のために犠牲にする必要があります。

あなたの言語では、関数がファーストクラスであるなら、構文糖を見つけたくなくなるほど簡潔な構文を見つけてみませんか?

-編集-

(まだ)誰も取り上げていないもう1つの問題は再帰です。許可した場合

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

そしてあなたは許可します

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

それはあなたが許可するべきであることに従っていますか

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

遅延言語(Haskellなど)では、ここで問題はありません。本質的に静的チェックがない言語(LISPなど)では、ここで問題はありません。ただし、静的にチェックされる熱心な言語では、最初の2つを許可し、最後の2つを禁止する場合は、静的チェックのルールの定義方法に注意する必要があります。

-編集の終わり-

* Haskellはこのリストに含まれていないと主張されるかもしれません。これは、関数を宣言する2つの方法を提供しますが、どちらもある意味では、他の型の定数を宣言するための構文の一般化です

1

これは、型がそれほど重要ではない動的言語では役立つかもしれませんが、変数の型を常に知りたい静的型付き言語ではそれほど読みやすいものではありません。また、オブジェクト指向言語では、変数がサポートしている操作を知るために、変数の型を知ることは非常に重要です。

あなたの場合、4つの変数を持つ関数は次のようになります:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

関数ヘッダーを見て(x、y、z、s)を見ると、これらの変数の型がわかりません。 3番目のパラメーターであるzのタイプを知りたい場合は、関数の先頭を見て、1、2、3のカウントを開始してから、タイプがdoubleであることを確認する必要があります。前者の方法では、直接見てdouble z

0
m3th0dman

ほとんどの言語でこのような区別を付けるのには非常に単純な理由があります。evaluationdeclarationを区別する必要があります。あなたの例は良いです:なぜ変数が好きではないのですか?まあ、変数式はすぐに評価されます。

Haskellには、評価と宣言の区別がない特別なモデルがあります。そのため、特別なキーワードは必要ありません。

0

関数は、ほとんどの言語でリテラルやオブジェクトなどとは異なる方法で宣言されます。これは、関数の使用方法やデバッグ方法が異なり、潜在的なエラーの原因が異なるためです。

動的オブジェクト参照または可変オブジェクトが関数に渡された場合、関数は実行中にオブジェクトの値を変更できます。この種の副作用により、関数が複雑な式内にネストされている場合、関数の動作を追跡することが難しくなる可能性があります。これは、C++やJavaなどの言語でよくある問題です。

すべてのオブジェクトにtoString()オペレーションがあるJavaのある種のカーネルモジュールのデバッグを検討してください。 toString()メソッドがオブジェクトを復元することが期待される場合もありますが、その値をStringオブジェクトに変換するために、オブジェクトを逆アセンブルおよび再アセンブルする必要がある場合があります。 toString()が(フックとテンプレートのシナリオで)呼び出してその作業を行うメソッドをデバッグしようとしていて、ほとんどのIDEの変数ウィンドウで誤ってオブジェクトを強調表示すると、デバッガーがクラッシュする可能性があります。これは、IDEが、デバッグ中のコードを呼び出すオブジェクトをtoString()しようとするためです。プリミティブの意味的な意味が値は、プログラマーではなく言語によって定義されます。

0
Trixie Wolf