Java学校の危険 の危険で、ジョエルはペンでの経験と「セグメンテーション違反」の難しさについて話します。
[segfaultsは、あなたができるまで難しい]「深呼吸して、本当に2つの異なる抽象化レベルで同時に作業するように心を強制しようとする」
Segfaultsの 一般的な原因 のリストを考えると、2つの抽象化レベルでどのように作業する必要があるのか理解できません。
何らかの理由で、Joelはこれらの概念をプログラマーが抽象化する能力の中核と見なしています。思い込みすぎたくない。それで、ポインタ/再帰についてそれほど難しいことは何ですか?例はいいでしょう。
私はポインタと再帰が大学で難しいことに最初に気づきました。私はいくつかの典型的な初年度コースを受講しました(1つはCとアセンブラ、もう1つはスキームにありました)。どちらのコースも数百人の学生から始まり、その多くは高校レベルのプログラミング経験を積んでいました(当時のBASICとPascalが一般的)。しかし、Cコースにポインターが導入され、Schemeコースに再帰が導入されるとすぐに、非常に多くの学生(おそらくは過半数)が完全にフラモックスしました。これらは、以前にたくさんのコードを書いたことがあり、まったく問題がなかった子供たちでしたが、ポインターや再帰にぶつかったときに、認知能力の面でも壁にぶつかりました。
私の仮説は、ポインターと再帰は同じですが、同時に2つのレベルの抽象化を頭の中で維持する必要があるということです。複数のレベルの抽象化について、ある種の精神的適性を必要とするものがあるので、一部の人々はこれを実現できない可能性があります。
私はまた、ポインタや再帰を誰にでも教えることが可能であることを完全に受け入れたいと思います...何らかの形で証拠はありません。経験的に、これら2つの概念を本当に理解できることは、一般的なプログラミング能力の非常に優れた予測因子であり、学部のCSトレーニングの通常のコースでは、これら2つの概念が最大の障害の一部として立っていることを知っています。
再帰は、単に「それ自体を呼び出す関数」ではありません。スタックフレームを描画して再帰降下パーサーの問題点を理解するまで、再帰が難しい理由を理解することはできません。多くの場合、相互に再帰的な関数があります(関数Aは関数Bを呼び出し、関数Bは関数Cを呼び出し、関数Cは関数Aを呼び出す場合があります)。相互再帰的な一連の関数のスタックフレームがNの場合、何が問題だったかを理解するのは非常に難しい場合があります。
ポインタについても、ポインタのconceptは非常に単純です。メモリアドレスを格納する変数です。しかし、繰り返しになりますが、別のノードを指すvoid**
ポインターの複雑なデータ構造に問題が発生すると、ポインターの1つがゴミを指している理由を理解するのに苦労しながら、トリッキーになる可能性がある理由がわかります住所。
Javaはポインター(参照と呼ばれます)をサポートし、再帰もサポートしています。表面上、彼の議論は無意味に見えます。
彼が本当に話しているのは、デバッグする能力です。 A Javaポインタ(エラー、参照)は、有効なオブジェクトを指すことが保証されています。ACポインタはそうではありません。そして、Cプログラミングでのトリックです。 valgrind、は、ポインタをどこで台無しにしたかを正確に見つけることです(スタックトレースで見つかったポイントではめったにありません)。
ポインターと再帰の問題は、必ずしも理解するのが難しいということではなく、CやC++などの言語に関して特にについて間違って教えられていることです(主に、言語自体が間違って教えられているためです) )。誰かが「配列は単なるポインタである」と言う(または読む)たびに、私は少し内側で死にます。
同様に、誰かがフィボナッチ関数を使用して再帰を説明するたびに、悲鳴を上げたいです。反復バージョンは書くのが難しくないので、これは悪い例ですand少なくとも再帰的バージョンと同じかそれ以上に実行され、再帰的ソリューションが有用である理由や、望ましい。クイックソート、ツリートラバーサルなどは、再帰の理由と方法についてfarより良い例です。
ポインターをいじる必要があるのは、それらを公開するプログラミング言語で作業することの成果物です。 Fortranプログラマーの世代は、専用のポインター型(または動的メモリー割り当て)を必要とせずにリスト、ツリー、スタック、およびキューを構築していたため、Fortranがおもちゃの言語であると非難されることはありません。
ポインターにはいくつかの困難があります。
これが、プログラマーがポインターを使用する際により徹底的に考えなければならない理由です(私は2レベルの抽象化について知りません)。これは、初心者が犯す典型的な間違いの例です。
Pair* make_pair(int a, int b)
{
Pair p;
p.a = a;
p.b = b;
return &p;
}
上記のようなコードは、ポインターの概念がなく、関数型プログラミング言語やガベージコレクションを備えた言語(Java、Python)のように、名前(参照)、オブジェクト、および値の1つを持たない言語では完全に妥当です。 。
再帰関数の問題は、十分な数学的背景(再帰性が一般的で必要な知識がある)を持たない人々が、以前に呼び出された回数に応じて関数の動作が異なると考えてアプローチしようとすると発生します。再帰関数は確かにを作成できるためそのように考える必要があるため、その問題はさらに悪化します(---)。
データ構造がインプレースで変更される Red-Black Tree の手続き型実装のように、ポインタが渡される再帰関数を考えてください。 機能的な対応物 よりも考えるのが難しいものです。
それは質問には記載されていませんが、初心者が難しい他の重要な問題は concurrency です。
他の人が述べたように、一部のプログラミング言語の構成要素には、概念的でない追加の問題があります。それは、これらの構成要素の単純で正直な間違いを理解したとしても、デバッグが非常に難しい場合があることです。
ポインタと再帰は2つの別個の野獣であり、それぞれが「難しい」とみなされる理由はさまざまです。
一般に、ポインターには、純粋な変数の割り当てとは異なるメンタルモデルが必要です。私がポインター変数を持っているとき、それはそれだけです:別のオブジェクトへのポインター、そこに含まれる唯一のデータは、それが指すメモリアドレスです。したがって、たとえば、int32ポインタがあり、それに値を直接割り当てている場合、intの値は変更せず、新しいメモリアドレスをポイントしています(これを使用してできる巧妙なトリックがたくさんあります) )。さらに興味深いのは、ポインターへのポインターを持つことです(これは、Ref変数をC#の関数パラメーターとして渡すと発生します。関数は、まったく異なるオブジェクトをパラメーターに割り当てることができ、その値は、関数が終了します。
再帰は、それ自体で関数を定義しているため、最初に学習するときに少し精神的に飛躍します。最初に出会ったときはワイルドなコンセプトですが、一度理解すれば第二の性質になります。
しかし、目の前の主題に戻ります。ジョエルの主張は、ポインタやそれ自体の再帰についてではなく、学生がコンピュータの実際の動作からさらに遠ざけられているという事実です。これはコンピュータサイエンスの科学です。プログラムの学習とプログラムのしくみの学習には明確な違いがあります。多くのCSプログラムが栄誉ある貿易学校になりつつあると彼が主張するので、「私はこのようにしてそれを学んだので、誰もがこのようにそれを学ばなければならない」ということはそれほど問題ではないと思います。
問題は抽象化自体の複数のレベルで考えることの1つであるというジョエルとは同意しません。ポインターと再帰が、プログラムの動作について人々が持っているメンタルモデルの変更を必要とする問題の2つの良い例であると私は思います。
ポインタは、説明するのに簡単なケースだと思います。ポインタを処理するには、プログラムがメモリアドレスとデータを実際に処理する方法を説明するプログラム実行のメンタルモデルが必要です。私の経験では、多くの場合、プログラマーはポインターについて学ぶ前にこれについてさえ考えていませんでした。彼らが抽象的な意味でそれを知っていたとしても、彼らはプログラムがどのように機能するかの認知モデルにそれを採用していません。ポインタが導入されると、コードがどのように機能するかについての考え方を根本的に変える必要があります。
再帰は、理解するための2つの概念的なブロックがあるため、問題があります。 1つ目はマシンレベルであり、ポインターのように、プログラムが実際に格納および実行される方法を十分に理解することで克服できます。再帰に関するもう1つの問題は、再帰的な問題を非再帰的な問題に分解しようとする自然な傾向があり、それが再帰関数のゲシュタルトとしての理解を混乱させることです。これは、数学的な背景が不十分な人々の問題であるか、または数学の理論をプログラムの開発に結び付けないメンタルモデルです。
問題は、不十分なメンタルモデルで立ち往生している人々にとって問題となるのは、ポインターと再帰だけだとは思いません。並列処理は、一部の人々が単に行き詰まり、メンタルモデルを考慮に入れることが難しい別の領域のようです。ポインターと再帰がインタビューで簡単にテストできることがよくあるのです。
私はP.ブライアンに+1を与えています。私は彼のように感じているからです。再帰は非常に基本的な概念であり、少しでも困難を抱えている彼は、マックドナルドで仕事を探すことを検討する必要がありますが、再帰さえあります。
make a burger:
put a cold burger on the grill
wait
flip
wait
hand the fried burger over to the service personel
unless its end of shift: make a burger
確かに、理解力の欠如は私たちの学校にも関係しています。ここでは、ペアノ、デデキント、フレゲのように自然数を導入する必要があります。そうすれば、後でそれほど難しくありません。
DATA | CODE
|
pointer | recursion SELF REFERENTIAL
----------+---------------------------------
objects | macro SELF MODIFYING
|
|
自己参照データと自己参照コードの概念は、それぞれポインターと再帰の定義の基礎となっています。残念ながら、命令型プログラミング言語への広範囲な暴露により、コンピューターサイエンスの学生は、言語の機能的な側面に対するこの謎を信頼すべきであるときに、ランタイムの操作上の動作を通じて実装を理解する必要があると信じるようになりました。 100までのすべての数を合計することは、1から始めてそれをシーケンスの次の数に追加し、循環自己参照関数を使用して逆に行うという単純な問題のように思われ、安全性に慣れていない多くの人にとっては危険でさえあるようです。純粋な関数。命令型言語の過去の経験はそれらを焼き払っており、介在するすべての状態を理解したいという願望は、関数が再び呼び出されたときに同じ名前が同じものを参照していないと変数が変更される可能性があるという疑いによって引き起こされます自分の中から。
自己変更データとコードの概念は、それぞれオブジェクト(つまり、スマートデータ)とマクロの定義の基礎となっています。特に、4つの概念すべての組み合わせからランタイムの運用上の理解が期待される場合は理解が難しいため、これらについて言及します。ポインターのツリーの助けを借りて再帰的なまともなパーサーを実装するオブジェクトのセットを生成するマクロ。プログラムの状態の操作全体を、抽象化のすべての層を一度に段階的に追跡するのではなく、命令型プログラマは、それらの変数が純粋な関数内で1回だけ割り当てられ、同じ純粋な関数が繰り返し呼び出されることを信頼することを学ぶ必要があります。 Javaのように不純な関数もサポートする言語であっても、同じ引数は常に同じ結果(つまり、参照透過性)を生成します。実行後に円を描いて走り回ることは、実りのない努力です。抽象化は単純化する必要があります。