重複の可能性:
関数型プログラミングとオブジェクト指向プログラミング
OOPの代わりに関数型プログラミングが必要な理由を誰かが説明してくれますか?
たとえば、なぜ C++ (または同様の言語) の代わりに Haskell を使用する必要があるのでしょうか?
OOP に対する関数型プログラミングの利点は何ですか?
重複の可能性:
関数型プログラミングとオブジェクト指向プログラミング
OOPの代わりに関数型プログラミングが必要な理由を誰かが説明してくれますか?
たとえば、なぜ C++ (または同様の言語) の代わりに Haskell を使用する必要があるのでしょうか?
OOP に対する関数型プログラミングの利点は何ですか?
関数型プログラミングで私が気に入っている大きな点の 1 つは、「離れた場所での不気味なアクション」がないことです。あなたが見るものはあなたが得るものであり、それ以上のものではありません. これにより、コードの推論がはるかに容易になります。
簡単な例を使ってみましょう。X = 10
Java (OOP) または Erlang (機能)のコード スニペットに出くわしたとします。Erlang では、これらのことをすぐに知ることができます。
X
は、私がいる直接のコンテキストにあります。ピリオド。それは、私が読んでいる関数に渡されたパラメーターか、最初の (そして唯一の、以下を参照) 割り当てられているパラメーターのいずれかです。X
この時点から変数の値は 10 になります。私が読んでいるコードのブロック内で再び変更されることはありません。できない。Java では、より複雑です。
X
はパラメーターとして定義される場合があります。X
これは、コードを常に逆方向にスキャンして、明示的または暗黙的に (for ループのように) 割り当てまたは変更された最後の場所を見つけないと、 の値がどうなるかわからないことを意味します。X
たまたまクラス変数である場合、そのメソッドのコードを検査せずにこれを知る方法がなく、私の下から変更される可能性があります。X
身近な環境では見えない何かによって変わる可能性があります。別のスレッドが を変更する #5 のメソッドを呼び出している可能性がありますX
。Java は比較的単純なOOP 言語です。X
C++ でねじ込むことができる方法の数はさらに多く、潜在的にあいまいです。
そして事は?これは、一般的な操作が関数型言語よりも OOP (または他の命令型) 言語の方がはるかに複雑になる可能性があることを示す単純な例です。また、高階関数のような可変状態などを含まない関数型プログラミングの利点にも対応していません。
Haskell について私が本当にクールだと思う点が 3 つあります。
1) 静的に型付けされた言語であり、非常に表現力があり、メンテナンスとリファクタリングが容易なコードをすばやく構築できます。Java や C# などの静的に型付けされた言語と、Python や Ruby などの動的言語の間で大きな議論がありました。Python と Ruby を使用すると、Java や C# などの言語で必要な行数のほんの一部を使用して、プログラムをすばやく作成できます。したがって、市場に迅速に参入することが目標である場合は、Python と Ruby が適しています。しかし、それらは動的であるため、コードのリファクタリングと保守は困難です。Java では、メソッドにパラメーターを追加する場合、IDE を使用してメソッドのすべてのインスタンスを見つけて修正するのは簡単です。1 つでも見逃した場合は、コンパイラがそれをキャッチします。Python や Ruby では、リファクタリングのミスは実行時エラーとしてキャッチされるだけです。したがって、伝統的な言語では、開発が迅速で保守性が低い一方で、開発が遅く保守性が優れているかのどちらかを選択できます。どちらの選択もあまり良くありません。
しかし、Haskell では、このような選択をする必要はありません。Haskell は、Java や C# と同様に静的に型付けされます。そのため、すべてのリファクタリング機能、IDE サポートの可能性、およびコンパイル時のチェックを利用できます。しかし同時に、型はコンパイラによって推論できます。そのため、従来の静的言語のように邪魔になることはありません。さらに、この言語には他にも多くの機能があり、数行のコードで多くのことを達成できます。したがって、静的言語の安全性とともに、Python と Ruby の開発のスピードを得ることができます。
2) 並列性。関数には副作用がないため、開発者が多くの作業をしなくても、コンパイラが並列に実行するのははるかに簡単です。次の擬似コードを検討してください。
a = f x
b = g y
c = h a b
純粋な関数型言語では、関数 f と g に副作用がないことがわかっています。したがって、g の前に f を実行しなければならない理由はありません。順序を入れ替えたり、同時に実行したりすることができます。実際、関数 h で値が必要になるまで f と g を実行する必要はまったくありません。f と g の呼び出しには副作用があり、特定の順序で実行する必要があるため、これは従来の言語には当てはまりません。
コンピューターに搭載されるコアの数が増えるにつれて、関数型プログラミングはより重要になります。これにより、プログラマーは利用可能な並列処理を簡単に利用できるようになるからです。
3) 最後に、Haskell の本当にクールな点は、おそらく最も微妙なところです: 遅延評価です。これを理解するために、テキスト ファイルを読み取り、ファイルの各行に単語 "the" が出現する回数を出力するプログラムを作成する問題を考えてみましょう。伝統的な命令型言語で書いているとします。
試行 1: ファイルを開き、一度に 1 行ずつ読み取る関数を作成します。各行について、「the's」の数を計算し、それを出力します。メイン ロジック (単語のカウント) が入力と出力に密接に結合されていることを除けば、これは素晴らしいことです。同じロジックを別のコンテキストで使用するとします。ソケットからテキスト データを読み取り、単語を数えたいとします。または、UI からテキストを読みたいですか? ロジックを最初から書き直す必要があります。
最悪の場合、新しいコードの自動テストを作成したい場合はどうすればよいでしょうか? 入力ファイルを作成し、コードを実行し、出力をキャプチャしてから、出力を期待される結果と比較する必要があります。それは可能ですが、それは苦痛です。一般に、IO をロジックと密結合すると、ロジックのテストが非常に難しくなります。
試行 2: では、IO とロジックを分離しましょう。まず、ファイル全体をメモリ内の大きな文字列に読み込みます。次に、文字列を行に分割し、各行の「the」をカウントして、カウントのリストを返す関数に文字列を渡します。最後に、プログラムはカウントをループして出力できます。コア ロジックは IO を含まないため、簡単にテストできます。ファイル、ソケット、または UI からのデータを使用して、コア ロジックを簡単に使用できるようになりました。それで、これは素晴らしい解決策ですよね?
違う。誰かが 100 GB のファイルを渡した場合はどうなりますか? ファイル全体を文字列にロードする必要があるため、メモリを使い果たします。
試行 3: ファイルの読み取りと結果の生成に関する抽象化を構築します。これらの抽象化は、2 つのインターフェイスと考えることができます。最初のメソッドには、nextLine() および done() メソッドがあります。2 つ目は outputCount() です。メイン プログラムは nextLine() と done() を実装してファイルから読み取りますが、outputCount() はカウントを直接出力するだけです。これにより、メイン プログラムをコンスタント メモリで実行できます。テスト プログラムでは、nextLine() と done() がテスト データをメモリから取得し、outputCount() が結果を出力するのではなく結果をチェックする、この抽象化の代替実装を使用できます。
この 3 番目の試行は、ロジックと IO を分離するのにうまく機能し、プログラムを一定のメモリで実行できるようにします。ただし、最初の 2 つの試行よりもはるかに複雑です。
要するに、従来の命令型言語 (静的であれ動的であれ) では、開発者はしばしばどちらかを選択することになります。
a) IO とロジックの密結合 (テストと再利用が難しい)
b)すべてをメモリにロードします(あまり効率的ではありません)
c) 抽象化の構築 (複雑で、実装が遅くなる)
これらの選択肢は、ファイルの読み取り、データベースのクエリ、ソケットの読み取りなどの際に出てきます。たいていの場合、プログラマーはオプション A を好むようで、結果として単体テストに問題が生じます。
では、Haskell はこれにどのように役立つのでしょうか? Haskell では、試行 2 とまったく同じようにこの問題を解決します。メイン プログラムは、ファイル全体を文字列にロードします。次に、文字列を調べてカウントのリストを返す関数を呼び出します。次に、メイン プログラムがカウントを出力します。コア ロジックは IO から分離されているため、テストと再利用が非常に簡単です。
しかし、メモリ使用量はどうですか?Haskell の遅延評価がそれを処理してくれます。したがって、コードがファイルの内容全体を文字列変数にロードしたように見えても、実際には内容全体がロードされていません。代わりに、ファイルは文字列が消費されるときにのみ読み取られます。これにより、一度に 1 つのバッファを読み取ることができ、プログラムは実際には定数メモリで実行されます。つまり、このプログラムは 100GB のファイルで実行でき、メモリの消費量はごくわずかです。
同様に、データベースにクエリを実行し、膨大な行セットを含む結果リストを作成し、それを関数に渡して処理することができます。処理関数は、行がデータベースからのものであることを認識していません。そのため、IO から分離されています。そして、内部では、行のリストが遅延して効率的にフェッチされます。そのため、コードを見るとそのように見えますが、行の完全なリストがすべて同時にメモリにあることはありません。
最終結果として、データベースにまったく接続しなくても、データベース行を処理する関数をテストできます。
遅延評価は非常に微妙で、その力を理解するにはしばらく時間がかかります。ただし、テストと再利用が容易な、素晴らしくシンプルなコードを作成できます。
最終的な Haskell ソリューションとアプローチ 3 の Java ソリューションを次に示します。どちらも一定のメモリを使用し、IO を処理から分離するため、テストと再利用が容易になります。
ハスケル:
module Main
where
import System.Environment (getArgs)
import Data.Char (toLower)
main = do
(fileName : _) <- getArgs
fileContents <- readFile fileName
mapM_ (putStrLn . show) $ getWordCounts fileContents
getWordCounts = (map countThe) . lines . map toLower
where countThe = length . filter (== "the") . words
ジャワ:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
class CountWords {
public interface OutputHandler {
void handle(int count) throws Exception;
}
static public void main(String[] args) throws Exception {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(new File(args[0])));
OutputHandler handler = new OutputHandler() {
public void handle(int count) throws Exception {
System.out.println(count);
}
};
countThe(reader, handler);
} finally {
if (reader != null) reader.close();
}
}
static public void countThe(BufferedReader reader, OutputHandler handler) throws Exception {
String line;
while ((line = reader.readLine()) != null) {
int num = 0;
for (String word: line.toLowerCase().split("([.,!?:;'\"-]|\\s)+")) {
if (word.equals("the")) {
num += 1;
}
}
handler.handle(num);
}
}
}
Haskell と C++ を比較すると、関数型プログラミングではデバッグが非常に簡単になります。なぜなら、C や Python などで見られるような変更可能な状態や変数がないためです。何度評価しても同じ結果を返します。
OOP はあらゆるプログラミング パラダイムと直交しており、FP と OOP を組み合わせた言語があり、OCamlが最も人気があり、いくつかの Haskell 実装などがあります。