美しいプログラムを書く(脱添字職人編)
では添字が多用され読み難くなっているソースコードのリファクタリングを通して
美しいプログラムを書くためのポイントをいくつか紹介しました。
そこでは
ということを述べました。
確かにごもっともな主張ではありますが、経験値の少ない人からすれば
「そんなことを言われてもどんなライブラリ関数があってどういう場面で使えるのかリファレンスを読んでもよく分からないし……」
ということはよくあります。
という訳で、簡単な例題を通して、
ライブラリ関数を適切に使えばプログラムを簡潔かつ明瞭にできることを実際に体験していきましょう。
どのようなプログラミング言語やアプリケーションの作成でも
「ちょっとした数のデータを相手に何か処理を行う」(=リスト処理を行う)
機会は頻繁にありますので、今回はリスト処理ライブラリを使うことにします。
class Language
{
public String Name {get; set;}
public Int32 AppearedIn {get; set;}
public IEnumerable<String> TypingDiscipline {get; set;}
}
var languages = new [] {
new Language {
Name = "C#",
AppearedIn = 2001,
TypingDiscipline = new [] {
"Dynamic",
"Nominative",
"Safe",
"Static",
"Strong",
},
},
new Language {
Name = "Haskell",
AppearedIn = 1990,
TypingDiscipline = new [] {
"Inferred",
"Static",
"Strong",
},
},
new Language {
Name = "JavaScript",
AppearedIn = 1995,
TypingDiscipline = new [] {
"Duck",
"Dynamic",
"Weak",
},
},
new Language {
Name = "Lisp",
AppearedIn = 1958,
TypingDiscipline = new [] {
"Dynamic",
"Strong",
},
},
new Language {
Name = "Python",
AppearedIn = 1991,
TypingDiscipline = new [] {
"Duck",
"Dynamic",
"Strong",
},
},
new Language {
Name = "Ruby",
AppearedIn = 1995,
TypingDiscipline = new [] {
"Duck",
"Dynamic",
"Strong",
},
},
new Language {
Name = "Scheme",
AppearedIn = 1975,
TypingDiscipline = new [] {
"Dynamic",
"Strong",
},
},
};
Select
)languages
から各プログラミング言語の名前のみを抽出してみましょう。
var names = new List<String>();
foreach (var l in languages)
names.Add(l.Name);
// names ==> new [] {"C#", "Haskell", "JavaScript", "Lisp", "Python", "Ruby", "Scheme"}
var names = languages.Select(l => l.Name);
リスト内の各データと一緒にインデックスも必要な場合も少なからずあるので、Select
にはインデックスも付いてくるバージョンも用意されています:
languages.Select((l, i) => String.Format("{0}. {1}", i, l.Name));
// ==> new [] {"0. C#", "1. Haskell", "2. JavaScript", "3. Lisp", "4. Python", "5. Ruby", "6. Scheme"}
Where
)languages
から初出の年が1990年よりも昔のものを抽出してみましょう。
var oldies = new List<Language>();
foreach (var l in languages) {
if (l.AppearedIn < 1990)
oldies.Add(l);
}
// oldies.Select(l => l.Name) ==> new [] {"Lisp", "Scheme"}
var oldies = languages.Where(l => l.AppearedIn < 1990);
Any
)languages
から2000年以降に出現した言語が存在するかどうか判定してみましょう。
var exists = false;
foreach (var l in languages) {
if (2000 <= l.AppearedIn)
exists = true;
}
// exists ==> true (C#が該当する)
var exists = languages.Any(l => 2000 <= l.AppearedIn);
All
)languages
中の言語が全て動的型付けかどうか判定してみましょう。
var valid = true;
foreach (var l in languages) {
if (!l.TypingDiscipline.Contains("Dynamic"))
valid = false;
}
// valid ==> false (Haskellが該当しない)
var exists = languages.All(l => l.TypingDiscipline.Contains("Dynamic"));
Min
/Max
)languages
から初出が最も古い年および新しい年を求めてみましょう。
var oldestYear = Int32.MaxValue;
foreach (var l in languages) {
if (l.AppearedIn < oldestYear)
oldestYear = l.AppearedIn;
}
// oldestYear ==> 1958 (Lisp)
var newestYear = Int32.MinValue;
foreach (var l in languages) {
if (newestYear < l.AppearedIn)
newestYear = l.AppearedIn;
}
// newestYear ==> 2001 (C#)
var oldestYear = languages.Select(l => l.AppearedIn).Min();
var newestYear = languages.Select(l => l.AppearedIn).Max();
Distinct
)languages
の初出年の一覧(ただし重複分は除く)を求めてみましょう。
var years = new List<Int32>();
foreach (var l in languages) {
if (!years.Contains(l.AppearedIn))
years.Add(l.AppearedIn);
}
// years ==> new [] {2001, 1990, 1995, 1958, 1991, 1975} (JavaScriptとRubyは1995年生まれ)
var years = languages.Select(l => l.AppearedIn).Distinct();
OrderBy
/ThenBy
)languages
の各言語を初出年でソートしてみましょう。
さらに初出年が同じ言語については名前でソートしてみましょう。
var sortedLanguages = languages.ToArray();
Array.Sort<Language>(sortedLanguages, (Comparison<Language>)((left, right) => {
var a = left.AppearedIn.CompareTo(right.AppearedIn);
if (a != 0)
return a;
return left.Name.CompareTo(right.Name);
}));
// sortedLanguages.Select(l => l.Name)
// ==> new [] {"Lisp", "Scheme", "Haskell", "Python", "JavaScript", "Ruby", "C#"}
var sortedLanguages = languages.OrderBy(l => l.AppearedIn).ThenBy(l => l.Name);
OrderBy
や ThenBy
は昇順ソートですが、逆順ソートを行うための OrderByDescending
や ThenByDescending
もあります。OrderBy
や OrderByDescending
はソートを行いますが、ソートではなく単にリストの中身を逆順にするための Reverse
もあります。Take
/Skip
)languages
から初出年が最も古いものを3つ取得または除去してみましょう。
var takenLanguages = new List<Language>();
var takenCount = 0;
foreach (var l in languages.OrderBy(l => l.AppearedIn)) {
takenCount++;
if (takenCount <= 3)
takenLanguages.Add(l);
}
// takenLanguages.Select(l => l.Name) ==> new [] {"Lisp", "Scheme", "Haskell"}
var skippedLanguages = new List<Language>();
var skippedCount = 0;
foreach (var l in languages.OrderBy(l => l.AppearedIn)) {
skippedCount++;
if (3 < skippedCount)
skippedLanguages.Add(l);
}
// skippedLanguages.Select(l => l.Name) ==> new [] {"Python", "JavaScript", "Ruby", "C#"}
var takenLanguages = languages.OrderBy(l => l.AppearedIn).Take(3);
var skippedLanguages = languages.OrderBy(l => l.AppearedIn).Skip(3);
個数による指定の Take
/Skip
だけでなく、
任意の条件を指定することができる TakeWhile
/SkipWhile
もあります:
languages.TakeWhile(l => l.AppearedIn % 2 == 0)
SelectMany
)languages
から各言語の型付けの種類を抽出して重複を取り除いてみましょう。
var ts = new List<String>();
foreach (var _ts in languages.Select(l => l.TypingDiscipline))
ts.AddRange(_ts);
var typingDiscipline = ts.Distinct();
// typingDiscipline ==> new [] {"Dynamic", "Nominative", "Safe", "Static", "Strong", "Inferred", "Duck", "Weak"}
var typingDiscipline = languages.SelectMany(l => l.TypingDiscipline).Distinct();
ToDictionary
)languages
からキーが言語名で値がその言語の情報であるようなディクショナリ
(他の言語ではマップやハッシュテーブルや連想配列と呼ばれているもの)
を作成してみましょう。
var languageTable = new Dictionary<String, Language>();
foreach (var l in languages)
languageTable[l.Name] = l; // あれ? 同一キーを持つ値が複数あったらどうするの?
// languageTable.Keys.OrderBy(k => k)
// ==> new [] {"C#", "Haskell", "JavaScript", "Lisp", "Python", "Ruby", "Scheme"}
var languageTable = languages.ToDictionary(l => l.Name);
例えば「キーが言語名で値が初出年」のように、ToDictionary
にはキーだけでなく値の選択もカスタマイズできるバージョンも用意されています。
var languageTable = languages.ToDictionary(l => l.Name, l => l.AppearedIn);
ToLookup
)ToDictionary
は変換元のリストにおいてキーと値が一対一に対応するという前提があります
(もし同じキーに対して対応する値が複数ある場合は例外が送出されます)。
大抵の場合はこれで問題はないのですが、
同じキーに対して対応する値が複数あるようなディクショナリを作りたい場合も少なからずあります。
例えばキーが初出年で値が言語名であるようなディクショナリを作る場合、
サンプルデータでは1995年生まれの言語としてJavaScriptとRubyがあるため、
値としてどちらを採用すれば良いかが分かりません。
という訳でそのようなディクショナリを作ってみましょう。
var languageTable = new Dictionary<Int32, IList<String>>();
foreach (var l in languages) {
if (!languageTable.ContainsKey(l.AppearedIn))
languageTable[l.AppearedIn] = new List<String>();
languageTable[l.AppearedIn].Add(l.Name);
}
// languageTable ==> new Dictionary<Int32, IEnumerable<String>> {
// {1958, new [] {"Lisp"}},
// {1975, new [] {"Scheme"}},
// {1990, new [] {"Haskell"}},
// {1991, new [] {"Python"}},
// {1995, new [] {"JavaScript", "Ruby"}},
// {2001, new [] {"C#"}},
// }
var languageTable = languages.ToLookup(l => l.AppearedIn, l => l.Name);
Aggregate
)これまで Select
、Where
、ToDictionary
等々のLINQの便利なAPIを紹介してきました。
九分九厘の作業はこれらを使うだけで済ませられるのですが、
稀にリスト全体に対して独自の処理をしたい場合が発生します。
しかし、そのほとんどは
「リストに対して任意の処理を行う」
「処理した結果を何らかの形でまとめる」
というパターンに当てはまります。
例えば languages
から各言語の初出年のうち最新のものを求めてみましょう
(これは Max
を使えばできるのですが、使用例なのであまり気にしないでください)。
var newestYear = Int32.MinValue;
foreach (var l in languages) {
if (newestYear < l.AppearedIn)
newestYear = l.AppearedIn;
}
// newestYear ==> 2001 (C#)
var newestYear = languages.Aggregate(
Int32.MinValue,
(newerYear, l) => (newerYear < l.AppearedIn ? l.AppearedIn : newerYear)
);
上記の例のように、実のところこれまで紹介してきたAPIのほとんどが
「リストに対して任意の処理を行う」
「処理した結果を何らかの形でまとめる」
のパターンになっています。
ですので Aggregate
を用いた形で表現可能です。
Aggregate
そのものは汎用的過ぎるため、ぱっと見て意図が通じ難くなります。
なので以下のようにアプリケーション固有のライブラリ関数の実装に使う方が良いでしょう。
static public class ExtensionMethods
{
static public IEnumerable<Int32> MyMax<Int32>(this IEnumerable<Int32> sequence)
{
return sequence.Aggregate((greater, value) => (greater < value ? value : greater));
}
}