4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

イテレータよりラムダのほうがバグが多いし実装に時間もかかる

Last updated at Posted at 2021-04-18

ラムダ使え!イテレータ滅ぼすべし!

などという主張を時折見かけます。
ラムダのほうがイテレータよりわかりやすく、バグも少ないプログラムが書けるのだそうです。
いいことづくめですね。

ところが先日、全く逆の結果を主張している論文が目に入りました。
果たしてどういうことでしょうか。

ということで以下はAn Empirical Study on The Impact of C++ Lambdas And Programmer Experienceという論文の軽い紹介です。
2016年の論文なので多少古いのですが、まあそんな急に結果がひっくり返ったりはしないでしょう。

本文は超長いので、歴史とか計算方法とか統計的根拠とかのあたりはまるっとスルーしています。
そのあたりも元論文ではしっかり書かれているので、この紹介を見て何か言いたくなったら元論文を読んできてからにしてください。

An Empirical Study on The Impact of C++ Lambdas And Programmer Experience

C++における、ラムダとプログラマの経験に関する実証的研究

Abstract

Java 8やC++ 11といった主流言語でもラムダが使用されるようになってきました。
我々は、C++ 11のラムダが人的要因に与える影響について、イテレータとの無作為化対照実験を行いました。

その結果、ラムダが開発者にとって有益であるかは疑問であり、テスト仕様に沿った正しいプログラムを書けるか、タスクを完了できるかどうかについて、むしろ悪影響があることが示されました。
イテレータを使った場合と比較して、ラムダを使用した場合はより多くのエラーが発生し、そしてコンパイルエラーの解消により多くの時間を費やしました。

Introduction

ソフトウェアエンジニアリングの研究によると、ソフトウェア開発の最大のコスト要因は、開発とメンテナンスであることがわかっています。
従って、開発の生産性を向上させることで、ソフトウェア全体のコストを削減できるでしょう。
ところがデバッグツールや開発ツールなど、技術的な性能に焦点を当てた研究がおこなわれている一方、言語構文自体の使いやすさに関する研究はほとんど行われていません。
Kaijanahoの調査によると、プログラミング言語の無作為化比較試験は、1976年から2011年の間でわずか22件しか発表されていません。
ラムダについての無作為化比較試験は一切見つかりませんでした。

この論文では、ラムダのプログラマーに与える影響を調査するために無作為化比較試験を行います。
実験の参加者として、ネバダ大学ラスベガス校のコンピュータサイエンス部の学生を集めました。
また5年以上の開発経験を持つプロの開発者も参加しました。

本研究の結果として、プログラムを正しく書くのに要した時間に、有意にマイナスの影響があることがわかりました。
また実験結果から、学生とプロ開発者の間では、生産性に関して大きな違いがあることがわかりました。

Experiment

参加者にいくつかのプログラミング課題を解いてもらいました。
参加者は2つのグループに分けられ、片方は課題解決に必ずイテレータを使わなければならず、もう一方は必ずラムダを使わなければなりません。
イテレータは、反復処理を行う際によく用いられる、このような課題を解決するための『普通の』方法です。
今回の実験が反復に焦点を当てているのは、ラムダを使うと反復処理が簡単になるという主張があったからです。
実験にC++を選んだのは、UNLVで最もよく使われている言語だからです。

参加者は54名です。
1年生が10人、2年生が8人、3年生が17人、4年生が7人、そしてプロの開発者が12人です。

Tasks

参加者には4つのプログラム課題を解いてもらいました。
正しく実行するためのメソッドが一部未実装であり、参加者はそのメソッドを埋めてプログラムを完成させます。

Warmup Task

最初の課題は、参加者に課題に慣れてもらうことで、環境が測定に与える影響を抑えることです。
そのため、グループに関わらず同じ課題が与えられました。

最初の課題
# include "task.h"

using namespace std;

/**
 * numbersの全要素をretValに入れるループを実装してください
 */
vector<int> getValues() {
	vector<int> numbers;
	numbers.push_back(1);
	numbers.push_back(5);
	numbers.push_back(65);
	numbers.push_back(21);
	
	vector<int> retVal;
	
	// ここから下を埋める
	// --------
	
	// --------
	
	return retVal;
}

Task 1

ここからは、指示された構造を用いて回答しなければなりません。

グループごとに異なるmarketBasketが与えられ、合計金額を求める関数を記述します。

IteratorグループのmarketBasket
void marketBasket::insert(string itemName, float itemPrice) {
	item newItem;
	newItem.name = itemName;
	newItem.price = itemPrice;
	items.push_back(newItem);
}

marketBasket::iterator::iterator(marketBasket *owner) : owner(owner), index(0) {
}

void marketBasket::iterator::next() {
	index++;
}

bool marketBasket::iterator::hasNext() {
	return owner->items.size() - index > 0;
}

item marketBasket::iterator::get() {
	return owner->items[index];
}

marketBasket::iterator marketBasket::begin() {
	return marketBasket::iterator(this);
}
Iteratorの回答例
float getSum(marketBasket mb) {
	float retVal = 0;
	
	// ここから下を埋める
	// --------
	marketBasket::iterator iter = mb.begin();
	while(iter.hasNext()){
		retVal+=iter.get().price;
		iter.next();
	}
	// --------
	
	return retVal;
}
LambdaグループのmarketBasket
void marketBasket::insert(string itemName, float itemPrice) {
	item newItem;
	newItem.name = itemName;
	newItem.price = itemPrice;
	items.push_back(newItem);
}

void marketBasket::iterateOverItems(function<void (item)> f) {
	for (vector<item>::size_type i = 0; i < items.size(); i++) {
		f(items[i]);
	}
}
Lambdaの回答例
float getSum(marketBasket mb) {
	float retVal = 0;
	
	// ここから下を埋める
	// --------
	function<void (item)> summer = [&] (item it) {
		retVal += it.price;
	};
	mb.iterateOverItems(summer);
	// --------
	
	return retVal;
}

Task 2

与えられたオブジェクトから最小値と最大値を探し、その合計を返すという課題が与えられました。

Iteratorの回答例
float getLowestHighestSum(marketBasket mb) {
	float highest = FLT_MIN;
	float lowest = FLT_MAX;
	
	// ここから下を埋める
	// --------
	marketBasket::iterator iter = mb.begin();
	while(iter.hasNext()) {
		if(iter.get().price > highest)
			highest = iter.get().price;
		if(iter.get().price < lowest)
			lowest = iter.get().price;
		iter.next();
	}
	// --------
	
	return highest + lowest;
}
Lambdaの回答例
float getLowestHighestSum(marketBasket mb) {
	float highest = FLT_MIN;
	float lowest = FLT_MAX;
	
	// ここから下を埋める
	// --------
	function<void (item)> myFunc = [&] (item i){
		if(i.price < lowest){
			lowest = i.price;
		}
		if(i.price > highest){
			highest = i.price;
		}
	};
	mb.iterateOverItems(myFunc);
	// --------
	
	return highest + lowest;
}

Task 3

marketBasketの中から価格が30以下のアイテムを取り出してunder30に入れるという課題が与えられました。

Iteratorの回答例
vector<item> getAllUnder30(marketBasket mb) {
	vector<item> under30;
	
	// ここから下を埋める
	// --------
	marketBasket::iterator marketIter = mb.begin();
	while(marketIter.hasNext()){
		if (marketIter.get().price < 30)
			under30.push_back(marketIter.get());
		marketIter.next();
	}
	// --------
	
	return under30;
}
Lambdaの回答例
vector<item> getAllUnder30(marketBasket mb) {
	vector<item> under30;
	
	// ここから下を埋める
	// --------
	function<void (item)> find = [&] (item i){
		if(i.price < 30)
			under30.push_back(i);
	};
	mb.iterateOverItems(find);
	// --------
	
	return under30;
}

Results

Completion of Tasks

01.png

最初の統計は、成功したタスクの数です。
実験の想定としては、全参加者がタスクを完了することを期待していましたが、実際はそうでもありませんでした。
これは特にラムダグループで顕著です。

1年生は、イテレータでは少なくとも時々タスクを完了することができましたが、ラムダでは誰一人として正答することができませんでした。
学年が上がるほど正答率は増えますが、全ての学年でイテレータのほうが正答率が高くなりました。
プロの開発者は、全員が制限時間内に正答することができました。

Time to completion

ふたつめの統計は、テストケースを解くのにかかった時間です。

以下は全てのタスクを解くまでにかかった時間の平均で、単位は秒です。

02.png

イテレータの参加者はタスクを解くのに平均1047秒かかり、一方ラムダは平均1503秒かかりました。

前述のように1年生は全員がラムダを解けなかったので、最大時間2400秒かかっています。
学年が上がるほど、イテレータ、ラムダともに解くのにかかる時間は短くなりますが、一貫してラムダの方が時間がかかっています。
プロはイテレータに236秒、ラムダに461秒で回答しています。

生産性の差が統計的に有意であり、プロと学生には生産性の差が存在します。

Errors

最後はプログラミング中に発生したエラーの数と、修正に要した時間です。
コンパイル1回で複数のエラーが発生した場合でも、エラーは1回とカウントします。

こちらは発生したエラーの数。

03.png

こちらは発生したエラーを修正するのに要した時間。

04.png

イテレータの方がエラー発生回数が少なく、修正に要する時間も短くなっています。

Discussion

全体として、ラムダとイテレータでは解決したタスクの数に大きな差があることがわかりました。
プロにおいては全て解決したので、解決したタスク数という基準では同等ですが、解決までに費やした時間はラムダの方が多くなっています。

以上より、C++においては、今のところラムダを使うことにメリットがありません。

この理由として、C++におけるラムダ構文選択が適切ではないという可能性が考えられます。
例としてfunction<float (int, int)>のような構文は筆者の知る限り他のどこにも使われておらず、C++のテンプレートの知識を持たないユーザも、持つユーザも混乱しがちです。

またエラーメッセージも適切ではありません。
error: member reference base type "int" is not a structure or unionを理解できる人は少ないでしょう。

感想

あくまでC++のラムダについての調査であり、必ずしも他のスクリプト言語や関数型言語などにまで一律拡大適用できるものではないということには注意が必要です。

とはいえ実際、他の言語においては、このような学術的調査が行われたことはありませんよね?
ラムダを使えば本当にわかりやすく、バグも少ないプログラムが書けるのでしょうか?
本当に統計的に有意な結果があるのでしょうか?
関数型信者が言ってるだけだったりしない?

そんなわけでJavaScriptなど他の言語においても、ぜひ同様の調査をやってみてほしいところですね。

4
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?