説明:ルールは、まだ宣言されていない変数へのアクセスを防ぐことを目的としています。
説明2:ルールは、コンパイラーが呼び出しと同じスコープで定義され、実際の定義(前方参照)の前に発生する関数への呼び出しに従うことを義務付けています-コンパイラーをインタープリターとよく似ています。
関数を宣言するときにJavaScriptと同様に動作する架空のプログラミング言語について考えています。つまり、function
キーワードを使用して名前付き関数を作成すると、スコープの最上部に「引き上げられ」、名前が表示されている場所で使用されます。
たとえば、以下は合法です。
f();
function f() { console.log("f called") };
ただし、潜在的な問題があります。関数f
が変数x
を使用する場合、それはafterとして定義されます。このような呼び出し:
f();
var x = 1;
function f() { console.log(x); }
JavaScriptの場合、変数も巻き上げられるため、これも合法です。プログラムは基本的に次のようなものに変換されます。
var x;
function f() { console.log(x); }
f();
x = 1;
したがって、単にundefined
と出力します。
私の架空の言語では、変数は巻き上げられません、つまり、前の例ではエラーが発生しました。ルールは、このエラーがどのように生成されるかに関するものです。
関数は、それらが定義されているブロック全体で表示されますが、呼び出し後に宣言された変数にアクセスする関数を呼び出すことはできません。
これは、この言語のコンパイラが関数呼び出しに遭遇するたびに、関数が現在のスコープで定義されているかどうかを確認し、定義されている場合は、現在のスコープの変数にアクセスするかどうかを確認する必要があることを意味します。呼び出し後に宣言されます。
このルールの理由は、宣言される前に変数にアクセスすることは意味がないためです。 JavaScriptのように変数をスコープの上部に移動するのは、エラーの余地を残すだけのハックです!だから私はそれをしてはいけないと思いますが、そのような状況は検出されてエラーとして扱われるべきです(なぜなら、それらは年代順に存在しないものを参照しているからです)。
これが悪いルールである理由、つまり複雑さや矛盾が生じる理由を探しています。
ルールが悪いものだと私が疑う理由は、Scala言語に存在しないことです。Scalaは、この問題をより一般的なルールで解決しました:
宣言または定義によって導入される名前のスコープは、バインディングを含むステートメントシーケンス全体です。ただし、ブロック内の前方参照には制限があります。ブロックを構成するステートメントシーケンス
s[1]...s[n]
で、s[i]
の単純な名前がs[j]
で定義されたエンティティを参照している場合、j >= i
、次にs[k]
からs[i]
までのすべてのs[j]
について、
s[k]
を変数定義にすることはできません。s[k]
が値の定義である場合、lazy
でなければなりません。
更新:
当分の間、私はScalaのアプローチを採用しました。
このアプローチは、私のルールのより一般的なバージョンです。すべての不正アクセスをキャッチしますが、そうでないものもあります。これは、ルールが複雑すぎるか、不可能であると思うので、私は喜んでトレードオフです。実装。
Scalaからの言い換え/説明されたルールは次のとおりです。
変数を前方参照することはできませんが、遅延値、defで定義された関数(これは(遅延)値に割り当てられたラムダの構文シュガーであり、Dovalが示唆するようにそれらを削除することもあります)、クラス、およびモジュールはできます。
制限とは、参照と宣言の間(両端を含む)に変数定義が存在してはならないということです。これは、プログラムに導入される前に参照されるエンティティによって変更される可能性があるためです。
あなたはすでに少なくとも1つの不整合を作成しており、特にそれにはひどい矛盾があります-他の値とは異なる方法で関数を扱います。関数宣言は概念的には、変数を無名関数にバインドするのと同じですが、関数宣言の際に「通常の」変数が巻き上げられないことを主張しています。そう...
f()
function f() { /* ... */ }
動作しますが...
f()
var f = function() { /* ... */ } // Pretend this is anonymous function syntax
しません。 (そして、他の値のように関数を渡すことを許可しないと、言語はひどく損なわれます。)次に、関数が再定義されるとどうなりますか?
f()
function f() { /* definition 1 */ }
...
function f() { /* definition 2 */ }
これをエラーとして扱うと、関数を値として再び拒否することになります。できないはずの理由はありません。
var f = function() { /* def 1 */ }
f = function() { /* def 2 */ }
したがって、関数の再定義を許可するとします。字句スコープをどのように扱いますか?
function f() { /* definition 1 */ }
function bar() {
f()
function f() { /* definition 2 */ }
定義2をbar
のスコープの先頭まで引き上げると、次のようになります。
function f() { /* definition 1 */ }
function bar() {
function f() { /* definition 2 */ }
f()
つまり、内部関数に別の名前を付けない限り、ネストされたスコープから外部定義を呼び出すことはできません。さらに、他の言語では、上記のプログラムの意味は、定義2の名前の選択とは無関係です。巻き上げ規則は、名前の衝突の有無によって意味を変更します。一方、定義[2]をbar
の先頭に上げない場合は、独自のルールに違反しています。
「架空の結果」対「架空のメリット」は、言語を設計していた場合に、そのようなルールの欠点を探していることを意味します。
私が目にする最大の欠点は、言語を「有用」なものにしないと(エラーの生成はこのクラスに該当します)、言語の能力が弱まることです。あなたはそれを使って何かをする機会がありません。
あなたはあなたがあなたの関数が変数を利用できるようにしたいなら、あなたはjavascriptが言うように同じスコープで両方を定義することができます:何かが最初に来なければならないという利点です-変数、または関数-その定義の字句スコープでは、これらの変数を「f」の最初の呼び出しの前に宣言されているかのように扱うことは完全に理にかなっています。
ローカル関数を持つのに便利です
func bigfunc() {...
prologue... setup .. parameter checking...
今:
for loop over the sanitized params (p1, p2, p3...) {
func a_case () {...will need access to prologue and setup vars}
func b_case () {... ditto....}
....
func z_case() {}
入力と小文字を探して...それから最初の文字を取り、それを '_case'の前に付加し、その後の処理のためにその関数を呼び出します。
endloop
} #end func
したがって、この場合の関数はヘルパー関数であり、bigfuncによって作成されたセットアップ情報へのアクセスが「必要」または「必要」ですが、その理由はありませんbigfuncの範囲外で表示されます。
したがって、これは見逃してしまう可能性のある機能の1つです。ネストされた関数を使用できるため、内部の関数は 'bigfunc'のコードでのみ呼び出すことができます。
別のオプションでは、bigfuncを呼び出すたびに、varが定義されているときに、varの値の独自のプライベートコピーを取得します。
つまりbigfuncが「関数ファクトリ」であり、関数が呼び出されたときに、変数「x」の「特定の」値を使用する匿名バインディングを返すとします。
だから….
func bigproducerfunc(parameters: object_instance) {
var object_instance; #maybe a filename?
// nested functions....
function size(...return size of object instance...)
function last_mod{,owner,destroy...} {
// do operation on value of 'x' that was passed in
// 'object_instance' when the function was initially called.
}
function check_mod_time {... checks last modification time
on the var it was called with)
}
var myhash funcs_by_name = (\add(), \size, \last_mod()...);
return refto(myhash).
}
これで、プログラムは操作したいファイルの名前を渡すことができます。プログラムはOSの違いを知る必要はありません-それらは各アクセサ関数で処理できます。それらにアクセスするには:
var hashref = bigproducerfunc("/etc/passwd")
var href2 = bigproducerfunc("/another pathname/")....
"$ hashref-> size"は、関数への参照リストが作成されて返されたときに渡された値を使用します。「bigproducerfunc」を呼び出すたびに、その関数で動作する関数のリストを作成できます1オブジェクトとそれのみ(スペルミスなどの可能性はありません)。
printf("Working name %s size = %s", $hashref->size);
この場合、その設定を使用して、作成時に渡したファイルでのみ機能する閉じた「環境」関数を作成できます。
処理したいファイルごとに、 'bigfunc'がファイル引数を指定して呼び出され、作成されたファイル名でのみ機能する一連の関数へのポインターが渡されます。これは、初期データを含む「閉じたシステム」と見なされます。
これは、「closure」と呼ばれることもあります-「関数ファクトリー」または「メソッドファクトリー」(この例ではbigfunc)に渡される、閉じたデータを操作する関数または関数のセット。関数を使用してデータをパックし、後で元のデータを再指定せずに使用できるようにします。また、関数が処理するデータを確認することもできます)。
===============================================
これらは、有用な何かをするのではなく、「エラー」としてフラグが付けられる「デッドセマンティック」または「デッドシンタックス」のケースに変換することによってあきらめることになる2つのタイプの異なる機能です。このユースケースで何かを行う2つの一般的な例ですが、作成できる他の例もあると思います...
デッドセマンティクスとデッド構文は、言語では「デッドスペース」です。
たとえば、 "Perl"を見てみましょう。 Perlは修正しても無害なデッドスポットを作成しましたが、おそらく言語の骨化によるものではありません。
構文 "string" x(例: "hi" x 5)は、連結されたその文字列のいくつかの繰り返しを作成します。
それを5回の倍数で実行したいので、乗数を渡します。
"string" x 3*$x; # error
'x'は '*'と同じ優先順位であるため、Perlでエラーが発生します。したがって、括弧を追加することで強制的に混乱させない限り、これを使用することはできません。
"string" x (3*$x); #ok
これを要求する理由はありません-演算子の優先順位表では、xはmult + divideと同じ優先順位にありますが、柔軟性が低いため、実際には同じと見なすべきではありません(関連付けがなく、 mult + divideと混合しないでください)。
通常、文字列は数値と組み合わされないため、数学演算子よりも低い「x」(繰り返し演算)の優先順位を作成することは理にかなっています。同様に、文字列連結機能は「加算+減算」よりも低くなければならないため、これは機能します。
> Perl -MP -we'use strict;
my $a="\nblah" . 4*5; #concatenate 'blah w/result of 4*5'
P "%s", $a;'
blah20
この:
> Perl -MP -we'use strict;
my $a="\nblah" . 4+5; #try to concat blah w/result of 4+5..(fail)
P "%s", $a;'
Argument "blah4" isn't numeric in addition (+) at -e line 2.
5
ではない。
どちらの場合も、死んだセマンティクスが言語に組み込まれました。
言語のセマンティクスを上手に使用するには理想的ではありません。それは不必要にエラーを作成し、完全に良い意味が明確に導き出されます。
これにより、優れた言語設計を実現するための洞察が得られることを願っています。 (私は注意する必要があります、何が機能し、何が機能しないかを知ることは、後から見ると常により簡単です;-))。
「宣言される前」という用語は、あまり明確に定義されていません。たとえば、Haskellでは次のように記述できます。
let
f x = x + y
y = something else
in f 42
私が言いたいのは、 "f"は、コードを前から読んだときにコードの前にあるからといって、 "以前に定義されている"ことを意味するのではありません。反対に、Haskellおよび関連言語では、同じ宣言グループ内のすべての項目が同時に定義(またはバインド)されていると見なされます。
あなたの質問に答えるために、架空の言語のユーザーにとっての否定的な結果は、「定義の順序」を任意の方法で強制した場合、定義を手動で順序付けしなければならないということです。その後に続くもう1つのことは、相互に再帰的な定義が不可能であること、または関数型を持つものを他の型を持つものとは異なる方法で、正当な理由なしに処理することです。
これは、元のC言語によく似ています。 (加えて、期間中の他の言語の全束。).
変数関数などを参照する前に定義する必要がありました。 「前」は厳密に「前のソース行」を意味します。
これが、「main()」関数が論理的に属している先頭ではなく、ほとんどのCプログラムの後ろにある理由です。
言語デザイナーは、これは素晴らしいアイデアだとは考えていませんでした!彼らはこれを行って、ソースコードを2回パスするコンパイラを節約し、利用可能なハードウェアで適切な時間にプログラムをコンパイルしました。
そして、それは本当に良い考えではありません-それは彼らが彼らがいくつかのうるさい構文を満足させるためにむしろ彼らがむしろしたくないものを定義しなければならないことをプログラマーを困らせるだけです。
ネストされたスコープをサポートする言語では、次の影響があります。
int foo()
{
int x,y;
x=1; y=0;
while(y < 5)
{
x=4;
y++;
int x;
x+=23;
}
return x;
}
内部スコープ内で、x
の使用がそのスコープレベルでの宣言に従っていた場合、コードの意味は明らかでした。
完璧なプログラミング言語は、プログラマーが意図したとおりに動作しないものをコンパイルすることを拒否するという原則を守り、プログラマーがx=4
外部スコープx
を変更しますが、プログラマが内部スコープを変更することを意図していた可能性もあり、コンパイルされた動作はプログラマの意図とは逆の動作をする可能性が高いことを意味します。
ソースファイル内の関数定義の順序が呼び出される順序と関係がない言語では、ファイルを関数定義への関数シグネチャの無秩序なマッピングを保持していると見なすことは理にかなっていると思います。他にもたくさんありますそのため、順序付けが重要でない場合は、「巻き上げ」の概念も重要です。順序付けが関係する場合、スコープ内で識別子を宣言すると、スコープのstartでその識別子の外部宣言が使用できなくなることをお勧めしますが、新しい宣言は、それが発生した後。さらに、スコープ内で外部スコープ識別子の宣言を解除する構文を定義し、コードがそれを使用できない場合に警告します[新しい外部スコープ識別子の定義がエラーにならないように、エラーではなく警告を出します重大な変化]。
一部の目的では、次のセマンティクスで「一時」変数を宣言する明示的な方法があると便利です。
ここでの基本的な考え方は、「変数」がコードの特定の領域内で常に1つの特定の場所に書き込まれた値を保持するという一般的な状況を認識することです。次のような構成を考えます。
int result;
...
result = someMethod();
if (result < 0) doSomething();
...
result = someOtherMethod();
if (result < 0) doSomething();
...
result = YetAnotherMethod();
if (result < 0) doSomething();
コード内の特定の場所でresult
の値が常にコード内のその場所に先行する最も近い割り当てステートメントによって設定されているかどうかが常に明確であるとは限らない場合。代わりにコードが次のようなものだった場合:
temp var result = someMethod();
if (result < 0) doSomething();
...
temp var result = someOtherMethod();
if (result < 0) doSomething();
...
temp var result = YetAnotherMethod();
if (result < 0) doSomething();
次に、result
の使用方法を見つけ、temp var
上のどこかの宣言は、temp var
宣言は使用する値を書き込んでいます。