私は長年、主に命令型言語(C++、C#、javascript、python)でプログラミングしてきましたが、最近、いくつかの関数型言語(LISP、Haskell)を試し、宣言型プログラミングのアイデアのいくつかを適用することに興奮しました。 C++。私はしばらく前に書いたカスタムの範囲ベースのSTL置換ライブラリを持っているので、かなりクリーンな方法で多くのことが可能になりました。
次に例を示します。大文字と小文字を区別せずに、ターゲット部分文字列がソース文字列内に存在するかどうかを確認する関数です。最初に平凡な命令的な方法:
bool StringContains(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
for (size_t i = 0; i <= endIndex; i++) {
// Check if target is here
size_t strPos = i;
bool foundHere = true;
for (char targetChar : target) {
char strChar = tolower(source[strPos]);
targetChar = tolower(targetChar);
if (strChar != targetChar) {
foundHere = false;
break;
}
strPos++;
}
// If found here, return true
if (foundHere) {
return true;
}
}
// If not found by now, return false
return false;
}
そして、ここでは私の宣言的なライブラリ(いくつかのC++ 11マジックを使用しています)を使用しています。
bool StringContainsDec(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
auto targetRange = All(target) | Transformed(tolower);
for (size_t i = 0; i <= endIndex; i++) {
// If found here, return true
auto sourceRange = All(source) | Sliced(i, i + target.size()) |
Transformed(tolower);
if (RangesMatch(sourceRange, targetRange)) {
return true;
}
}
// If not found by now, return false
return false;
}
もう少しコンパクトで、おそらく英語のようで読みやすい、それはニースです。 「|」シェルスクリプトパイプに似ており、値を次の操作にルーティングします。そう:
All(source) | Sliced(i, i + target.size()) | Transformed(tolower)
つまり、反復されると、「ソース」の各文字を受け取り、インデックスiとi + target.size()の間でスライスされ、各文字をtolower()に渡す範囲を設定します。
RangesMatch()は2つの範囲のそれぞれを反復し、各要素が一致する場合にtrueを返します。
だから、それはすべてうまくいくし、正しく動作する。しかし、時間の経過とともに、実際の状況でこのアプローチを実験してきました。
今、これらすべては私の実装や私の好みに固有のものかもしれませんが、その一部はスタイルにも固有のものだと思いますか?この文字列関数はほんの一例ですが、両方を並べて実装すると、あらゆる種類の関数が見つかります-時間の80%の命令型スタイルが私に有利です。単純な古いループとifステートメントではなく、より高次の関数、map/reduceなどをいじり回します。テキストエディターに問題がある場合は、コードが簡潔になり、入力が少し少なくなる可能性がありますが、実際の複雑な状況では、混乱して保守が難しくなります。
宣言的な過大評価ですか?特に関数型言語での実際の複雑なプロジェクトで、両方のアプローチについて幅広い経験を持っている人はいますか?他の人の意見を知りたいです。
宣言型コードはデバッグが困難です。
それはデバッガの品質の関数だと思います。デバッガーが命令構造を理解しているが宣言構造を理解していない場合、もちろん、宣言構文はデバッグが難しくなります。ただし、優先順位が異なる別のデバッガを簡単に想像できますが、その逆のことが当てはまります。
are一部の言語デザイナーdoはツールに非常に関心があるため、ツール性が言語設計に影響を与えたり、優れたツールを促進するために言語機能に妥協したりすることさえできます。明らかな例は、ツールベンダー(JetBrains)によって設計されたKotlinです。 Scalaの主要な開発者は、Scalaの型推論を拡張することにも反対しています。その方法がわからない(そうする)か、実装するのが難しいためです(しかし、彼らは賢いコンパイラライターを持っています)が、それを実装する方法を理解していないため適切なエラーメッセージを表示(90年代半ばのC++テンプレートのインスタンス化エラーを考えてください。)
宣言的なコードは少し遅いです。 […]宣言型を使用すると、コードが実際に行っていることを簡単に失う可能性があります。
はい。それがポイント全体です。 呼び出された "宣言的"なのはそのためです。-whatではなく、-howと宣言したためです。
これにより、コンパイラが物事を最適化するためのlot余裕ができます。
Supero(Haskellのスーパーコンパイラー)の論文の1つに優れた例があります。著者は、Haskellでの単純で表現力豊かな宣言型の純粋に機能的な1行の単語カウント関数を比較します(main = print . length . words =<< getContents
)、Supero、GHC、およびYHCとCで手動で最適化されたステートマシンベースのwhile
ループの組み合わせでコンパイルされ、Haskellの方がわずかに高速であることがわかっています。どうしてそうなるのでしょうか?まあ、コンパイラは実際にHaskellコードを手書きのCバージョンと同じステートマシンループに変換しましたが、C(少なくともインラインアセンブリなし)ができない追加のトリックを1つ実行できます:状態をエンコードします)プログラムカウンタ内。
あなたの場合は、宣言型DSLを作成します(作成する場合)。ただし、C++オプティマイザーはDSLのセマンティクスについて何も認識していないため、追加の自由度を利用できません。
宣言的なコードは、時間の経過とともに変更するのが困難であると私は思います。各文字に追加の操作を行いたい場合は、別の[…]ラムダなどを追加する必要があります。命令型プログラミングでは、ループ[…]内に単純なol 'コードの行を追加するだけです。
私はついていません。本当に違いはありますか:
step1();
step2();
step2a(); // inserted later
step3();
そして
transform1 |
transform2 |
transform2a | // inserted later
transform3;
私が書いているとき、私は命令スタイルがより直感的であると思います。
「直感的」な(絶対的な)ものはありません。直感性は親しみやすさのすべてです。スコッティがweで直感的なユーザーインターフェイスと見なすものを備えたコンピューターを使おうとしたときに、スタートレックムービーを覚えていますか?彼は最終的にマウスに音声コマンドを話そうとする。
多くの人々は、ループは直感的であり、再帰は直感的ではないと考えています。しかし、ほんの数か月前に、次のようなコードを書いた完全なプログラミング初心者から、StackOverflowのRubyタグに質問がありました。
def main
# do something
main
end
彼にとって、thisは、何かを何度も繰り返す直感的な方法でした。 (そして、なぜですか?「何かをしてから、あなたがしていることをやり直してください」は、私たちの命令型の人が「ループ」と呼ぶもののための完全に賢明なメンタルモデルです。そうではありませんか?)そして、Scheme、ML、またはHaskellプログラマーにとって、thisは直感的であり、ループは直感的ではありません。 (実際、pure MLまたはHaskellプログラマーは、彼らの言語ループがないであるため、私たちが何を話しているのかさえ知りません。)
私の個人的な別の例:Rubyプログラマーであり、Smalltalkのファンとして、なぜ静的AOTコンパイラーが必要なのか理解できません。それでも、C++コミュニティーは誰もが必要な理由を理解できません。動的JITコンパイラ。
両方のスタイルで同じ量の(深刻で、おもちゃではなく、複雑で、大規模な製品レベルのシステム)コードを記述しない限り、より馴染みのあるスタイルはより「直感的」になります。それはまさに物事の本質です。
IMHO宣言型プログラミングはveryがあいまいです。
特に、国によって(フランスでは米国と同じではありません)、時期によって( AI冬 の前後に)意味が異なります。
それについての私の理解は、宣言的知識についてより賢く話す Jacques Pitrat のそれです:
知識は宣言的な形で提供しますが、使用方法は含まれていません。
ある意味では、宣言型プログラミングは用語で矛盾しています。プログラミングは、物事を行う方法についてコンピューターを構成することと見なすことができますが、宣言的とは、システムが方法で物事を行う方法を見つける必要があることを意味します。どのようにそれを実行するかではなく、何を実行したいか。
宣言的な知識をシステムが理解したら、「プログラミング」は非常に簡単です。多くの宣言的な知識を個別に与えます(方法に関する宣言的なメタナレッジを含む)宣言的知識をコンパイルして使用するため)およびいくつかの目的と目標。これはいくつかの [〜#〜] agi [〜#〜] システムの夢でもあり、J.Pitratはこれらについて多くのことを書いています。
そして、プログラミングは、ソースコードを書くという概念に拡張される可能性があります。開発者は、何らかのシステムによって理解される形式化を記述します。そのソースコードは、開発者にとって好ましい形式化です(これはフリーソフトウェア愛好家のためのソースコードの定義です)。
実際、宣言的知識と手続き的知識の間には継続的なスペクトルがあります。
したがって、IMHO関数型プログラミングは厳密に宣言型プログラミングではありませんが、実際に関数型言語は手続き型プログラミングよりもmore宣言型です。また、宣言型システムは過大評価されていませんが、それらを開発するには数十年は必要です(read the mythical man month )
実用的な短期的見解では、宣言型プログラミングは単にコードよりもデータ(「目標」を与える「宣言型」構成データを含む)を優先することを意味する場合があります。
エキスパートシステム についてもお読みください。
PS。 J.Pitratのブログ と本を読むことを強くお勧めします。彼は生涯を宣言的知識に捧げました。