carvo update

f:id:note103:20170705123353g:plain

  • Macだと音声も出るようになっているので、音声付き動画も作りました。
    • 6秒目ぐらいで急に音が出ます。

www.youtube.com

  • 以下、つれづれに。
  • 昨日も別のところに書いたのだけど、今回はメンテナンスの邪魔になりがちなファットな要素(=便利だと思っていたけど本質的な要素とはちょっとズレた機能)をザクザク外すのが主な変更。
  • テストもちょっとだけ入れつつ。
  • 未解決の問題として、どうもどこかの段階で、ファイルへの回答記録記入の動作に不具合が生じてしまった模様。
    • ゲームの途中で単語カードを切り替えた場合、切り替える前の記録が消えてしまう。
    • 正答/誤答数などを記入する `result.txt` の方は無事なようだけど、詳細に英単語などを記録する `log.txt` の方が上書きされてしまうみたい。
    • 以前は何回切り替えてもすべて保持していたのだけど……。
    • よって、ヘルプから記録に関する文言を一旦カットして、忘れないように自分でIssue登録しておいた。
  • 今後のTODOとしては、上記バグ修正のほか、残りのテストを充実させること、その流れでTravis CIとの連携を復活させたいところ。
    • CI連携に関しては、以前に少し頑張ってカバレッジ可視化サービスと繋げて表示したりもしていたのだけど、なにしろ拙い&多めのコードを全然カバーしきれず効率悪かったので一旦やめていた。
    • まだまだ自分の中でも整理できないまま動かしている部分が多々あるので、その辺を整理しつつテストも補って、というふうに進めていきたいところ。
  • 最後にREADMEを貼っておきます。

    
      続きを読む
    
    
  

bashのエラーにハマって直った

最近は作業記録をけっこう細かい粒度で付けているのでわかるのだけど、昨日普段どおりに付けた最後の記録が

2017/06/14 Wed 13:07:02 今から**やる

だったので(**の部分は業務内容なので割愛)、少なくともその時点までは正常だったのが、次の記録では

2017/06/14 Wed 14:12:29 Dropboxのファイル移動が怪しいかな

となっていて、その1時間ほどの間に何かが起きていた。

現象

なんの話かというと、その14時頃に遭遇していた問題というのがあって、ターミナルを開くと

-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file
-bash: __git_ps1: command not found

みたいなエラーがザバッと出てくる。

あれ、と思って.bashrcを読み込み直すと、

$ source ~/.bashrc
-bash: /Users/note103/.bashrc: line 12: syntax error near unexpected token `}'
-bash: /Users/note103/.bashrc: line 12: `}'
-bash: __git_ps1: command not found

あらら。慌てて今度は.bash_profileを読み込み直すと、こうなる。

$ source ~/.bash_profile
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file
-bash: __git_ps1: command not found

さらには、僕は普段から自分で作ったPerl製のコマンドラインツールをよく使っていて、冒頭の作業記録もそれを使って記入しているのだけど*1、そのツールを使って「なんかよくわからんことが起きている」というメモを書こうとしたらこんなエラーが出て、

Can't locate TinyCalcs.pm in @INC (you may need to install the TinyCalcs module) (@INC contains: /Library/Perl/5.18/darwin-thread-multi-2level /Library/Perl/5.18 /Network/Library/Perl/5.18/darwin-thread-multi-2level /Network/Library/Perl/5.18 /Library/Perl/Updates/5.18.2/darwin-thread-multi-2level /Library/Perl/Updates/5.18.2 /System/Library/Perl/5.18/darwin-thread-multi-2level /System/Library/Perl/5.18 /System/Library/Perl/Extras/5.18/darwin-thread-multi-2level /System/Library/Perl/Extras/5.18 .) at /Users/note103/path/to/memo.pl line 8.
BEGIN failed--compilation aborted at /Users/note103/path/to/memo.pl line 8.
-bash: __git_ps1: command not found

メモすら取れなくなってしまった。

ちなみに、ここに出てくるTinyCalcs.pmというのは、その自分で使っているメモツールでuseしている自作モジュールなので、これはそのモジュールが読み込めてないよ、というエラー。

さらに言うと、ここで注目すべきはPerlバージョンで、上には5.18と出ているけど、普段ぼくはplenvというPerlバージョン管理ツールでインストールした5.24を使っているので、なぜかそれが無視されてPerlバージョンまでダウングレードしてしまっている。

という、なかなか味わったことのないカタストロフィーにこの段階でゾ〜〜……ッとして、軽くパニックに陥ってしまった。

検証

上のエラーに戻ると、まず

-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file

あたりは、当のコードを見てもとくにそれ自体に問題があるとは思えない。

ちなみに、その初めの2行で出てくるシェルスクリプトはどちらもGitを便利にする系のツールで、前者はGitコマンドをタブで補完するためのもので、後者はプロンプトにブランチ名を表示してくれるもの。

解説は以下あたりがわかりやすいか。
qiita.com

それから、どのエラーにも等しく顔を出してくるこれ、

-bash: __git_ps1: command not found

これは、.bash_profileに記述されている以下に対応している。

PS1='[\h] \W$(__git_ps1 ":%s")\$ '

ようは、上記のコマンド補完スクリプト( git-prompt.sh )を読み込めないのでそのコマンドもないよ、というメッセージ。

あとは.bashrcや.bash_profileに関する以下のエラーに関しても、

-bash: /Users/note103/.bashrc: line 12: syntax error near unexpected token `}'
-bash: /Users/note103/.bash_profile: line 82: syntax error: unexpected end of file

ここで示されている各該当部分はとくに問題なさそうというか、むしろその直前まで何も問題なかったのだから、これを真に受けていろいろいじってしまったらかえって二次災害に繋がってしまう。

よって、具体的に手を動かすこともできないまま、「一体なにが原因なんだろう……」と悩みはじめ、まあ経験的にはどう考えてもその直前に無意識にやったことが原因であることは間違いないのだけど、あまりにもいろんな作業を並行して進めていたせいで、原因になりそうな特定の作業をまったく思い出せない。

そしてその時点では想像できなかったのだけど、これにハマったまま結局夜になってしまった。*2

もちろんその間も、いろんなサイトを検索して見て回ったのだけど、

結果的には、今回の件に関係あるものはひとつもなかった。

一方、じつは最初の段階で、「前にも似たようなエラーでハマって解決したことがあったんだよな……なんだっけあれは……」と思っていて、少ししてからそれは.bashrcに仕込んでいたエイリアスに「done」というエイリアス名を使っていたことが原因のエラーだったことを思い出したのだけど、とはいえその後は教訓を活かして「done」というエイリアス名は使っていなかったので、「ん〜、似てると思ったけどそれはナイか……」と却下して、いよいよ出口がなくなった。

こういう場合、プログラミングを職業としている人ならいつまでも個人的にハマっているわけにはいかないから、職場の先輩や同僚などにヘルプを求めると思うのだけど、自分はそういう環境もないし、Perl入学式の先輩たちに意見を聞くことも可能ではあったのだけど、再現条件を示すのも大変そうだったので、そのままさらに独自対応の道を進むことに。

で、とにかくもういろんな「あの手この手」の試行錯誤をして、何か必要な不可視ファイルを捨ててしまったのではないかとか(冒頭のメモに書いた「Dropbox」はその流れで疑った)、いっそMacのTime Machine機能を使って半日ほど前の状態に復元してしまうべきか、とかも考えたのだけど、そんな中でもとりあえず、Perlのエラーについてはたぶん.bash_profileに記載している以下の設定、

if [ -d ${HOME}/.plenv  ] ; then
    export PATH=${HOME}/.plenv/bin:${HOME}/.plenv/shims:${PATH}
    eval "$(plenv init -)"
fi

このplenvの設定を読み込んでいないことによるものだろうとは思っていたし(5.18というのもダウングレードというよりシステムPerlに戻った状態なのだろうと)、そうであるならひとまずはbashまわりのミスに狙いを絞っていいはずだと自分に言い聞かせて、気持ちを落ち着かせるようにした。

しかしそれ以上のことはまったくわからないので、とりあえず「もうどんな手を使ってもいいから一旦このエラーを全部消してみよう」と思って、まずはどのファイルのどの記述がエラーを出しているのか確認するために、.bash_profileと.bashrcに

echo 1

から

echo 5

までを適当に散らばらせてみた。

で、その状態で新たにターミナルを開いたら、こんな感じになった。

1
3
4
5
-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 84: syntax error: unexpected end of file
-bash: __git_ps1: command not found

おっと、面白い。出力された数字から「2」が抜けている。

具体的には、このとき1と2は.bash_profileのトップと最終行に入れていて、3,4,5はそれぞれ.bashrcのトップ、真ん中、最終行に入れている。
(.bash_profileは80行程度であるのに対して、.bashrcは500行を超えているのでそのように配分した)

で、なぜ出力がこうなるのか考えてみると、.bash_profileの上の方には以下の記述があるので、

if [ -f ~/.bashrc ] ; then
    source ~/.bashrc
fi

おそらくコンピュータは最初に.bash_profileの1を出力して、その後に上記の呼び出しを伝って.bashrcの3,4,5を吐き出して、また.bash_profileに戻ってきたところでエラーを吐いて、2に至る前に死んでいる。

では、という感じで、今度はその呼び出しをコメントアウトして、

# if [ -f ~/.bashrc ] ; then
#     source ~/.bashrc
# fi

その状態で新たにターミナルを開いてみると……

1
2

おお。エラーが消えた。そしてさっきは出てこなかった「2」(.bash_profileの最終行)がちゃんと出てる。

ということは、問題は.bash_profileの方にはない。
加えて、マシン自体やその他のアプリケーションとかからの影響で不具合が起きてるわけでもない。

これはやはり、.bashrcで何かよからぬものを拾って、その状態で.bash_profileに戻ってきたから発生したエラーなのだ。

……という仮説に基づいて、ここからは.bashrcの検証へ。

追跡

まずは先ほどの.bash_profileのコメントアウトを外して、さっきまでと同様のエラーがすべて出る状態に戻す。

1
3
4
5
-bash: /Users/note103/.git-completion.bash: line 52: syntax error near unexpected token `elif'
-bash: /Users/note103/.git-completion.bash: line 52: `	elif [ -d "$1/.git" ]; then'
-bash: /Users/note103/.git-prompt.sh: line 120: syntax error near unexpected token `;;'
-bash: /Users/note103/.git-prompt.sh: line 120: `			;;'
-bash: /Users/note103/.bash_profile: line 84: syntax error: unexpected end of file
-bash: __git_ps1: command not found

次に、500行以上ある.bashrcのファイルのうち、1行目に置いている

echo 3

から、真ん中あたりに仕込んでいる

echo 4

までを残して、そこから最終行までの下半分を一旦カットしてターミナルを開いてみる。

1
3
4
2

ヤッホー! エラーが消えた。そして残したecho文の数字も全部出ている。

ということは、.bashrcの真ん中に設置した

echo 4

の次の行から、先ほどカットした最終行の

echo 5

までの間に、問題の「それ」はあると考えられる。

ここまで来ると、もう先ほどまでの苦悩やヤケ感は何だったのかと思うほど体がラクになっている。

あとは単純に、これまでの繰り返し。機械的に対象領域をどんどん半分に区切っていって、最後までエラーを吐いている行を追い詰めればいい。

で、追い詰めた犯人はこれ。

alias fi="perl /path/to/findline.pl"

(パス名は仮のもの)

ここでようやく、「あ〜……なんだよやっぱり、前に経験したやつと同じじゃん!」と気がついた。

つまり、さっき上の方で「それはナイ」と却下した「done」と同じ。

簡単に解説すると、そもそも「done」をエイリアス名に使って何が問題だったのかというと、おそらくシェルスクリプトのfor文でそれを使うからだろう。

こんな感じのやつ。

for i in foo bar baz
do
    echo $i
done #<=ココ

で、この「fi」というのもまさにそれで、「fi」はシェルスクリプトのif文でこのように使う。

s=foo
if [ $s == foo ] ; then
    echo FOO!
else
    echo BAR!
fi #<=ココ

(この場合は「FOO!」と出力される)

つまり、今回も「done」のときと同様に、構文に出てくる「fi」をエイリアス名に使ってしまったから各種のエラーが生じたのだと思われる。

実際、とりあえずその「fi」を別のものにしたらエラーはすべて消えたし、それによってPerlも元通り5.24.0に戻ったし、そのPerlを使った自分のモジュール&コード群もすぐ使えるようになった。

この時の記録。

2017/06/14 Wed 19:40:55 Dropbox全然関係なかったじゃん

まとめ

*1:いずれここでも紹介したいが。

*2:一応業務もやっていたけど、解決しないまま夜になるとは思っていなかった。

crontabでPerlスクリプトを動かそうとしてハマった話

crontabとは

以下がわかりやすいです。

www.server-memo.net

@songmuさんによる以下のシリーズも充実しています。
gihyo.jp

そのcrontabでPerlスクリプトを動かそうと思ったところ、以下のようなハマり方をしたのでメモ。

なお、環境はMacです。

crontabはシステムPerlで動く

最初にテスト的なコードを書いてみたらあっさり動いたので、逆に気づくまで時間がかかってしまったのだけど、たとえば以下のようなPerlのコードをホームディレクトリに置いておいて、

#!/usr/bin/env perl
#
# hello.pl

use strict;
use warnings;
use feature 'say';

say "Hello!";

crontabをこのように設定してみると、

10 * * * * perl hello.pl >> test.txt

普通にtest.txtの中に

Hello!

が書き込まれていく。(この場合は毎時10分に「Hello!」が追記されていく)

ただし、このときにhello.plやtest.txtをホームディレクトリ以外の場所に置くなら、それは絶対パスで記述しなければならない。

10 * * * * perl $HOME/sample/hello.pl >> test.txt

とか、

10 * * * * perl /Users/note103/sample/hello.pl >> test.txt

とか。

そこまではまあ、そうだろうなっていう感じなんだけど、どうもタスクが増えるにつれて、その辺を正確に記述してもうまく動かないケースが出てきた。

具体的には、ターミナルで普通に実行するぶんには動くんだけど、crontabに設定すると動かないとか。

で、先に結論というか原因から言うと、上で何気なく記述している「perl」というコマンドが、普段ぼくが使っている「perl」とは別物だった。

ぼくは普段、plenvというのを使って、Perlの現状ほぼ最新バージョンである 5.24.0 を使っているんだけど、crontabはそのplenvとかは経由せずに、マシンにもともと入っているシステムperlを使っていて、ぼくが使っているMacのそれは5.18.2だった。*1

システムPerlに必要なモジュールが入ってない

で、しかしこれは単にバージョンが違うというだけの話ではなくて、システムPerlの方は何しろ普段は使っていないから、いつも使っている要インストールな非標準モジュール(厳密には5.18.2時点で標準モジュールではないそれ)が入ってない。

だから当然のことながら、そういうモジュールを使っているスクリプトは動いてくれなくて、けっこうハマった。

その後、これについては動作する/しない条件を手を替え品を替え特定していく中で、「うわ、これもしかしてシステムPerlで動いてるのか」と気付き、さらに「システムPerlにはあのモジュールが入ってないのか……」などと気付いていったので、とりあえずはシステムPerlにcpanmをインストールして(それも入ってなかった)、以後はcpanm経由で必要なモジュールをどんどんインストールして、なんとか動くようになった。

システムPerl以外のPerlを指定できない

それですべて解決かと思ったんだけど、どうも一個だけ、そこまで設定してもターミナルからの実行とcrontabでの実行とで動作の異なるものが残っていた。

具体的には、以下のようなコード。(実際にはちょっと違うけど、最小限の再現コード)

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use LWP::UserAgent;

my $url   = "https://pawoo.net";
my $res   = LWP::UserAgent->new->get($url);
my $title = $res->title() // 'No title';
say $title;

これを普通にターミナルから(v5.24.0で)実行すると、こうなるんだけど、

Mastodon (マストドン) hosted by pixiv - Pawoo

crontabから(v5.18.2で)実行すると、こうなる。

No title

ようは後者だとLWP::UserAgentが望みどおりの挙動をしてくれてないということで、でもモジュール自体はシステムの方にも入っていることを確認したので、そうなると原因はモジュールの有無ではなく、Perl自体のバージョンが古いこと以外に思いつかない。

というか、それ以外の原因があるとしても、次に打てる対策は「とりあえずシステムPerlのバージョンを上げても同じ結果になるかを確認する」ということ以外に考えられないということ。

なんだけど、システムPerlのバージョンを上げるというのはちょっと聞いたことがなくて、実際Perl入学式の諸先輩に聞いても「普通そういうことはしない」という話だったので、そこでまただいぶハマった。

で、今サラッと書いたように、本件についてはここで一旦完全に行き止まりという感じになったので、Perl入学式のチャットルームで例のごとく先輩方に相談してみたところ、いろいろな対処法を教えて頂いた。

たとえば、先に.bashrcを読み込ませて、普段使っているplenvなりperlbrewなりを使うように設定するとか。
あるいは、ラッパースクリプトを介してplenvなどを噛むようにしておくとか。

で、そういった案の中に「絶対パスperlを指定する」という話もあって、これはすぐ試せそうだったのでやってみたら、動いた。

具体的には、ぼくの環境だと以下のような感じ。

10 * * * * /Users/note103/.plenv/versions/5.24.0/bin/perl /Users/note103/sample/hello.pl >> test.txt

って、これだとちょっと可読性が低いので、実際には変数を利用してこんな感じで。

perl524="/Users/note103/.plenv/versions/5.24.0/bin/perl"
pathtodir="/Users/note103/sample"

10 * * * * $perl524 $pathtodir/hello.pl >> test.txt

この変数の使い方などについては、冒頭にも挙げましたが @songmu さんの以下の記事が参考になりました。
http://gihyo.jp/dev/serial/01/perl-hackers-hub/002502

で、その記事はまた上記のチャット相談会で @papixさんから教えてもらったのだったけど、その他にも短い時間でいろんな方がそれぞれの知見を提供してくださって、先輩方には本当に感謝です。

コード内に相対パスが残っていた

というわけで、長かったcrontab問題もようやく解決……としばらく安心していたのだけど、まだ終わっていなかった。

というのも、自作モジュールを使ったコードの中で、以下のようにlibモジュールをuseしていたのだけど、

use lib './lib'

これが相対パスだったので、やはりcrontabの方では動いていなかった。

なので、これを絶対パスにしなきゃ……と思って直し始めたのだけど、念のためこのlibモジュールについて、こちらもいつもお世話になっています木本さんの以下の解説を読んでいたら、
d.hatena.ne.jp

use FindBin;
use lib "$FindBin::Bin/lib";

とした方がスマートに見えたので、それに入れ替えたらそれでも動くようになった。
多謝!

ということで、一連のメモはひとまず以上なのだけど、上記で踏み抜いた穴のいくつかについては、以下のWebページでも簡潔に解説されていたので、あわせて参考情報として貼っておきます。

プロンプトとcronでの実行結果の違い − Linux Square − @IT

そのまえに、よくありがちなパターンを書いておきますと、

というのがあります。2番目が結構厄介で、僕もその為に何回か書き直したことがあります。

この後者の話なんて、最後に挙げたものそのものだった。

*1:この辺の記述、「Perl」と「perl」で表記ユレしているように見えるが、一応言語名としては大文字始まりの「Perl」、コマンド名としては全部小文字で「perl」としている。ちゃんと調べていないのでその使い分けがいいのかどうかはわからないが……。

2つの配列から重複を弾く(Perlで)

いきなり例題から。

以下のような2つの配列があるとき、

my @fruits1 = qw/orange banana apple lemon/;
my @fruits2 = qw/orange banana/;

@fruits1のうち@fruits2とカブるものをカットして、重複しないappleとlemonだけ@fruits1に残したいとする。

こういうとき、今までぼくはこう書いていたのだけど、

#!/usr/bin/env perl
#
# cut_overlap.pl

use strict;
use warnings;
use feature 'say';

# 以後、配列名は短縮
my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

for my $f2 (@f2) {
    for my $f1 (@f1) {
        if ($f1 eq $f2) {
            say "match:\t$f1!";
            @f1 = grep {$f1 ne $_} @f1;
        }
    }
}

say '===';
say "\@f1の残りは……";
say for @f1;
say "です!";

まあ、これでも目的は達成できる。実行結果は以下。

match: orange!
match: banana!
===
@f1の残りは……
apple
lemon
です!

最初にorange、その後にbananaがマッチして、あとにはappleとlemonが残っている。

コードでやってることとしては、まず2つの配列をループさせて、要素同士がマッチしたらその要素を元の配列からgrepで削除する。

削除というか、実際には「対象の要素以外を元の配列に入れ直す」という感じか。

で、ここでは重複を弾きたい(数を減らしたい)@f1の方を内側のループにして、grepでもその配列を対象としてカットしている。

というのも、この内と外を逆にすると、

# 以後、シバンやプラグマは省略
#
# cut_overlap2.pl

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

for my $f1 (@f1) { #<= 外側にもってくる
    for my $f2 (@f2) { #<= 内側に入れる
        if ($f1 eq $f2) {
            say "match:\t$f1!";
            @f1 = grep {$f1 ne $_} @f1;
        }
    }
}

say '===';
say "\@f1の残りは……";
say for @f1;
say "です!";

結果。

match: orange!
===
@f1の残りは……
banana
apple
lemon
です!

こんなふうに、意図に反して最初のorangeだけマッチして、本来消えてほしいbananaが残ってしまう。

中身はどういうことになってるのか、必殺のprintデバッグ

# cut_overlap3_debug.pl

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

for my $f1 (@f1) {
    for my $f2 (@f2) {
        say "f1:$f1 & f2:$f2"; #<= 中身を出力してみる
        if ($f1 eq $f2) {
            say "match:\t$f1!";
            @f1 = grep {$f1 ne $_} @f1;
        }
    }
}

say '===';
say "\@f1の残りは……";
say for @f1;
say "です!";

結果。

f1:orange & f2:orange
match: orange!
f1:orange & f2:banana
f1:apple & f2:orange
f1:apple & f2:banana
f1:lemon & f2:orange
f1:lemon & f2:banana
===
@f1の残りは……
banana
apple
lemon
です!

ということで、どうやら最初にorangeにマッチした後、@f1が2周めでbananaを飛ばしてappleの周回に入ってしまっている。(実行結果の4行目)

ちなみに、さっきの上手くいった場合だと、内部はどうなっているのかというと、

f1:orange & f2:orange
match: orange!
f1:apple & f2:orange
f1:lemon & f2:orange
f1:banana & f2:banana
match: banana!
f1:lemon & f2:banana
===
@f1の残りは……
apple
lemon
です!

という感じで、このときは目的上の影響がなくて気づかなかったけど、じつはここでもorangeがマッチした後、@f1のbananaがスッ飛ばされている。(実行結果の3行目)

ということで、実利的には最初の方法であれば目的は果たせるものの、裏で起こっていることはどちらも意図と違うというか、ちょっと気持ち悪い感じがあって、そこでいつもお世話になっていますPerl入学式のサポーター陣とのチャットでいろいろ相談してみた。

で、さっそく @xtetsuji さんからいただいた解答例がこちら。

# xtetsuji_1.pl
# 
# 出力部分は省略

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

array_minus1(\@f1, \@f2);

sub array_minus1 {
    my $f1 = shift;
    my $f2 = shift;
    @$f1 = map {
        my $f1_value = $_;
        ( grep { $f1_value eq $_ } @$f2 ) ? () : $f1_value;
    } @$f1;
}

さらに、もう1案いただいた。

# xtetsuji_2.pl

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

array_minus2(\@f1, \@f2);

sub array_minus2 {
    my $f1 = shift;
    my $f2 = shift;
    my %f2_value_is = map { $_ => 1 } @$f2;
    @$f1 = map { $f2_value_is{$_} ? () : $_ } @$f1;
}

結果はいずれも、以下。

@f1の残りは……
apple
lemon
です!

前者はgrep, 後者はmapを使っているけど、どちらも「マッチしたら空リストに入れる=元の配列から外す」という処理になっている。

そしてそれとは別に、@skjmさんからAcme::Toolsのminusもあるよ、と教えて頂いて。
Acme::Tools - search.cpan.org

これを使うと、こんな感じ。

# acme_minus.pl

use Acme::Tools 'minus';

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

my @result = minus(\@f1, \@f2);

say "\@f1の残りは……";
say for @result;
say "です!";

結果は同じなので省略。

このAcme::Tools::minusの中身はこんな感じ。

sub minus {
  my %seen;
  my %notme=map{($_=>1)}@{$_[1]};
  grep !$notme{$_}&&!$seen{$_}++, @{$_[0]};
}

http://cpansearch.perl.org/src/KJETIL/Acme-Tools-0.21/Tools.pm

ちょっと略記が多いのでわかりづらいのだけど、自分が見慣れた書式に噛み砕くと……

sub minus {
    my $f1 = shift;
    my $f2 = shift;

    my %notme = map { ($_ => 1) } @{$f2};

    my %seen;
    my @result = grep { !$notme{$_} && !$seen{$_}++ } @{$f1};

    return @result;
}

といった感じか。

ちなみに、これの途中にある以下のインクリメント。

!$seen{$_}++

何度見てもこれのある意味がわからず、実際、これを取っても結果は同じなのだけど、じつは最初の配列@f1に対して、

my @f1 = qw/orange banana apple lemon/;
my @f2 = qw/orange banana/;

以下のように、同配列内の重複要素を入れてみると、結果も変わる。

my @f1 = qw/orange banana apple lemon apple/; #<= 末尾にappleを追加
my @f2 = qw/orange banana/;

先のインクリメントを入れた状態だと、これまで通りこうなるのだけど、

@f1の残りは……
apple
lemon
です!

そのインクリメントを外して以下のようにすると、

my @result = grep { !$notme{$_} } @{$f1};

結果はこうなる。

@f1の残りは……
apple
lemon
apple
です!

つまり、そのインクリメントは元になる配列(@f1)内の重複をカット(ユニーク化)してくれていた。

ということで、これについてはもう要件の問題というか、このツールを使う人がそもそもどういう結果を期待しているか? によって要不要を判断するところだろう。

とりあえずぼくの当初の希望としては、「片方の配列からもう片方の配列と重複する要素を取り除く」ということだけを求めていて、「元の配列内に存在する重複も取り除く」ことは求めていないので、そのインクリメントなしバージョンの方が適切かもしれない。

結論としてのコード集

すでにけっこう長くなってしまったけど、もう少しコード例を挙げつつ、では結局のところ、ぼく自身は今後どういうときにどういうコードで対応していくか、というまとめ。

remove

まずはこれまで話題にしたとおり、「片方の配列からもう片方の配列と重複する要素を取り除く」ということをしたい場合には、こんな関数を使う。

sub remove {
    my $whole = shift;
    my $part = shift;

    my %remove = map { $_ => 1 } @$part;

    my @result;
    for my $element (@$whole) {
        if (! $remove{$element}) {
            push @result, $element;
        }
    }
    return @result;
}

これを以下のように呼び出すと、

my @f1 = qw/orange banana apple lemon apple/;
my @f2 = qw/orange banana/;

my @result = remove(\@f1, \@f2);
say for @result;

@f1から@f2の要素と重複する要素を取り除いたものが出てくる。

apple
lemon
apple

この際、@f1内の重複はカットしない。

crush

次に、じつはこれまでに出た要件とは別に、「とにかく重複するものは全部消してほしい」と思うこともあるので、その対策。

この場合、上記の修正前の Acme::Tools::minus でも微妙に適合していなくて、なぜなら上述のとおり、同配列の重複については最低でも1つ残してしまうから。

しかしここでの新たな要件は、たとえば以下の2つの配列があった場合、

my @f1 = qw/orange banana apple lemon apple/;
my @f2 = qw/orange banana/;

lemon以外はすべて重複しているので、

lemon

とだけ出てほしい。

で、そのためにこのような関数を作ってみた。

sub crush {
    my $x = shift;
    my $y = shift;

    my %crush;
    map { $crush{ $_ }++ } ( @$x, @$y );

    for (keys %crush) {
        if ($crush{$_} >= 2) {
            delete $crush{$_};
        }
    }

    my @result;
    for ( @$x, @$y ) {
        if ($crush{$_}) {
            push @result, $_;
        }
    }
    return @result;
}

これで以下のように実行すると、

# 以後、果物の種類を増やす

my @f1 = qw/orange banana apple grape lemon apple/;
my @f2 = qw/orange banana strawberry/;

my @result = crush(\@f1, \@f2);
say for @result;

以下のように、とにかく全体の中で重複したものは取り除いてくれる。

grape
lemon
strawberry

この際、関数内ではハッシュで処理しているので、途中で順番がめちゃくちゃになっているのだけど、用途としては関数に渡した順に返ってきたほうが便利な気もするので、最後のfor文で順番を元に戻している。

ちなみに、関数名の crush というのは、重複した果物どうしをぶつけてつぶしてしまうイメージより。ジュースになって、形が消えるというような。

uniq

ついでにもう一つ、途中で少し話題にしたけど、渡した配列全体の中から、重複した余分な分はカットしつつ、でも1つは残しておきたい場合。

これは List::MoreUtilsのuniq関数を使えば早い。

use List::MoreUtils 'uniq';

my @f1 = qw/orange banana apple grape lemon apple/;
my @f2 = qw/orange banana strawberry/;

my @result = uniq(@f1, @f2);
say for @result;

実行。

orange
banana
apple
grape
lemon
strawberry

登場した果物すべてが1つずつ残っている。

uniqはリファレンスではなく普通に配列を渡すだけなので、用途が合えば作業も手っ取り早くてよい。

まとめ・謝辞・宣伝

ということで、さすがにこれだけ対処法を作っておけば、この辺りの要望には応えやすくなるだろう。

冒頭に示した危なっかしい二重ループ&grepの方法に比べると、コードの効率としても安心感としてもだいぶ改善したのではないか。

そして今回もまた、いつものようにPerl入学式のサポーターの皆さんには大変お世話になりました。
ぼくも一応サポーターなんですが、まだまだ教わってばかりです……。

ちなみに、Perl入学式は東京・大阪に加えて去年からスタートしたin沖縄、そして今年からはin北海道も増えて、ますます積極的に活動中です。
www.perl-entrance.org

近いところだと今週土曜にin東京の今年度前期の第2回が開催されます。
perl-entrance-tokyo.connpass.com

その翌週には大阪と沖縄、さらにその後に北海道もありますので、興味のある方はぜひご確認のほど。
perl-entrance-okinawa.connpass.com
perl-entrance-sapporo.connpass.com
perl-entrance-osaka.connpass.com

以上です。

(大阪の画像……)

怖くないwhile

※おもにPerlの話です。

非エンジニアながら趣味でプログラミングに入門したのが2013年の夏。そろそろ4年になろうとしているけど、Perlの基礎を学びはじめてからつい最近まで、一貫して怖かったのがwhile文だった。

怖い、というのも変な気がするが、ようはすぐに無限ループするのがつらく、それを避けたいがためにwhileを使わずに済むかぎりはなるべく使わないようにしてきた、ということ。

すぐに無限ループするとか、なるべく使わないようにする、とかいうのは、結局whileの構造というか、挙動をよく理解していないからそうなる。

理解したくないわけではないのだけど、触るとビビッと痛みを味わい、そこから回復するまでにけっこう時間がかかるので、限られた時間を使ってプログラミング学習をしている身としては、やはり距離を置かざるをえなくなっていた。

べつに無限ループしたって、Ctrl-c とかで抜けちゃえばいいじゃん、と思う人もいるかもしれないが、ぼくは普段からちょっとしたコードの実行ならVimのquickrunというプラグインを使っているので、これで無限ループをするとVimごと強制終了&再起動しなくてはならず、ときにはPerlのプロセスが走り続けてマシンのメモリがゼロになってしまい、マシン自体の再起動もできないまま電源ボタン長押しでマシンごと強制終了、みたいなことも幾度となく繰り返してきたので、やっぱりそれはなかなかつらい。
(そのうちにそこまで行く前にプロセスを切断する方法も覚えたけど、それはまた別の話)

じゃあwhileを使わないでどうしていたのかというと、ずっとfor文を使っていた。

ここで言うfor文というのはPerlでよく使われるそれで、だからC言語なんかで使われるそれではなく、foreachのエイリアスとしてのそれである。

forはいい。forは無限ループしないから。
いや、させようと思えばさせられるけど、普通に使えばまずしない。

無限ループしないループは人に優しい。

しかしながら、forでは実現できない繰り返しというのもやはりあって、それで仕方なく時々whileに付き合ううちにようやくわかったのが以下の本題。

forは「流れ」 whileは「繰り返し」

forもwhileも「繰り返し」としてひとまとめにされがちだが、実際は全然違う。

上で無限ループについて言ったように、同じことを両方にさせることも可能だが、元々備わっている性質が違うということ。

forというのは、たとえてみれば高速道路の料金所のようなものである。
ひとつのチェックポイントを上りから下りへ(あるいはその逆へ)、多くの車が次々と通過していくが、道はワンウェイで、同じ車が何度も同じ料金所を通ることはない。

だから、これを「繰り返し」と表現するのは厳密にはおかしい。

forを通して起きている現象は本来「流れ」とでも表現すべきもので、上では高速道路にたとえたが、より汎用的に言えば「滝」のようなものか。滝はいつも同じ姿をしているように見えるが、実際には同じ水は二度と同じ場所を流れない。

一方のwhileはまさに繰り返しで、同じものが同じところを何度も走る。
それはたとえてみれば競馬とか、F1とか、あるいは陸上競技の1万メートル走とか、そういう感じ。
同じ馬や車や人が同じコースを何周も走っている。

具体例で見てみよう。

forをforらしく使うには、「最初に用意したデータをすべて通過させる」という用途で用いるのがいい。

たとえば、10個の文字列の語末に一律で「!」をくっつけるとか。

#!/usr/bin/env perl
#
# for.pl

use strict;
use warnings;
use feature 'say';

my @arr = qw/aaa bbb ccc ddd eee fff ggg hhh iii jjj/;

for my $str (@arr) {

    say $str.'!';

}

f:id:note103:20170508124043g:plain

ここでは、最初に用意した @arr の中身を処理し終えたらプログラムも終わる。
プログラムの目的は、@arr の中身(aaa〜jjj)を対象として、それらに何かすること。

一方、whileがやることは根本的にそういうものとは異なる。
これの特徴を一言で言うなら、「やめろと言うまで繰り返し続ける」ということになる。

誰かがどこかで「もうやめてください」と泣いて懇願するまで、それは同じものに対して同じことを何度でも実行し続けるし、逆に言えばそれをやめさせる条件をちゃんと設定しておけば、おとなしくそこでやめる。

whileで無限ループが生じてしまう原因の大半は、結局のところ、この「やめる条件」を設定できていないことに尽きるだろう。

そして、なぜそれを設定できていないのかと言えば、それはfor文と同じように「一連の処理を通して元のデータになんらかの変更を加える」ことを強く考えてしまっているからで、相対的に「いつ・どういう条件でやめさせるか」ということにまで想像が至ってない(考えるのが後回しになってる)からである。

しかしここまでの話を見ればわかるとおり、whileを使う上で最初に考えるべき&最も重要なことは、「いつ・どういう条件でやめさせるか」の方である。
先にそれさえ決めれば、無限ループは生じない。

whileの特徴を示すには、対話型のプログラムを作るのが良いだろう。

#!/usr/bin/env perl
#
# while.pl

use strict;
use warnings;
use feature 'say';

print '>> ';
while (my $input = <STDIN>) {

    chomp $input;

    if ($input eq 'stop') {
        say 'Bye';
        last;
    }
    else {
        say $input;
        print '>> ';
    }
}

f:id:note103:20170508124100g:plain

もちろん、というか確かに、というか、whileでも先にfor文で挙げたような処理を行うことはできる。

#!/usr/bin/env perl
#
# while2.pl

use strict;
use warnings;
use feature 'say';

my @arr = qw/aaa bbb ccc ddd eee fff ggg hhh iii jjj/;

while (my $str = <@arr>) {

    say $str.'!';

}

f:id:note103:20170508124121g:plain

しかし、これならwhileを使う必要はない。というか、forを使う方シンプルだし、目的に適っている。

また逆に、for文で先のwhile文のような対話型処理を行うことも可能ではある。

#!/usr/bin/env perl
#
# for2.pl

use strict;
use warnings;
use feature 'say';

print '>> ';
for (;;) {

    my $input = <STDIN>;
    chomp $input;

    if ($input eq 'stop') {
        say 'Bye';
        last;
    }
    else {
        say $input;
        print '>> ';
    }
}

f:id:note103:20170508124144g:plain

しかしこれまで数年間Perlを触っていて、こういうコードを見たことはないし、今後もあまり見る機会はないように思える。

よって、一般的にはfor[eache]文は「垂れ流し型」、while文は「対話型」の用途で使われる(使われやすい)という前提で話を続ける。

「限定40食の蕎麦屋」と「気まぐれなラーメン屋」

必要なのは、ありありとしたイメージである。

forとwhileの違いを「ありありと」イメージできなければ無限ループは止まらない。

forとwhileの違いは、「限定40食の蕎麦屋」と「気まぐれなラーメン屋」の違いとして想像することができる。

「限定40食の蕎麦屋」は、for文のたとえである。以下のようなコードになる。

#!/usr/bin/env perl
#
# for_soba.pl

use strict;
use warnings;
use feature 'say';

my $limit = 40;

for my $count (1..$limit) {

    say "soba $count";

}

f:id:note103:20170508124211g:plain

最初に限定食数を変数 $limit に設定して、それを売り切ったら終わりである。

一方のwhile文は、「気まぐれなラーメン屋」にたとえられる。
コードにすると、このようになる。

#!/usr/bin/env perl
#
# while_ramen.pl

use strict;
use warnings;
use feature 'say';

print 'time >> ';
while (my $input = <STDIN>) {

    chomp $input;

    my $open = 11;
    my $close = $open + int(rand 12);

    say "Opening hours is $open - $close.";
    if ($input < $open || $input > $close) {
        print 'time >> ';
    }
    else {
        say 'ramen';
        last;
    }
}

f:id:note103:20170508124232g:plain

ここの店主は午前11時に店を開けるが、何時まで営業するかが日によって違う。店主の気まぐれですぐに閉めたり、22時まで営業したりする。

客は自分の行きたい時間を「13」とか「18」とか入力し、開いていれば「ramen」にありつけるが、開いてなければ開くまで時間を入力し続けることになる。

whileはこういう処理に向いている。

言い換えると、forは「何を(あるいは何回)実行するのか事前にわかっている時」に使いやすく、whileは「何回実行するのか事前にはわからない」という状況で使いやすい。

そしてそのようなwhileでは、「何回繰り返すのか」が事前には決まっていないから、自分で能動的に「やめどき」を作らなければならない。

終わりに

この記事のきっかけになったのは、BSジャパン(テレ東のBS版)で毎週放送されている『ワタシが日本に住む理由』という番組で、バングラデシュ出身の日本蕎麦屋さんの回を見たことだった。

腕の良い&こだわりのある彼は、毎朝40食分の蕎麦を打ち、それが売り切れたらその日の営業は終わってしまう。

番組じたい面白かったが、それを見ながら、「これはfor文みたいだな」とふと思った。

そして、これがfor文ならwhile文は何だろう? と考えて上のような話になった。

for文は「繰り返し」ではない。「流れ」である。
手元に溜めておいた素材を出しきったら、そこでおしまい。

たしかに見た目には「回っている」ように見える。というより、システム自体は回っているが、その上を流れるデータは回っていない。

それは新聞などを印刷する輪転機のようでもある。
輪転機はグルグルと回り続けているが、その上を流れる紙は長い1本道をスタートからゴールまで駆け抜けているだけである。

while文は「繰り返し」とも言えるが、本質はその「終わらなさ」だと思う。

それは『うる星やつら劇場版 ビューティフル・ドリーマー』のように、あるいはビル・マーレイ主演による『恋はデジャ・ブ』のように、同じ日々を何度も繰り返す。

どこかに決定的な「終わりへの抜け道」を作らなければそこから抜け出すことはできず、それが実現するまではひたすら終わらない1日を繰り返さなければならない。

whileはやっぱり怖い。

最近よく使っている自作ツール(3) select.pl

TL;DR

www.youtube.com

ちょうどひと月ぐらい空いてしまいましたが、以下のシリーズの続きです。

note103.hateblo.jp

note103.hateblo.jp

今回は、複数のファイルを直感的に選択して、それらに同じコマンドを渡すためのツールです。

リポジトリは以下。

GitHub - note103/select

で、いつものようにLICEcapを使ったGIF動画と、文章による説明とを組み合わせつつ記事を作ろうかと思っていましたが、けっこうそれも大変なんだよなあと思い直し、それでこのところ「音声入力による文字起こし」の実験をする過程で泥縄的に身に付けたスクリーンキャスト的手法で紹介してみようか、と思って作ったのが冒頭の動画(YouTube)です。

説明としてわかりやすいかどうかは不明ですが、文章で全部書くよりはずっとラクでした。

で、とはいえ、それだけではいろいろ情報が不足するだろうと思って、その補足ぐらいは文章で補おう・・と思っていましたが、やっぱりそれもまた面倒というか、いや面倒ならやらなければいいのですが、いや説明はしたい、そもそも自作ツールを紹介したいからわざわざこんな記事を書いているのだ、ということで、それならいっそその動画で喋った音声もそのままGoogleドキュメントの音声入力機能でテキスト化しちゃえばいいのでは、と思ってそうしてみました。

で、さらにその際、なんだかメタっぽいですが、せっかくなのでその音声入力による文字起こしの模様も動画に撮ってみたので、記事の最後に貼り付けておきます。興味のある人はご高覧のほど。

ということで、上の動画で喋った内容をGoogleドキュメントの音声入力でテキスト化した上で、簡単に手直ししたもの(それでも40分ぐらいかかったけど)を以下に掲示して本記事を終わりたいと思います。

このシリーズ自体はまだ続きがあるので、また時間ができたら更新したいと思っています。

Transcript

[00:00]

  • ええ、はい。今日はですね。これ、プログラミングの、いつも練習をですね、記録しているブログに、動画でですね、ちょっとのせてみようかと思っておりまして。
  • `test`っていうディレクトリで、ちょっとやってみようかなという感じですけど。
  • 今日紹介したいコードはですね、`select.pl`ってやつで。詳しくはですね、GitHubとかにも上げてるはずなので、ブログの方で紹介したいと思いますけども。
  • 私は今この`e`っていう名前のエイリアスを入れてるんですけども。
  • こんな感じですね。複数のファイルをこんなふうに選択して、選択するとこういう`+`マークがつくんですね。
  • で、デフォルトでただ`e`って入れるだけだと、echoするよって、選択したファイルを全部echoするよ、みたいな感じになるんですね。
  • で、そのままやっていいかなってなった場合は……(実行)はい、ちょっとわかりづらいですけど、フルパスで全部出るんですね。3つファイルがあってですね、全部echoされたと。

[02:00]

  • で、それの何が嬉しいのって感じなんですけど、eあるいはそのselect.pl をこれPerlで書いてるんですけど、実行するときの引数にですね、コマンドを何か入れると。たとえば、こんな感じでcatするよと。`e cat`って入れて、 `yes` ってやるとですね。
  • この`project1.txt`ってやつの中身がcatされて、`project3.txt`の中身もcatされて、みたいな感じでですね。ようするに、複数のファイルを選択して、それに同じコマンドを渡すみたいなことをやりたいと。
  • んで、そんなのべつに普通にやればいいんじゃないの、みたいな感じもするんですけど、なんでこんな、ハッシュ値が入っているような長いファイル名にしているのかっていうことが、これを作った理由を示しているんですけど。
  • これをですね、たとえばcatしようって言ってですね。project2なんかを選ぼうと思ってもですね、めちゃくちゃ長いファイル名だと、めんどくさいわけですね。
  • たとえばproject2と、なんかを選びたいなと。projecct2とproject4を選びたいなってときにですね、2015……とか書いても、これでタブを押しても駄目なんですね。2016の……05の……とかやってるとですね、いつまでも選択が終わらないぞ、みたいな。
  • 2017のこれを選びたいので‥‥とかやってもですね、めんどくさくてしょうがないですね。
  • まあ、できますけど、それもこれでやれば、これで選ぶと、楽ですよね。
  • ちなみにもう1回選択すると、消えたりとかするんですけど。
  • まあ、それだけっちゃそれだけですけど(笑)。

[04:40]

  • あとついでにっていうか、`de`っていうエイリアスを仕込んでるんですけど、これ消したいなという時ですね。複数のファイルを消したいと。
  • そういう時は、これ私は`trash`っていうコマンドが入ってるんですけど、ない場合は、まあ`rm`とかでもいいんでしょうかね。
  • 私はこれを仮のゴミ箱がこういう場所にあるんですが、ここにtrashしたいと。(実行)で、どうなったかっていうとですね。1,2,4のファイルしか、もう残ってないと。
  • 複数のファイルをどっかに飛ばしちゃいたいという時にこれはよく使いますね。
  • あとまあ、ちなみに、不可視ファイルをですね。捨てたいなっていう時も時々あるんですね。
  • そういう場合は、`de.`っていうのでやると、ちゃんとそれも飛んでってくれると。
  • 逆にっていうか、そのまま`de`だけだと不可視ファイルは出ないようになってるので、こういうコマンドならこれも消せるよと。
  • そういう感じにしてたりします。

[06:30]

  • あとは複数じゃなくて、1個だけファイルを選んで何かやりたいっていう時は、また別にこれだけで済ませるコマンドを作ってるんですが、またそれは必要があれば紹介するかもしれないですけども。
  • まあどんどん消しちゃって。無くなっちゃいますけど。
  • という感じで、そういうselectコマンドを紹介してみました。

余録

www.youtube.com

※音声入力による文字起こしについては以下をご参照。
21世紀の文字起こし - the code to rock
21世紀の文字起こし(2) - the code to rock

補足

動画の中で、エイリアスのことをちらちら言っていました。補足的に以下に示しておきます。

alias e="perl /path/to/select.pl"
alias de="perl /path/to/select.pl 'trash -r'"
alias de.="perl /path/to/select.pl -a 'trash -r'"

alias d="sh /path/to/delete.sh"
alias d.="sh /path/to/delete.sh -a"

最後にオマケ的に紹介していた`d`, `d.`などは、すでに前回紹介していたdelete.shのエイリアスでした。
最近よく使っている自作ツール(2) delete.sh - the code to rock

プログラミングを学ぶ理由

ひさしぶりに彼女とお茶を飲んで、まったり雑談していたら、不意に

あなたがそんなに好きなプログラミングっていうの、私に教えてみてよ

と煽ってきたので、彼女のMacBook AirのシステムPerlで、ターミナルから「Hello」が出力されるだけのコードを目の前で書いてみた。

print 'Hello';

これをhello.plで保存する。
shebangさえ付けない。改行も入れない。ほんとに最小限。

実行。

$ perl hello.pl
Hello

この「Hello」って書いてある部分を書き換えると、出力される内容もどんどん変わっていくんだよ、と言ってそこをいろんな数字や言葉に置き換えていく。

print 'Hi';

実行。

Hi
print 1234;

実行。

1234

ほらね、という感じで様子を見ると、「これの何が面白いのか、まったくわからない」と言う。

なんで「Hello」って出さなきゃいけないの? 「1234」ってなに? 何か意味あるの? と言う。

あなたもPerl入学式で、人に教えたりするの? それはすごいね。私みたいな人だったら大変じゃない? だって本当に、何もわからないんだから。と言う。

いや、そうじゃない。とぼくは言う。

問題は、プログラミングをやりたいと思っているかどうかなんだよ。「わからない」なんて当たり前なんだよ。だって、最初は誰だって何も知らないんだから。

でも、「プログラミングをやりたい」って思う人は、わからなくてもわからないままやるんだよ。わからなくても、わかるまでやる人が「プログラミングをやりたい人」なんだ。

だから、そういう人に教えるのは何も難しくない。その人がわかるまで付き合ってあげればいい。「わからない」なんて前提なんだ。わからなくても、それはつまずきでもなければ間違いでもない。息を吸ったり水を飲んだりするのと同じだ。それは必要なことですらある。だから困ったりしない。

難しいのは、「プログラミングをやりたくない」という人に教える場合だと思う。そういう人にとっては、「わからない」ということが致命的な問題になる。「何のためにやるのか」ということが問題になる人に教えることは、だから難しい。

「プログラミングをやりたい」という人は、すでに「何のために」がはっきりしているか、そんな目的すらなくてただ何となく「やりたい」という人だから、ぼくらがわざわざ「プログラミングが何の役に立つのか」とか、「どこが面白いのか」なんて教えてあげる必要はない。その人たちはすでに学ぶこと自体を楽しんでいるし、「わからないから」という理由でやめたりはしない。

でも自分以外の意志によってやらされている人は、何かそれを「やる理由」がほしいと思うだろうね。「楽しみ」や「目的」を外部から提供してもらう必要がある。それが悪いってことじゃない。自然なことだ。ぼくだってプログラミング以外の何かに関してはそうだと思う。

そのうち小学校とかでもプログラミング教育をやるらしいけど、それはまた別の話で、基礎的な教養として教えるものだろうから、算数や漢字や英語に馴染ませるようにプログラミングに馴染ませるのはいいことだと思うけど、そういうことじゃないなら、まあ、やりたい人がやればいいことで、ということは「プログラミングの何が面白いのか」を他人が教えてあげたり、他人から教えてもらったりする必要はどこにもないってことになるだろうね。