私は命令型プログラミングの考え方に固執していて、Haskellを適切に受け入れていませんか?
あなたは立ち往生していません、少なくとも私はそうは思わない。あなたが経験することは絶対に正常です。命令型言語で作業しているときに、プログラミングの問題を非常に具体的な観点から、つまりvan Neumannマシンの観点から見ることを(おそらく知らずに)学びました。
たとえば、いくつかの数のシーケンスを含むリストを作成するという問題がある場合(たとえば、最初の1000個の偶数が必要な場合)、すぐに次のことを考えます。リンクリストの実装(おそらくプログラミング言語の標準ライブラリから) 、ループと、開始値に設定した変数をしばらくループし、2を追加して変数を更新し、リストの最後に配置します。
マシンにサービスを提供することを主にどのように考えているかわかりますか?メモリの場所、ループなど!命令型プログラミングでは、特定のメモリセルを特定の順序で操作して、常にソリューションに到達する方法について考えます。(これが、初心者が(命令型)プログラミングの学習を困難に感じる理由の1つです。プログラマー以外の人は、問題を一連のメモリ操作に減らすことで問題を解決することに慣れていません。なぜそうすべきなのですか?しかし、それを学んだら、命令型の世界では、力を持っています。機能的なプログラミングでは、それを学ぶ必要があります。)
関数型プログラミング、特にHaskellでは、リストの構築法則を述べるだけです。リストは再帰的なデータ構造であるため、この法則ももちろん再帰的です。私たちの場合、たとえば次のように言うことができます。
constructStartingWith n = n : constructStartingWith (n+2)
そして、ほぼ完了です!最終的なリストにたどり着くには、どこから始めて、いくつ欲しいかを言うだけです。
result = take 1000 (constructStartingWith 0)
より一般的なバージョンのconstructStartingWith
がライブラリで利用可能であることに注意してください。これは呼び出されiterate
、開始値だけでなく、現在のリスト要素から次のリスト要素を作成する関数も受け取ります。
iterate f n = n : iterate f (f n)
constructStartingWith = iterate (2+) -- defined in terms of iterate
別のアプローチは、リストを簡単に作成できる別のリストがあると想定することです。たとえば、最初のn個の整数のリストがある場合、各要素に2を掛けることで、偶数の整数のリストに簡単に入れることができます。これで、Haskellの最初の1000(非負)整数のリストは単純になります。
[0..999]
そしてmap
、各引数に与えられた関数を適用することによってリストを変換する関数があります。必要な関数は、要素を2倍にすることです。
double n = 2*n
したがって:
result = map double [0..999]
後で、より多くのショートカットを学習します。たとえば、を定義する必要はありませんがdouble
、セクションを使用できます。(2*)
または、リストをシーケンスとして直接書き込むこともできます。[0,2..1998]
しかし、これらのトリックをまだ知らなくても、気分が悪くなることはありません!あなたが今直面している主な課題は、最初の1000個の偶数のリストを作成する問題が2段階の問題であることがわかる精神を発達させることです:a)すべての偶数のリストがどのように見えるかを定義しますb)取るそのリストの特定の部分。との手書きバージョンをまだ使用している場合でも、そのように考え始めると完了です。iterate
take
オイラー問題に戻る:ここでは、トップダウン方式を使用できます(そして、実際に知っておくべきいくつかの基本的なリスト操作関数:、、、head
)。まず、素数のリストがすでにある場合は、最初の1000を削除し、残りの頭を取得して1001番目の素数を取得できます。drop
filter
any
result = head (drop 1000 primes)
無限のリストから任意の数の要素を削除した後でも、そこから選択する空でないリストが残っていることがわかっているhead
ため、ここでの使用head
は正当化されます。1000を超える素数があるかどうかわからない場合は、次のように記述してください。
result = case drop 1000 primes of
[] -> error "The ancient greeks were wrong! There are less than 1001 primes!"
(r:_) -> r
さて、難しい部分です。続行する方法がわからないので、いくつかの擬似コードを書くことができます。
primes = 2 : {-an infinite list of numbers that are prime-}
2が最初の素数、いわばベースケースであることは確かです。したがって、それを書き留めることができます。埋められていない部分は、私たちに考えるべきことを与えてくれます。たとえば、リストは明らかな理由で2より大きい値で開始する必要があります。したがって、洗練された:
primes = 2 : {- something like [3..] but only the ones that are prime -}
さて、これは人が認識することを学ぶ必要があるパターンが現れるポイントです。これは確かに述語filter
、すなわち素数によって編集されたリストです(素数をチェックする方法がまだわからなくても、論理構造が重要なポイントです(そして、私たちは確かに素数のテストが可能です!))。これにより、より多くのコードを記述できます。
primes = 2 : filter isPrime [3..]
見る?ほぼ完了です。3つのステップで、かなり複雑な問題を減らし、残りの記述は非常に単純な述語だけになります。繰り返しますが、擬似コードで書くことができます:
isPrime n = {- false if any number in 2..n-1 divides n, otherwise true -}
そしてそれを洗練することができます。これはすでにほとんどhaskellなので、簡単すぎます。
isPrime n = not (any (divides n) [2..n-1])
divides n p = n `rem` p == 0
まだ最適化を行っていないことに注意してください。たとえば、偶数が素数ではないことがわかっているので、奇数だけを含むようにすぐにフィルタリングされるリストを作成できます。さらに重要なのは、isPrimeで試す必要のある候補者の数を減らしたいということです。そして、ここでは、いくつかの数学的知識が必要です(もちろん、これをC ++またはJavaでプログラムした場合も同様です)。これは、n
テストしているものが素数で割り切れるかどうかを確認するだけで十分であり、二乗が。より大きい素数による除算性をチェックする必要はありませんn
。幸いなことに、私たちはすでに素数のリストを定義しており、そこから候補のセットを選ぶことができます!私はこれを運動として残します。
標準ライブラリと、セクションやリスト内包表記などの構文糖衣構文の使用方法を後で学び、徐々に独自の基本関数を書くことを諦めます。
後で、命令型プログラミング言語で何かをやり直さなければならないとき、無限のリスト、高階関数、不変データなどなしでは生きていけないことに気付くでしょう。これは、Cからアセンブラーに戻るのと同じくらい難しいでしょう。
楽しむ!