ページ 1 / 1
ラムダ式はいらない子?
Posted: 2018年9月08日(土) 20:15
by 遊び人
お世話になっております。
C++11にて、ラムダ式なるものが追加されました。
「猫でもわかるC++プログラミング」には、述語関数で利用するとちょっぴり便利などと言うようなことがかかれていますが、正直私ほどのレベルではこれだけのためにわざわざラムダ式を追加する必要があったのかな?と感じてしまいます。わざわざ追加されているということは、何か有用な使い方があるはず!
是非ご教授ください。
コード:
#include <iostream>
#include <algorithm>
#include <vector>
#include <windows.h>
using namespace std;
int main() {
vector<int> v;
vector<int>::iterator s, e;
for (int i = 0; i < 5; i++) // vに値を追加する
v.push_back(i * i);
s = v.begin();
e = v.end();
// 述語関数をラムダ式で記述して、for_eachを呼び出す
for_each(s, e, [](int x) {
if (x % 2 == 0)
cout << x << "は偶数です。" << endl;
else
cout << x << "は奇数です。" << endl;
});
system("pause");
return 0;
}
Re: ラムダ式はいらない子?
Posted: 2018年9月09日(日) 09:43
by Rittai_3D
ラムダ式は「その場でスグに関数が作れる」ところが利点だと思います。
が、「ラムダ式は本当に必要なのか?別に必要ないのでは?」と疑問を抱いている間には有用性に気がつかないものでしょう。
もっと言うと、ラムダ式を使う必要がないと思うならば使わなくても問題がないので、「あ、ここでラムダ式使うと便利だ!」ということがある時までに使わなけば有用性に気づくと思います。
実行結果
https://wandbox.org/permlink/131rlsvBgffvjyYL
► スポイラーを表示
コード:
#include <iostream>
#include <array>
#include <algorithm>
void show_odd_even_function( int v )
{
std::cout << v << ( v % 2 == 0 ? " even" : " odd" ) << std::endl;
}
struct show_odd_even_functor
{
void operator() ( int v )
{
std::cout << v << ( v % 2 == 0 ? " even" : " odd" ) << std::endl;
}
};
int main()
{
std::array< int, 10 > arr {};
for( auto& e : arr ) {
static int t = 0;
e = t++;
}
// 外部の関数を使う場合
std::for_each( arr.begin(), arr.end(), show_odd_even_function );
std::cout << std::endl;
// 関数オブジェクトを使う場合
std::for_each( arr.begin(), arr.end(), show_odd_even_functor() );
std::cout << std::endl;
// ラムダ式を使う場合
std::for_each(
arr.begin(),
arr.end(),
[]( int v )
{
std::cout << v << ( v % 2 == 0 ? " even" : " odd" ) << std::endl;
}
);
}
上のコードを見ていただくと、ラムダ式が一番手っ取り早く書けているのがわかると思います。
述語関数の内容が上のコードのように、外部に関数 or 関数オブジェクトを作るレベルのものでなければ、ラムダ式を使うのが楽です。
また、ラムダ式には名前をつけられるので、同じ関数内で同じ処理をしたい場合は
コード:
void hoge()
{
auto func = []( int v ) { /* 処理する内容 */ };
std::for_each( std::begin( array1 ), std::end( array1 ), func );
std::for_each( std::begin( array2 ), std::end( array2 ), func );
}
のように書け、外部に関数 or 関数オブジェクトを作る必要がなくなります。
Re: ラムダ式はいらない子?
Posted: 2018年9月09日(日) 11:29
by 遊び人
なるほど、確かに短い関数の場合は、外部に関数を作って・・・とするのが少し面倒だとは思っていました。
よくよく考えてみれば、ラムダ式のような「別に無くても作れるけどあると便利だよね」というような機能は他にもたくさんありましたね。
言ってしまえばテンプレートもそれにあたりますし。
Re: ラムダ式はいらない子?
Posted: 2018年9月09日(日) 11:52
by 参照魚
ラムダ式は周囲の変数を式の内部に閉じ込めた関数オブジェクトを簡易に生成する事ができるのが最大の利点と思っています。
コード:
void sample( void ){
int x = 10;
const auto f = [=](){ printf( "%d\n", x ); }; // この時点の値がコピー
x = 20; // その後に変更しても
func(); // 10が表示される
}
生成した関数オブジェクトを保持しておいて、後で別の場所で遅延実行させることもできます。構造体と関数ポインタでも同様の事ができますが、より簡潔に直感的に記述する事ができます。
Re: ラムダ式はいらない子?
Posted: 2018年9月09日(日) 12:09
by 遊び人
変数のように扱える関数という点も面白いですね
Re: ラムダ式はいらない子?
Posted: 2018年9月12日(水) 00:48
by Dixq (管理人)
# 北海道在住のため、地震でしばらくこれませんでした・・。
ラムダ式はStreamAPIと相性がよく、関数型プログラミングを実現させてくれる点も見逃せません。
もし命令型プログラミングと、関数型プログラミングの違いが分からなければ、リンク先をご覧ください。
https://qiita.com/munieru_jp/items/6c0dbada463e00429fd1
以下のコードはリンク先からの引用です。
コード:
List<String> numTextList = Arrays.asList("0", "1", null);
List<Integer> numList = new ArrayList<>();
for(String numText : numTextList){
if(numText == null){
continue;
}
int num = Integer.parseInt(numText);
numList.add(num);
}
今、リストにある、nullを除外した"0"と"1"を取り出してint型のリストに詰めたい時、
従来の命令型プログラミングだとこのように書かなければなりません。
やりたいことは
「nullを除いて"0"と"1"を取り出して」
「文字列をint型に変換して」
「リストに詰める」
ことです。これをこの文章通りにプログラミングできればバグも減り、
コードの可読性も向上するはずですが、命令型プログラミングではそれが困難です。
ここで、StreamAPIとラムダ式を用いて書き直してみます。
コード:
List<String> numTextList = Arrays.asList("0", "1", null);
List<Integer> numList = numTextList.stream()
.filter(s -> Objects.nonNull(s))
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
これを改めてみてみましょう。
「nullではない物をフィルターにかけて」
「文字列をint型に変換して」
「リストに詰める」
ことをしています。
どうでしょう、命令型プログラミングと関数型プログラミングはどちらが分かりやすでしょうか。
実現したいことを文章に起こした通りに実装できていると思えます。
私は
この本を持っていますが、ラムダ式はそれだけで一冊本が書けてしまうほどの使い方があります。
深く知りたければラムダ式に特化した本を読んでみるとよいかと思います。
Re: ラムダ式はいらない子?
Posted: 2018年9月12日(水) 20:24
by 遊び人
なるほど・・・関数型プログラミングを学ぶことで、より理解が深められそうですね!
本気でラムダ式を利用しようとなると、プログラミングの考え方から見直さないと駄目みたいです(笑)
見やすさ、バグの生まれにくさという点で関数型のほうが圧倒的に優れているように見えますし、コーディング時間も短縮できそうで、関数型が流行るのも納得という感じです。
全くの余談ですが、関数型よりも命令型のほうが「俺今めっちゃプログラミングしてる」感はありあすね。
Re: ラムダ式はいらない子?
Posted: 2018年9月12日(水) 21:41
by Dixq (管理人)
"「俺今めっちゃプログラミングしてる」感"を大事にしたい頃は命令型プログラミングをしていてよいと思います。
そのうち、より効率的に可読性高く、かつマルチスレッドセーフにということを考えていくと
自然と関数型プログラミングの考え方に出会うことがあると思うので、
必要だという段階に達した時にまた考えればよいと思います。
JavaだってJava7まではStreamAPIはありませんでしたし、C++もC++11,14まではありませんでした。
それまでは無しでもみんな大規模なシステムを作っていたのです。
しかし、より効率よく、バグの少ないコーディングをするかと突き詰めていくと今とは違うコーディングに行きつきます。(その時それが関数型プログラミングではないかもしれません)
その段階を経るのは悪いことではありません。
色んな段階で色んなコーディングを経験してみてください。
Re: ラムダ式はいらない子?
Posted: 2018年9月14日(金) 10:33
by tk-xleader
C++のラムダ式の話であることを前提に回答しますが、C++のラムダ式は関数オブジェクトの糖衣構文なので、ラムダ式がなくても同等のコードを記述することができます。
ただ、1回しか書かないような述語関数のために一々関数オブジェクトを書くのは面倒ですし、ラムダ式のうち、変数キャプチャを使うような類のものを関数オブジェクトとして記述するというのは分かりづらいコードになりますし面倒ですね。
ラムダ式の方が便利な例をということであれば、最初に挙げられた例のfor_eachの出力先をstd::coutではなく、std::ofstreamで開いたoutput.txtとするようなコードをラムダ式ありと無しで書いてみるといいかと思います。
Re: ラムダ式はいらない子?
Posted: 2018年9月15日(土) 18:11
by 遊び人
一見何が変わるのかわかりませんでしたが、いざ作ってみるとあ~ら不思議。なかなかうまくいかない...
ラムダ式では、キャプチャの機能を使用することで簡単に外部の変数などにアクセスできますが、関数オブジェクト等の場合、for_eachなどのテンプレートでは直接関数の引数を記述することができないため、非常に面倒ったんですね。
ラムダ式の他にも様々な方法がありましたが、どれも非常に分かりづらい&面倒くさいでラムダ式を使わない理由がないと思うほどです。
当たり前のように利用していたキャプチャですが、なくなって初めてその大切さに気付きました。
コード:
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <functional>
#include <Windows.h>
using namespace std;
// グローバル変数
ofstream of2; // output2.txtに書き込む用
// ファイルを開く
void OpenFile(ofstream &of, const char* file_name) {
try {
of.open(file_name);
}
catch (const ofstream::failure e) {
cout << "msg:" << e.what() << endl;
}
}
// 関数オブジェクト
struct Output {
void operator() (int x) {
of2 << x << ((x % 2) ? "は奇数です" : "は偶数です") << endl;
}
};
int main() {
vector<int> v;
vector<int>::iterator s, e;
for (int i = 0; i < 5; i++) // vに値を追加する
v.push_back(i * i);
s = v.begin();
e = v.end();
// 述語関数をラムダ式で記述して、for_eachを呼び出す
ofstream of;
OpenFile(of, "output.txt");
for_each(s, e, [&of](int x) {
of << x << ((x % 2) ? "は奇数です" : "は偶数です") << endl;
});
of.close();
// 述語関数をラムダ式なしで記述
OpenFile(of2, "output2.txt");
for_each(s, e, Output());
of2.close();
system("pause");
return 0;
}
Re: ラムダ式はいらない子?
Posted: 2018年9月16日(日) 00:12
by かずま
遊び人 さんが書きました: ↑5年前
ラムダ式では、キャプチャの機能を使用することで簡単に外部の変数などにアクセスできますが、関数オブジェクト等の場合、for_eachなどのテンプレートでは直接関数の引数を記述することができないため、非常に面倒ったんですね。
それがグローバル変数を使った理由ですか?
関数オブジェクトを次のよう書けばよいのではありませんか?
コード:
#include <iostream> // cout, endl
#include <fstream> // ofstream
#include <vector> // vector
#include <algorithm> // for_each
using namespace std;
int main() {
vector<int> v(5);
for (int i = 0; i < v.size(); i++) v[i] = i * i; // vに値を入れる
auto s = v.begin(), e = v.end();
// 述語関数をラムダ式で記述して、for_eachに渡す
{
ofstream of("output.txt");
if (!of) return cout << "can't create output.txt\n", 1;
for_each(s, e, [&](int x) {
of << x << ((x % 2) ? "は奇数です" : "は偶数です") << endl;
});
}
// 述語関数を関数オブジェクトで記述して、for_eachに渡す
struct Output {
ostream& of;
Output(ostream& o) : of(o) {}
void operator() (int x) {
of << x << ((x % 2) ? "は奇数です" : "は偶数です") << endl;
}
};
ofstream of2("output2.txt");
if (!of2) return cout << "can't create output2.txt\n", 2;
for_each(s, e, Output(of2));
}
元のプログラムで、例外をキャッチしようとしていますが、
ファイル名を "x/output.txt" などにしてオープンできない
ようにしても例外は発生しません。
また、例外をキャッチしたらプログラムは停止しませんから、
オープンできてないファイルへの書き込みを行おうとしますよ。
エラーにはなるけど。
Re: ラムダ式はいらない子?
Posted: 2018年9月16日(日) 10:58
by 遊び人
ファイルをオープンできなかった場合の処理について普通に失念しておりました・・・ハズカシイ
その記述は一番最初に思いつきましたが、下記のようにしていたため、エラーを出しまくって諦めてました・・
コントラスタで引数を取ることで、そのような書き方ができたんですね。
コード:
// 述語関数を関数オブジェクトで記述して、for_eachに渡す
struct Output {
void operator() (ofstream &o, int x) {
of << x << ((x % 2) ? "は奇数です" : "は偶数です") << endl;
}
};