Perl の基礎知識で実用プログラミング 〜旅情編〜

Perl5 Advent Calendar 2016 7日目の記事です。
昨日は @mackee_w さんの PerlのELVM バックエンドを実装してチューニングした話 でした。

今日のこの記事では、Perl 初心者さん向けに「Perl の基礎知識があれば、こんなに実用的なプログラムを作れますよ」という例を紹介します。

目次

開発動機

みなさんは、旅行の荷物とのおつき合いは順調でしょうか。
旅先で忘れ物に気づいて困ったり、その逆に、余計な物まで持参して荷物の重さに苦しんだりしたことはありませんか? また、荷造りの際に、何を持って行くべきかで毎度のように頭を悩ませていたりしませんか?

私はどれもこれも当てはまるのですが、どちからといわずとも、忘れ物よりは荷物の重さや選定で困っていました。
問題を自覚したのは、今年の夏のことです。青春18きっぷを10日分購入して、始発で出発し、旅先でも電車に乗り倒し、終電1本前で帰宅するというアクティビティに励んだのですが、「荷物はなるべく軽く小さく」というポリシーに反して、どんどん荷物が増えていく...

旅慣れている方なら、青春18きっぷ、始発、終電1本前というキーワードだけでお察しかもしれませんが、実はこの旅、けっこうシビアだったのです。移動に片道6時間とか9時間とかはザラで、戻ってくるのにも同じくらいの時間がかかるのですが、旅先でちょっと大きめに予定が狂うと、楽しみにしていた目的地を回れなくなったり、家に帰れなくなったりするわけです。
何度か肝の冷える思いをしているうちに、備えあれば憂い無し式にどんどん荷物が増えていき、ついには「リュックが重すぎて走っても前に進めなかったために電車を逃す」という珍しい体験をしました。

その後は、無駄な荷物を減らすために、持って行くべきものとそうでないものの峻別に真剣に取り組むようになったのですが、これがとんでもなく時間を喰う苦行でした。
旅の前提条件って毎回違うんですよね。短い旅と長めの旅と長い旅、PC の要不要、ドレスコードの有無、などなどなど。今挙げた条件の組み合わせだけで 3 * 2 * 2 = 12パターンですが、実際にはもっと細々した思い出しにくい事柄がいろいろと有り、条件はずっと複雑になります。
しかも、旅に出れば何かしら価値観に影響を受けるので、同じような条件でも必要な荷物はどんどん変化していきます。

この難題をどうにかするために、スプレッドシートの関数を駆使するなどして持ち物リストの作成やブラッシュアップにも励んでみましたが、結局、納得の行くリストは完成しませんでした。


と、そういうわけで、自分の頭で考えるのは諦めて、難題解決のための凄いプログラムを作ったのでした。

技術的トピック

「凄いプログラム」と聞くと身構えてしまう人のために、この記事やプログラムに登場する技術的トピックを挙げておきます。

Perl のハッシュ / 配列 / リファレンス / デリファレンス / サブルーチン / 標準入力 / ファイル読み込み / 正規表現 / ソート / eval (解説有り)
Perl以外で、TSV形式 (解説有り)

どうでしょう? 「凄いプログラムという割に、あんまり高度なことはやってなさそうだな」という気がしてきませんか? だとしたら正解で、この記事はあなたのような方のために書かれました。だいたいのところは理解できる内容になっていますので、安心して読み進めてください。

それでは、いざ、凄いプログラムの紹介へ。

凄い機能

  • 前提条件をヒアリングして、旅の内容にふさわしい持ち物リストを一瞬で作ってくれる
  • なんと、必要な数量まで教えてくれる

使い方

  1. ExcelGoogle スプレッドシートなどで所定のフォーマットのアイテムリストを作る
  2. TSV形式 (後述) で書き出す
  3. プログラムと同じディレクトリ (フォルダ) に items.tsv というファイル名で置く
  4. コマンドラインでプログラムを起動する
  5. 旅についての質問に次々に答えていく

たったこれだけで、その旅に必要な荷物のリストが完成するという仕組みになっています。

動作イメージ

続いて、動作イメージです。
データにリアリティがあるほうが凄さが伝わりやすいので、私秘伝のアイテムリストをサンプルとして使います。

旅の設定もリアルに行きます。ちょうど明後日から2泊3日の北海道遠征予定で、YAPC 参加ついでにちょっと足を伸ばして趣味スポットを訪れるので、そのための持ち物リストを作ります。

$ perl ./motteke.pl

で起動し、

何泊しますか? 自然数を入力してください。:

2 とタイプして Enter を押すと、

以下の質問は、該当する場合のみ'y'と答えてください。

と来るので、

IT系イベント?: y
スーツ?: 
仕事?: 
同行者?: 
寒い?: y
暑い?: 
未踏エリア?: y
洗濯?: 
田舎?: y
登山?: 
超寒い?: y
雨?: 
雪?: y
飛行機?: y

こんな感じで回答すると、即座に持ち物リストが出来上がります。少々長くなりますが、リスト全体を載せてしまいます。

--------------------
もってけリスト
--------------------
00 カバン スタンプ帳 * 1冊
01 カバン 傘 * 1本
02 カバン MacBook Pro * 1個
03 カバン iPhone * 1個
04 カバン 財布 * 1個
05 カバン ペン * 1本
06 カバン メモ帳 * 1冊
07 カバン 交通系ICカード * 1枚
08 カバン 名刺入れ (名刺入り) * 1個
09 カバン サングラス * 1本
10 カバン A5ファイル * 1枚
11 カバン ハンカチ * 1枚
12 カバン>茶ポーチ モバイルバッテリー * 1個
13 カバン>茶ポーチ ポケットティッシュ * 1袋
14 カバン>茶ポーチ 歯ブラシ * 1本
15 カバン>茶ポーチ 防水用ビニール袋 * 1枚
16 カバン>茶ポーチ カロリーメイト * 1個
17 カバン>茶ポーチ カイロ * 2個
18 カバン>半透明ポーチ MacBook 電源アダプタ * 1個
19 外泊袋 外出着 * 2日分
20 外泊袋 インナーの靴下巻き * 2個
21 外泊袋 替えハンカチ * 2枚
22 外泊袋>黒ポーチ Lightning・USB 変換アダプタ * 1個
23 外泊袋>黒ポーチ iPhone 充電アダプタ * 1個
24 外泊袋>透明ポーチ コンタクトレンズ * 2セット
25 外泊袋>透明ポーチ コンタクトレンズ保存液 * 2本
26 外泊袋>透明ポーチ 麺棒 * 6本

良い旅を!


もう1パターンいってみます。
これまたリアルな例で、前回の旅行です。登山かつ仕事かつ客先訪問という大量の荷物を必要とする3泊4日でした。

$ perl ./motteke.pl
何泊しますか? 自然数を入力してください。: 3

以下の質問は、該当する場合のみ'y'と答えてください。
IT系イベント?: y
スーツ?: y
仕事?: y
同行者?:
寒い?: y
日射し?: y
暑い?:   
未踏エリア?: y
洗濯?:   
田舎?: y 
登山?: y 
超寒い?: 
雨?:
雪?:
飛行機?: 

--------------------
もってけリスト
--------------------
00 - ビジネスバッグ * 1個
01 - トレッキングポール * 2本
02 カバン スタンプ帳 * 1冊
03 カバン 帽子 * 1枚
04 カバン MacBook Pro * 1個
05 カバン iPhone * 1個
06 カバン 財布 * 1個
07 カバン ペン * 1本
08 カバン メモ帳 * 1冊
09 カバン ノート * 1冊
10 カバン 交通系ICカード * 1枚
11 カバン 名刺入れ (名刺入り) * 1個
12 カバン サングラス * 1本
13 カバン A5ファイル * 1枚
14 カバン A4ファイル * 1枚
15 カバン ハンカチ * 1枚
16 カバン>財布 EX-ICカード * 1枚
17 カバン>財布 EX身分証明用クレカ * 1枚
18 カバン>茶ポーチ モバイルバッテリー * 1個
19 カバン>茶ポーチ ポケットティッシュ * 1袋
20 カバン>茶ポーチ ウェットティッシュ * 1袋
21 カバン>茶ポーチ 歯ブラシ * 1本
22 カバン>茶ポーチ カロリーメイト * 1個
23 カバン>茶ポーチ カイロ * 1個
24 カバン>半透明ポーチ MacBook 電源アダプタ * 1個
25 カバン>半透明ポーチ ディスプレイアダプタ * 1個
26 スーツケース>スーツ袋 スーツ * 1着
27 スーツケース>靴袋 革靴 * 1足
28 外泊袋 シャツ * 1枚
29 外泊袋 外出着 * 3日分
30 外泊袋 インナーの靴下巻き * 3個
31 外泊袋 替えハンカチ * 3枚
32 外泊袋>ネガネケース メガネ * 1本
33 外泊袋>黒ポーチ Lightning・USB 変換アダプタ * 1個
34 外泊袋>黒ポーチ iPhone 充電アダプタ * 1個
35 外泊袋>黒ポーチ 目薬 * 1本
36 外泊袋>透明ポーチ コンタクトレンズ * 3セット
37 外泊袋>透明ポーチ コンタクトレンズ保存液 * 3本
38 外泊袋>透明ポーチ 麺棒 * 9本

良い旅を!

上が 26件で下が38件。実はサンプルのリストからは、公にしたくない趣味の品々などを除外してあるのですが、それでもけっこうなボリュームです。これらの一つ一つについて、都度自分の頭で要不要を判断していたころには、もう戻れそうにありません。この凄いプログラムは、はっきり言って、めちゃ便利です!

アイテムリストリスト

さて、ここからは、技術的な話が増えてきます。

プログラム本体の説明に入る前に、アイテムリストについて説明します。
まずはサンプルのアイテムリスト をご覧ください。

4列からなるスプレッドシートで、左から順に

A列 "入れ物"
B列 "品名"
C列 "数量"
D列 "単位"

となっています。

論理演算子三項演算子

2行目以降のデータ行を見ると、C列 "数量" の内容がただの数字ではないことにすぐ気づかれると思います。
いくつかのデータについて、その意味を表にまとめます。

品名 数量 単位 説明
財布 1 財布を1個持って行く
コンタクトレンズ保存液 泊数 セット N泊するならレンズをNセット持って行く
革靴 スーツ ? 1 : 0 スーツを着るなら革靴を1足持って行く。着ないなら革靴は不要
雨合羽 (登山 && 雨) ? 1 : 0 登山をするときに雨ならば1枚持って行く。それ以外の場合は要らない
ウェットティッシュ (登山 || 暑い) ? 1 : 0 山に登るか、もしくは暑い時期なら 1袋持って行く。それ以外の場合は要らない

'&&' や '||' は Perl や他の言語の論理演算子と同じ働き、'?' や ':' は三項演算子と同じ働きをしています。日本語の説明と見比べてみると、演算子を用いることで、簡潔で紛れのない表現に落とし込めていることが分かりますね。

もうちょっと大げさに比較するなら、こんな感じでしょうか。
「いくつ要るの?」
「えーっと、山に登るときとか暑いときは1袋で、じゃなかったら要らない」
「『じゃなかったら』はどこに掛かるのか。正確に要点だけを言え」
「 (登山 || 暑い) ? 1 : 0」

TSV形式

使い方の項でもちらっとふれましたが、アイテムリストは TSVという形式で保存します。

Google スプレッドシートの場合
ファイルメニュー>形式を指定してダウンロード>タブ区切りの値 でTSV形式でダウンロードできます。
Excel の場合
では、名前をつけて保存の "フォーマット" で "タブ区切りテキスト" を選択することで、任意のシートをTSV形式で保存できます。

TSV形式というものを知らなかった方も、ここまでで何となくお察しのことと思いますが、TSV というのはタブ文字区切りのテキストデータのことです。データの区切りをカンマで表現する CSV と似たようなものですが、下記のように違います。

CSV
データをカンマで区切ったりクォートしたり、場合によってはエスケープしたりする
TSV
単純にタブ文字で区切るだけ

今回のプログラムでは、簡単にデータを読み込むために、シンプル仕様のTSV形式を採用しました。

# 改行文字を外して \t で split するだけでデータを取り出せる
chomp($line);
my @cols = split(/\t/, $line);

プログラム

続いてプログラム本体についての説明です。

ソースコードこちらに置いてあります。
全体の処理の流れは、文章で説明するよりも、main サブルーチンのコードを見てもらったほうが掴みやすいと思います。

sub main {
    # TSVファイルからアイテムデータを読み込む
    my $file_path = './items.tsv';
    my $items = load_tsv($file_path);
             
    # C列"数量"に登場する言葉を、ユーザーに確認するために取り出しておく
    my $words = get_words($items);
             
    # ユーザーから旅の条件を聞き出す
    my $conditions = ask_conditions($words);
                                                                                                                                            
    # 各アイテムの必要数量を求める
    calculate_quantities($items, $conditions);
             
    # 持ち物リストを出力する
    print_item_list($items);
             
    print "\n良い旅を!\n";
}

ソースコード内に難しい構文は有りませんが、データ構造が少し複雑なので、戸惑われる方もいるかもしれません。&main に登場する $items, $words, $conditions という3つの変数について下記にまとめるので、読解の参考にしてください。

$items

「TSV ファイルから読み込んだ内容」のハッシュの配列のリファレンス。

[                    
  {                  
    container        => 'カバン',
    quantity         => '(IT系イベント || 仕事) ? 1 : 0',
    unit             => '個',
    name             => 'MacBook Pro',
    numeric_quantity => 1,
  },                 
  {                  
    container        => 'カバン',
    quantity         => '1',                                                                                                                
    unit             => '個',
    name             => 'iPhone',
    numeric_quantity => 1,
  },                 
  (中略)             
]  

numeric_quantity は、calucurate_quantities($items, $conditions) に追加されるキーで、値は最終的な必要な数量です。

$words

「$item の quantity キーの値から取り出した言葉」の配列のリファレンス。

['IT系イベント', '仕事', (中略) , '泊数']                                                                                          

$conditions

「ユーザーから得た条件群」のハッシュのリファレンス。
"泊数" 以外のキーの値は、条件を満たす場合は 1、満たさない場合は 0 になります。

{                      
  '泊数'         => 2,                                                                                                              
  'IT系イベント' => 1, 
  'スーツ'       => 0, 
  '仕事'         => 0, 
  (中略)               
}

最後に

初心者さん向けの Perl プログラミング勉強会などで「シンタックスは学んだけど、この知識をどう活かせばいいのかが分からない。もっといろいろなことを勉強しないと何もできない気がする」という声をときどき聞くので、基礎知識でできることの一例を紹介しました。
この記事が、どなたかが何かを作り始めるきっかけに繋がれば幸いです。

なお、私 @wakegisky は、Perlガイド東海の主催者の一人として、初心者さん向けのプログラミング勉強会を名古屋で不定期開催しています。お近くの方は、もしタイミングが合えば、ぜひ Perlガイド東海のイベントにお越しください。主催者一同、お待ちしておりますので!


明日の記事は、だれか氏 (未定) の何か (未定) です。だれか氏〜〜!