質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
関数型プログラミング

関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

Q&A

解決済

3回答

5531閲覧

関数型プログラミングについて教えてください。

moredeep

総合スコア1507

関数型プログラミング

関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

1グッド

2クリップ

投稿2016/10/04 01:43

編集2016/10/04 12:34

お客さんが最近関数型プログラミングに興味を持っているため、
私もいろいろ調べているのですが、疑問しか出なかったので質問させてください。

疑問は主には以下3点です。

  1. 副作用とはなにか

  2. 関数型の言語(Haskellとか)を使う利点とは

  3. 関数型で情報の蓄積は可能なのか

  4. 副作用とはなにか


関数型のメリットに、副作用がなくなるというのが、いろいろなサイトで言われています。
しかし、その副作用の説明がバラバラで、結局何が言いたいのかがまったくわかりません。

日本語としては、
副作用 = 副次的な作用
なので、普通に考えれば以下①~⑥のようになる認識です。

C#

1using System; 2 3namespace CSharpTes 4{ 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int begin = 1; 10 int end = 100; 11 12 A classA = new A(); 13 for (int i = begin; i <= end; i++){ 14 classA.add(i); 15 } 16 17 B classB = new B(); 18 for(int i = begin; i <= end; i++){ 19 classB.add(i); 20 } 21 22 C classC = new C(); 23 24 Console.Out.WriteLine(classA.sum()); 25 Console.Out.WriteLine(classB.getSum()); 26 Console.Out.WriteLine(classC.add(begin, end)); 27 Console.ReadKey(); 28 } 29 } 30 31 // リストもどき 32 class A{ 33 _A data; 34 public A() 35 { 36 // 条件分岐を減らすために入れている 37 // これは今回の論点ではありません。 38 data = new _A(0); 39 } 40 public void add(int num){ 41 _A newData = new _A(num); // ① データの追加なので作用 42 getEndData().next = newData; // ② それまでの最後尾のリストのデータが変わるため副作用 43 } 44 private _A getEndData() { 45 _A current = data; 46 while (current.next != null){ 47 current = current.next; // ③ 再代入だが副作用とは言えない 48 } 49 return current; 50 } 51 public int sum(){ 52 _A current = data; 53 int sum = 0; 54 while (current.next != null){ 55 current = current.next; // ④ ③と同じ処理だがこっちは副次的なものなので副作用と言える 56 sum += current.num; // ⑤ 合計するのが関数の主旨なので作用 57 } 58 return sum; 59 } 60 }; 61 62 class _A{ 63 public int num; 64 public _A next; 65 public _A(int num){ 66 this.num = num; 67 } 68 69 }; 70 class B{ 71 int sum; 72 public void add(int num){ 73 sum += num; // ⑥ 合計するのが関数の主旨なので作用 74 } 75 public int getSum() 76 { 77 return sum; 78 } 79 }; 80 81 // 関数型? 82 class C{ 83 // 状態の変化は起こらない? 84 public int add(int begin, int end){ 85 if(begin >= end){ 86 return begin; 87 } 88 return begin + (add(begin + 1, end)); 89 } 90 }; 91}

また、「IO処理は副作用」という記述を見ましたが、
「IO処理の際にファイルポインタの移動等が発生するから副作用が多い」
という意味であっているでしょうか。

①~⑥、およびIO処理の認識が
あっているのか間違っているのか、理由を添えて教えていただきたく。

  1. 関数型の言語(Haskellとか)を使う利点とは

これまで色々な技術を見て、そのたびに「こういうことに使えそう」、
「あの案件の時に使えていればよかった」等思いました。
(手続き型からオブジェクト指向への移行時や、MVC、MVVMを知った時等々)

しかし、今回関数型のことを調べても、利点がまったく思いつきません。

色々なサイトに、状態の変化がなくなるためにバグが減る、
とありますが、チラっと実装した結果、私にはそのようには思えませんでした。

C等の言語を関数型に置換する際、再帰処理が散見されますが、
そもそもC等の言語は、「再帰処理は複雑なため、バグが多くなりやすく、メンテナンス性も悪くなるので控える」
みたいな流れがあったと思います。
(まぁ、昔はメモリも少なかったというのもありますが、それは瑣末な問題です。)

アルゴリズムが完璧にかければ減るのでしょうが、
関数型の書き方が人間には複雑に見え、実装時のポカミスが増える気がします。

  1. 関数型で情報の蓄積は可能なのか

ここだけ実装の話になってしまいますが、
再代入が許可されていないHaskell等の関数型で情報の蓄積は可能なのでしょうか。
(DBやファイルに書き込むという話はナシで)

以上

回答を頂いても反論してしまうと思いますが、お付き合いいただければと思います。

回答を受け追記

今のところの私の理解を追記します。

  1. 副作用とは

関数外に影響を及ぼす操作
→関数外までスコープが及ぶ変数の読み書き、ファイルの読み書き

  1. メリット

ある程度複雑な対話が必要なシステムには不向きだが、
数学の関数ように入力→出力のみのプログラムに向いている

  1. 情報の蓄積

その時々によって情報を書き換える必要があるため関数型には不向き
しかし実装は可能

htsign👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

###1. 副作用とは

「副作用」は、「参照透過性」と対立するものと考えてください。

置きかえて考えるとたとえば、グローバル変数はなるべく排除しますよね。
スコープが広くなると、可能な状態が増えて、結果バグも増えるからです。

じゃあ、空間的だけでなく時間的にも、可能な状態を減らせばバグが減る。
これが参照透過、あるいは「不変」の考え方です。

数学関数は時間によって状態が変わりません。たとえば、
三角関数の入出力が、株価みたいにタイミングで変わらないですよね。

関数型言語では、C言語などの関数と違い、数学の関数に近い使い方をします。


###2. 関数型言語の利点

関数型のことを調べても、利点がまったく思いつきません

単純に、関数型言語は関数型プログラミングに向いています

C等の言語を関数型に置換する

C言語でも関数型プログラミングは可能(Cが関数型言語ではないこととは別問題。
また、もっと極端に言えば、アセンブリや機械語でも原理的には可能)ですが、
それは「C言語でもOOPは可能」なことの延長線上に過ぎません。

C言語でクラスを作ったりするのは車輪の再発明ですよね。
だから普通は、JavaやC#などのOOP言語を使う。それと同じことです。


関数型の書き方が人間には複雑に見え、実装時のポカミスが増える

これはデータベースのクエリ言語であるSQLを考えると分かります。
SQLは宣言型言語なので、関数型に近い位置にいます。

SQLは手続き型の言語と違うので、人間が理解しづらい面もありますが、
といって今さら、アレはもう手続き型で冗長に書きたくないですね。

SQLの延長線上に、C#のLinqがありますし、さらにその先には、
Rubyなどが持つ「map」「reduce」などの高階関数的メソッドがあります。

つまり、集合的な操作が適している部分は、非常に簡潔に書けます。これが利点。

###3. 関数型での情報の蓄積

関数型プログラミングでは、不変性を守るため再代入を制限します。

だから、手続き型がループで表現する状態の変化は、
再帰中の関数の入出力の変化で表現します。

関数型言語のモデルであるラムダ計算と、
命令型言語のモデルであるチューリングマシンには、互換性があるので、
関数型だからできない、という処理は原理的にありません

だからたとえば、ポール・グレアムがやったように、
普通のWebサービスを作れます


それを踏まえた上で、ただし、向き不向きはあると思います。

ステートフルな処理ならオブジェクト指向、
ステートレスな処理なら関数型プログラミング、
もしくはGUIとCUI、という風に私は使い分けてます。

ステートマシンそのものでGUIが本質のゲームを作るなら、
(現に普及具合を見れば分かるように)今でもOOPが向いてます。

私がLispのような関数型(またはPrologのような論理型)言語を使いたいのは、
たとえば、入出力の連鎖で構成しやすい、自然言語処理のような記号計算です。
そして、Webサービスも文字列(HTML)出力なので、この記号計算に近い分野だと見ています。

結局、プログラミングに銀の弾丸は無く、すべて適材適所だと思います。

投稿2016/10/04 04:48

LLman

総合スコア5592

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

moredeep

2016/10/04 12:37

回答ありがとうございます。 ふわっとですが、わかった気になれました。 現状私が理解した内容を追記しましたので、問題ないか見てもらえると幸いです。
LLman

2016/10/04 17:56

>関数外に影響を及ぼす操作 「関数内」でも、後に影響を及ぼす操作を排除します。 関数型だと、ある関数が引数1に対して2の戻り値を返していたのが、 3を返すようにはしないのです。つまり、手続き型だと当たり前だった、 関数の状態を変化させることをしないのです。だから「不変」なんです。 それでは状態の変化をどうやって表すかといえば、引数を変化させます。
moredeep

2016/10/06 01:39

>>関数外に影響を及ぼす操作 >「関数内」でも、後に影響を及ぼす操作を排除します。 浅学ですみません。 スコープが関数内に限定されている変数は、次回に関数が呼ばれる時はその時の情報は持っていないのではないでしょうか。 オブジェクト指向で考えると、(IOは別で書いているのでIOを除くと、)メソッドの振舞いを時によって変えるには、 クラス変数やグローバル変数などの、関数スコープ外の変数を変える必要があるように思えます。 何か見落としがあるのでしょうか。
LLman

2016/10/06 04:13 編集

>メソッドの振舞いを時によって変えるには、 >関数スコープ外の変数を変える必要がある たとえば、Javaの文字列(String)は不変ですよね。 そこで、「Javaでは文字列を変えられない」という事態は起こらず、 別の新しい文字列を作ることで、プログラム全体として、状態を変化させられます。 それと同じで、単体の変数や関数は不変でも、 再帰と条件分岐と関数の入出力の変化によって、 プログラム全体としては、状態の変化を扱えます。 関数が不変というのは、同じ入力に対しては、同じ出力を返すということですが、 別の入力に対しては、別の出力を返せるので、それによって状態を変化させられます。
guest

0

ちょうど自分自身、関数型言語に取り組もうとしているところで、実用したことはないのですが、理解の範囲でコメントします。

その1 「副作用」について

まず、「副作用」という言葉で混乱しやすいのですが、「メインの動作」に対する副作用、という意味ではありません

まずは、対義語になる「純粋な関数」というものを考えます。これは「同じ引数を与えれば常に同じ結果を返す」というものです。つまり、そのような状態を破る

  • 関数の実行を超えて生き残る変数に値を読み書きする
  • 他の純粋でない関数を呼ぶ
  • 入出力を行う

などの行為がすべて副作用です。1・3・4・5はローカル変数に値を書き込むだけなので副作用ではありません(外から見れば、どう実装していても関係ないですし)。

その2 純粋関数型のメリットについて

React.jsでも「純粋な関数」が求められたり、それ以外の箇所でも「冪等性」を要求されるような場面が存在しています。そもそも状態の存在しない純粋関数型言語であれば、普通に書くだけで自動的に「純粋な関数」が出来上がります(参考)。

あと、Haskellの場合は型が厳格なので、「実装時のポカミス」は多くがコンパイルを通りません。再帰も、末尾再帰をループに変換する最適化で、極端なスタックの消費をせずに済みます。

その3 情報の蓄積について

プログラム内では、変数は再代入できませんので、再帰によって「同じ関数に別な引数を与えて実行する」ことで状態が移り変わっていきます。

乱数ジェネレーターの場合、

「乱数生成関数(初期状態の乱数ジェネレーター)」→「1つ目の乱数と「1つ生成した乱数ジェネレーター」を返す」
「乱数生成関数(1つ生成した乱数ジェネレーター)」→「2つ目の乱数と「2つ生成した乱数ジェネレーター」を返す」

というような流れとなっています。

投稿2016/10/04 02:38

maisumakun

総合スコア145184

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

moredeep

2016/10/04 12:35

回答ありがとうございます。 ふわっとですが、わかった気になれました。 現状私が理解した内容を追記しましたので、問題ないか見てもらえると幸いです。
guest

0

3.についてです

コード

haskell

1echo ls = do 2 a <- getLine 3 if a == "" then return () 4 else do print (a:ls) 5 echo (a:ls) 6 7 8main = echo []

入力

1 2 3 4

出力

["1"] ["2","1"] ["3","2","1"] ["4","3","2","1"]

投稿2016/10/04 02:12

編集2016/10/04 02:14
ozwk

総合スコア13521

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問