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に戻ってきたところでエラーを吐いて、5に至る前に死んでいる。

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

# 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:一応業務もやっていたけど、解決しないまま夜になるとは思っていなかった。

変数は「箱」か?(3)

概要

  • 本記事では、「プログラミング入門者に初めて《変数》を教えるとき、どのように説明することが適切か」ということ、そして「その際に《箱》を喩えに用いることで生じる問題を、別のどのような説明を用いることで解消できるか」ということについて考える。
  • この際、まず何よりも重要なことは、「議論の前提を明らかにし、それを一致させてから議論をスタートする」ということである。
  • 他の場面で他の人が似たような話題をとり上げているかもしれないが、本記事と同じ前提や問題意識による議論であるかはわからない。似たような話題だからといって、その目的や前提も安易に同一視してしまうと、関わる皆の時間が無駄になってしまうので、注意が必要である。
  • 箱以外の説明の仕方には、さしあたって良いものが二つある。
    • 変数を「名前を付けたデータ」であると説明する
    • 変数を「あだ名」に喩える
  • ただし、変数の利便性を伝える最も効率的な方法は、それを使った場合と使わなかった場合とのコード例を比べて見せることである。
  • このような問題提起に対しては、「しょせん比喩に過ぎないのだから、その疑問自体がおかしい」という定番の応答がよくあるが、それはメタレベルからの視点にもとづいており、疑問の真意を直視していない。
    • 「比喩」が「現実ではないこと」を理解できない人などいないし、稀にそのような人がいるとしても、このような疑問を呈したからという理由でそう決めつけるのは非論理的である。
  • 本記事は、「箱を使った喩えが悪い」という主張でも、「そのように喩えた人が悪い」という主張でもない。より良い変数の説明の仕方を考えることを目的としている。
  • 論理を丁寧に積み上げていくと、以前に採用していた粗雑な論理には戻れなくなる。そして、箱の喩えに疑問を感じた人に対する「比喩と現実を並べて考えてはいけない」という意見は、その雑な論理から導かれている。
  • そのような粗い論理の段階を早々に抜け出す一助になればと思い、これを書いた。

はじめに

この話については、すでに2度も長文を書いていて、

変数は「箱」か? - the code to rock
変数は「箱」か?(2) - the code to rock

かつ、今それらをざっと見返してみたら、今回書こうと思ったこともほぼ書いてあったので、じゃあそれでいいか、という気もしたのだけど、話題自体はまだ時々見かけるというか、以前にまとめたそれらの考えがあまりちゃんと外に届いていないようなのと、あとは前に書いたものの方は自分でもまどろっこしいというか、その時点でも「あまり上手く書けないなあ」と思いながら書いていて、しかしそれからしばらく時間が経って、もう少しシンプルに「こんな感じかなあ」というふうに思えるようになってきたので、今の視点からあらためて書き直してみたい。

前提を定義する

変数を箱に喩えるのがいいのか悪いのか、あるいは「良くも悪くもないけど何らかの問題がある」のか、それとも「問題なんてない」のか、みたいな議論が起こるとき、どうもありがちなパターンとして、異なる見解をもつ人同士が「同じ前提」を共有していない、ということがあるように思う。

前提や目的が一致していないと、途中で「そもそも何が問題なんだっけ?」みたいになりやすい。

そうなると、時間の無駄とまでは言わないまでも、やはり不毛感や消耗感から逃れがたい。
誰もそんなものは望んでいないのだから、まずは「前提」を一致させ、共有しなければいけない。

では、この記事における「前提」は何かというと、変数を「箱」に喩えることに対する、ぼくが持っている次の意見に集約される。

 プログラミングを学ぶ初心者に対して、「変数」の使い方を初めて教えるとき、変数を箱に喩えて、「値を箱に入れるイメージ」を伝える人がいるけれど、現実の世界で箱に物体を入れてしまったら、その物体を複数の場所から同時に取り出すことはできないのだから、本来そのようなことが可能になる変数の有用さを、箱のイメージでは伝えることができない。
 たしかにその喩えは、値を「格納」するイメージを伝えることには適しているが、「ひとつの値を複数の場所で同時に使える」という変数の性質を一緒に説明できる別のイメージがあった方が良いのではないか。

まあ、これだけと言えばこれだけである。

これに対しては、「いやいや、こういうときはひとまず値を《格納》するイメージだけでも伝えられれば良いのだよ。複数の場所でどうした、なんていうことはまた別に説明すればいいのだ」という意見もあると思うし、そういう意見があるのはまったく構わない。

重要なのは、どちらの意見が正しいのかということではなく、ましてやどちらか一方の意見に統一することでもなく、「何を目的にこの話を進めるのか」という、前提を一致させておくことである。

前提に関する行き違いを解消する

しかしながら、上記の前提を受け入れられない(同意できない)という人もいると思う。

というより、冒頭に書いたこととも繋がるが、この話で行き違いが生じるとすれば、その大半は、上記の前提を受け入れられるか、受け入れられないか、という違いに起因しているようにも思える。

上の話に対して「なるほど、たしかにそうだよね」と思えない人の中には、おそらく「しょせん比喩じゃないか」と思う人が少なくないだろうと想像する。

「しょせん比喩じゃないか」に続くのは、「比喩に過ぎないものと現実世界の物体とを繋げて考えるなんて、馬鹿げている。考えるだけ時間の無駄だ」といったものだろう。

そして、このズレこそが、もしかすると本件における最大の論点なのではないかと個人的には思っている。

「箱に喩えたら矛盾が生じるじゃないか」というぼくの疑問は、上の「前提」の最初に出てくる、「プログラミングを学ぶ初心者」の視点である。

一方で、「そんな疑問は馬鹿馬鹿しい」という回答は、プログラミングをすでに身につけた、「教える側」の視点によるものである。

この二つの視点は、同じ地平に立っていない。永遠に交わらない二つの線分である。

じつを言えば、最初にぼくが「箱」で説明を受けたとき(そう、「箱」で説明を受けたのだ)には、べつにそれを不思議とは思わなかった。
よく言われるように、その喩えを「わかりやすい」とすら思った。

しかしながら、自分で変数を使ってあれこれやれるようになってから、「あれ……でも、箱じゃ喩えとして変じゃないか?」と思うようになった。
どのような意味で「変」だと思ったのか、何が矛盾すると思ったのかということは、上に書いたとおりである。

これを聞いて、「箱でよい」と思っている人は「そら見たことか」と思うだろう。

「キミは箱の喩えを聞いて変数を理解したのだろう? そして自分で変数を使えるようになって、後から矛盾に気づいたのなら、それはキミが成長したということだよ。キミのその成長こそが、箱の喩えが学習に適している証拠なのだよ」と。

しかし、それは話がズレている。

ここでの議論の目的は、「初学者が最短コースで変数を理解すること」ではない。
「最終的に変数の性質をちゃんと理解できること」でもない。

ここで明らかにしたいのは、「箱に喩えることによって生じる矛盾を、何か別の説明の仕方で解消できないか?」ということだ。
「箱では変じゃないか?」というのはそういう疑問である。

その疑問に対して、「最終的に理解できるんだから問題ない」というのは答えになっていない。
視点が一段、メタレベルに上がってしまっている。

「最終的に理解できることがいいことかどうか」という問題なら、誰だって「理解できるのはいいことだ」と答えるだろう。

また、もしぼくが「箱に代わる説明の仕方さえわかれば、最終的に変数を理解できなくたっていい」と主張しているなら、それに対して「経緯はなんであろうと、最終的に理解できればいいんだよ」という反論も成り立つが、そういう話もしていない。

ここまで書けば、「比喩に過ぎない」とか「わかればいい」といったことはもう言う必要がなくなるのではないだろうか。

というかまあ、そもそも箱の喩えが「比喩に過ぎない」ことがわからない人などいるのだろうか? という疑問もあるのだけれど。

それが比喩に過ぎなくて、現実そのままの話ではないことなど当然わかっている。
それが比喩であるという前提で、「比喩として」もっと良いものがあるのではないか? と言っているのである。

このような質問をした人が、なぜ「比喩と現実の区別がつかない人」として扱われなければならないのか。そこにもまた、じつはこの議論を左右する核心が隠れているような気がしているのだけど、その問題については追々回収していきたい。

参照渡しは関係ない

さて、大前提についてはすでに説明したが、より具体的に話を進めるためには、もう少し細かい前提を補足していく必要がある。

よくこの話をしていると、「変数といっても、値渡しの変数と参照渡しの変数で性質が違うからなあ」みたいな意見を聞くことがある。

しかし、ここでぼくが想定しているのは、値渡しに使う変数でも、参照渡しに使う変数でも、どちらでもよい(というか関係ない)ものだ。

繰り返しになるが、ここで前提としているのは、「プログラミング初学者に初めて変数を教えるとき」のことである。

プログラミング初学者に初めて教えるのが「参照渡し」を主とするプログラミング言語でも、「値渡し」を主とする言語でも、その初学者にしてみれば大した違いではない。

それに、これも上に書いたように、ここで言う「箱の矛盾」というのは、「一回代入しただけで複数の場所で同時に使えるようになる」という変数の現象を「箱」では説明できない、ということである。

この点に関しては、値渡しであろうが参照渡しであろうが変わらないはずである。

つまり、これもまた「前提」の問題であって、今回ぼくはそのような前提を持っているが、そのことを知らずに「変数と言っても値渡しと参照渡しがあるから……」と考えはじめてしまったら、それは無用な回り道になる。

配列も関係ない

また同様に、「配列」も関係ない。

たしかに配列を説明するときには、ぼくも「箱」(というか下駄箱など)は有用な喩えとして使えるかもしれないと思う。

しかし、ここでの前提に配列は関係ない。プログラミングの初心者に配列の説明から入るケースはほとんど無いだろうし、そのような教え方があるとしても、ここではそれを前提としていない。

理想的な変数の教え方

ここからさらに、具体的な点を補足する。
ようやく少しだけコードも出てくる。

もしぼくが、初心者に変数の概念を教えるなら、たぶんこういう順番で教える。
見てもらえればわかるが、ここでは喩えを一切使わない。

まず、以下のような式を並べる。
Perlに慣れているのでPerlで)

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

say 13 + 1;
say 13 - 2;
say 13 * 3;
say 13 / 4;
say 13 % 5;

これを実行すると、こんな。

14
11
39
3.25
3

次に、このプログラム内の「13」をすべて「21」にしましょう、と言う。

Vimなどを使っていると一瞬でできるけど、ポチポチ書き換えているとけっこう面倒くさい。

ましてや、ここでは5行しかないけど、もし業務で100行も200行も同じことをする羽目になったら結構つらい。
ということを軽く言っておく。

ちなみに、「13」を「21」に変えて実行するとこうなる。

22
19
63
5.25
1

さてそして、ここで変数を紹介する。
まずは先ほど「13」から「21」に変えた場所に、今度は変数xを入れてもらう。
こんな感じで。

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

my $x = 21;

say $x + 1;
say $x - 2;
say $x * 3;
say $x / 4;
say $x % 5;

実行結果は前述のとおり。

そしてさらに、満を持してという感じで、「21」を「13」に戻してもらう。
もちろん、こうなる。

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

my $x = 13; #<= ここだけ変更

say $x + 1;
say $x - 2;
say $x * 3;
say $x / 4;
say $x % 5;

先ほどは5行分の修正が必要だったが、今度は1箇所変えただけで終わった。

そしてここでまた、「これがもし100行や200行あったらどうなったか」とか言ってみる。

変数というのは基本的に、こうやって「それがなかった場合に比べてどれだけ便利になるのか」を示しながら教えるのがいいと思う。*1

だから、そうではなく「言葉」で説明するとき、つまり箱などの喩えを使うときというのは、あくまでその本題的な説明(実際のコードを示しながらの説明)に入る前のイントロ程度のもの、と考えるのが合理的だと思える。

喩えを使わない説明 〜名前を付けたデータ〜

そこまでを踏まえた上で、ではその「言葉」で説明するときに、箱の喩えを使わずにどう説明したらいいのかといったら、以下の二つのあり方を示したい。

  1. 「名前を付けたデータ」と説明する
  2. 「あだ名」に喩える

まず前者だが、個人的には、ほとんどこれだけで充分という気もしている。

これの元ネタは、ござ先輩こと湯本堅隆さんによる以下の本である。

独習Python入門――1日でプログラミングに強くなる!

独習Python入門――1日でプログラミングに強くなる!

ぼくはいつも、入門書のたぐいを読むときには避けがたく「この本は変数をどう説明してるのかな」と、その本の試金石のようにそれをチェックしてしまうのだけど、この説明にはかなり関心してしまった。*2

非常にシンプルで、まあ身もふたもないとも言えるけど、いずれにせよ著者がちゃんと自分の体験と考えにもとづいて出してきた説明だと感じる。

そしてこの説明を見て、「べつに何かに喩えなくてもいいのでは?」という思いも少し強くなった。

喩えを使う説明 〜あだ名〜

と同時に、「何かもっと適した喩えはないのか」と考えてきた身としては、それはそれでちょっと悔しいというか、何か具体的な物事に喩えたい気持ちも落ち着かない。

そんな中で、そういう「喩え系」として現状一番いいかなと思うのが、上記後者の「あだ名」である。

まあニックネーム、あるいはTwitterなどのWebサービスのアカウント名も近い感じだろうか。

一瞬話がズレるが(と言っても変数の話だが)、以前に基本情報技術者試験の勉強をしていたら、本の中で「変数というのは中身の値がコロコロ変わっていくから《変数》と言うのだ」と説明していて、それもけっこう目からウロコだった。

だったら「変数」ではなく「変値」とかにしてくれたらもう少し直感的だったのに、とも思ったが、それはそれとして、「中身がコロコロ変わる」という性質は変数を語る上でたしかに重要である。

上に示したコード例でも、「13」が「21」に変わり、それがまた「13」に戻っていく。

そのまま話を戻すと、人のあだ名というのも、中身は交換可能である。

まあ「永ちゃん」と言えば矢沢永吉だし、「ミスター」と言えば長嶋茂雄だが(たぶん)、それは定数みたいなものだとして、たとえば「健ちゃん」とか「よっしー」とか「ジョニー」とかは、地域や時代によって、対象となる実在の人物がバラバラのはずである。

ここでまた一瞬離れてしまうが、ぼくがプログラミングに入門して、エンジニア界に近づいていろいろ受けたカルチャーショックの中でも、とくに印象的だったのは「みんなハンドルネームで呼びあってる」ということだった。
「本名は知らないけど、Twitterのアカウント名は知ってる」みたいなケースがけっこう多い。

これって何かに似ているなあ、とその時に思ったのが、昔はデイヴ平尾とかジョニー大倉みたいに外国人の名前をニックネームにして、「よう、ジョニー」とか言い合ってたんだよなあ、見たことないけど。なんかそれに似ているなあ、ということだった。

あらためて話を戻すと、その「あだ名」とか、あるいは「肩書き」、またあるいはその上位概念である「名前」などはじつに「変数的」である。

たとえば居酒屋で、そこに同席していない部長や社長についていろいろ話題にするとき、その場に部長や社長本人がいなくても、「このまえ部長がどうしたこうした」と言えば、皆の頭の中にその人の姿が思い浮かぶ。

これは変数が同時に複数の場所で呼び出され、それぞれが同じ値を取り出していることによく似ている。

そして「部長」や「社長」の中身が交換可能であったり、あるいは「よっしー」の中身が高校時代の友達である吉沢くんと会社の同僚である吉田さんとで交換可能であったりすることも、また変数的である。

ここでは、箱に見られた矛盾(破綻)が解消されている。

逆に、もし同じ状況を箱で再現しようとしたら、たとえば「部長」と書かれた箱の中に実在の部長が入らなくてはいけなくなる。
そしてそうなれば、複数の異なる場所で同時に部長の話をすることもできなくなる。

そう考えると、そもそも物体に喩えることに問題があるように思えてくるが、ある現象を何か他の事象に喩えるメリットというのは、複数の異なる人間が「ありありとした実感」を共有することにあるので、必ずしも物体に喩える必要はないし、物体以外のもの(つまりあだ名や肩書き)に喩えることでそれを実現できるならそれでいい。

「わかりやすさ」とは何か

ということで、ひとまず結論的なことも書いたので話は終わりに向かうが、まだこの文章で触れていなかったことについていくつか触れておきたい。

まず、これは少し前にTwitterにも書いたのだけど、「箱に喩えればわかりやすい」などと言うときの「わかりやすさ」とは、「その情景をイメージしやすい」という意味でのわかりやすさであって、「変数の性質を理解しやすい」ということとイコールではない。

思うに、「箱ならわかりやすい」と言う人はそこをごっちゃにしている。

「箱にモノを入れる様子」がイメージしやすいことについては、ぼくも同意できる。

しかし、それは「言葉から映像を想像しやすい」ということであって、「変数の性質を理解しやすい」ということとは別である。
まずはそこを分けて考えるべきである。

プログラマーは本当に箱を想像しているのかもしれない

と同時に、ぼくは最近、プログラマーは変数に値を代入しているとき、実際にモノを箱に格納しているかのようなイメージを頭の中に思い浮かべているのかもしれない、とも思っている。

というか、むしろプログラマーが普段からそういう状況を頭に思い浮かべているからこそ、変数を「箱」に喩える説明が生まれたのではないかと思っている。

ぼくが「箱に入れてしまったら、複数の場所から同時に取り出せないではないか」と言ったとき、「それとこれとは別の問題だ」という反応があるのは、現実空間にある箱と、プログラマーの頭の中にある箱とが、異なる性質を持っているからではないだろうか。

もしそうであるなら、教える側(プログラミングをすでに身につけた人)が、教わる側に「箱」と言ったとき、それは初めから「現実には存在しない不思議な箱」を意味している。

またそうであるなら、「現実世界に存在する箱と性質が違うよ!」なんて言ったところで、「何を自明なことを……」と思われてしまうのも理解はできる。

しかし一方で、このように「変数と箱」の問題がくすぶり続けるということは、その教える側の「自明さ」が、必ずしも教わる側にきちんと伝わっていない、ということなのではないだろうか。

そしてこの点を解消できないかぎりは、同様のコンフリクトがもうしばらく続いてしまいそうな気がしている。

それでも大切にすべきこと

さてその上で、もしそのように、認識のズレにある程度避けがたい経緯があるとしても、より優先すべきことは、「箱に喩えるのは変じゃないか?」といった疑問が挙がったときに、上に書いたような「メタレベル」の回答をしたり、相手の真剣な疑問を踏みつぶしたりしてしまわないことである。

よくTwitterでは、大人の矛盾した説明に対して素朴な疑問を提示した小学生が、教師から理不尽に叱責されるエピソードなどがRTで回ってきて、「こんな教師は嫌だ」みたいな話が盛り上がるけれど、そのわりにこういう疑問に対しては冷淡というか、情の薄い反応が少なくないのは不思議というか、むしろ人間の本性がそのようなものだから、そういう小学生のエピソードもなくならないということかな、という気もしてくる。

「人」を否定しているわけではない

その話とも繋がるが、この変数と箱の話題に対しては「なぜそんなことを言うのかわからない。まったく無意味である」という、攻撃のようにも受け取れる感情的な反応が少なくないと感じるのだけど、それはもしかすると、「箱」を例に使って教えたり、教わったりしてきた自分を否定されたように感じさせてしまうからかな、と思うこともある。

だから明記しておくのだけど、これはそういう例を使っている「人」を否定する話ではない。

結果的に、この話題によって自分を否定されたような気持ちになる人はいるかもしれないし、それを100%避けることもできないのだけど、目的はそこにはない。

目的は、「もっと良い説明はないだろうか? それを考えてみたい」ということである。

極められた思考がもたらす不可逆な論理

ここまで長く書いてきたのは、「ある種の思考の道筋は、一度通ったら戻れなくなる不可逆性を持っている」と考えているからだ。

どれだけ意見の異なる人同士でも、「それでも一致する共通見解」というものはあるはずで、それが見つかってしまうと、もうそれより前には戻れない。

少し前に、天動説と地動説の盛衰について語った面白い記事があったけど、

gendai.ismedia.jp

どれだけ多様な観点から考えたとしても、現代において天動説を本気で(地動説より)支持する人は出てきえない。

深い思考を丹念に積み重ねれば、「たしかにそこまでは言えるよね、そこまでは同意できるよ」という共通見解を作っていくことができる。

それは、「もう今から天動説には戻れないよね」というのと同じで、「ここより前には戻らない」という不可逆な地点の発見でもある。

「箱」で説明を受けた人が、「それだと矛盾しますよね?」と言ったときに、「ただの喩えだよ」と答えるのではあまりに雑すぎる。
というより、それはまともに答えられないことを誤魔化しているだけである。

「箱」で説明するのが悪いわけではない。上にも書いたが、説明する人の頭の中では実際に「値を箱に格納するイメージ」が動いているのかもしれないし、もしそうなら、それはもはや「喩え」ではなく「事実の伝達」である。

でも、その説明で納得できないという人がいるなら、「ただの喩え」などと回答するのではなく、一体どこに疑問を持っているのか、それを解消するにはどんな説明が必要なのか、一緒に考えてみてほしい。

そんな暇がないのであれば、非情な言葉を投げつけるのではなく、静かに離れてあげてほしい。

すでにここまでは考えた。ここより前の段階には、もう戻らないでほしいなあと思っている。

*1:変数に限らず、新しいことを教える際には多くの場面で有効な方法だと思う。

*2:厳密には「名前を付けられたデータ」と説明されているが、こちらでそのように書くと少し冗長になるので、「られ」は端折らせてもらった。

Qiitadonについて語る

f:id:note103:20170602192506p:plain

f:id:note103:20170602192525p:plain

というわけで、Qiitadonについて書いてみたいと思います。

Qiitadonは、今週の初めにオープンしたQiita/Incrementsによるマストドンインスタンス

qiitadon.com

公式アナウンスはこちら。

速報系としてはこの辺とか。

その他、マストドン全般についてはITmediaの以下の連載を見ていればほぼ100%追えるはず。
www.itmedia.co.jp

初期の盛り上がりみたいなものについてはぼくもこの辺に書きました。
scrapbox.io

もうひと月以上経ってるのかあ……。

さて、そのQiitadonですが、IT企業系だとぼくの知るかぎりpixivのPawooドワンゴfriends.nicoに続いて3つめの参入。

pixivもドワンゴ(というかニコ動)もそれぞれ核となる自社サービスおよびそのコミュニティみたいなものがあって、それと連動した感じで展開していることを思うと、たしかにQiitaもそういう面があるので、それでやることにしたのかなあ、とも思ったり。

一方、friends.nicoは先行するmstdn.jpにちょっと似て、ノンジャンルに近いインスタンスに見えるんだけど、Pawooは明らかに絵に関心が強い(描くのも見るのも)ユーザーが集まっていて、その意味ではQiitadonはすでに技術系のネタに興味のある人が集まっている感じがするので、Pawooに近いように思える。

さらに一方、Pawooの方はけっこうどしどし投稿が流れていくんだけど、Qiitadonの方はそうでもない。
これはたぶん、Pawooの方は半匿名的に参加できたり、自分の作品をどんどん投稿していくような文化があるのに比べて、Qiitadonの方はけっこうリアルな立場で参加している人が多そうというか、なんか変なことゆったときに自分がこうむる影響が少なくなくて、それで若干自重ぎみになるのかなという感じはする。

マサカリ回避傾向というか。

まあ、とはいえ個人的にはそれはそれ、ひとつの特色というか、インスタンスごとに持っている性格の一部として捉えればいいのではとは思っている。

その話にもちょっと近いけど、Qiitaにはすでにけっこう独特のコミュニティ感みたいなものがあって、個人的にはちょっとはてなに似ていると思ってる。

最近だとコミュニティ・ガイドラインを発表したら軽く賛否両論になったり。*1 *2

まあ、話題にのぼるというのはそれだけ関心が寄せられているということだから、それ自体はいいことだろうけど、どうも中の人の意向がうまく外部に届いてない気がするなあ、という印象は持っていた。

賛否両論の「否」の方がなんで出てくるのかっていうと、いろんな要因があるとは思うけど、けっこう大きいのは「中の人からの反応がナイから反応が来るまで強く叩いてしまう」という、人間が本来的に持っているある種の幼さというか、抱えたフラストレーションを我慢できずに発散してしまう、みたいなところがあるんじゃないかと個人的には思っている。

Qiitaはその辺に対しても真摯に取り組んでいるとは思うのだけど、そこでさらにこのQiitadonが機能することによって、その辺の「ユーザーと中の人との間を埋める潤滑油」みたいな感じになるのでは、とちょっと期待している。

もう少し具体的なことでも、いろいろ期待できることはある。

たとえばこれも「隙間を埋める」という感じだが、「ちょっとした技術系の気づきを得たけどQiitaに書くほどのボリュームでもない」とか、「Qiitaに書くにはちょっと技術要素が弱い(いわゆるポエム)」みたいな話について、さらっと投稿できる場所になれば良さそうだと思う。

案外、人は最初のきっかけというか、入り口さえ見つかってしまえば、だんだん書いているうちに「ちょろっと書くだけのつもりだったのにけっこうなボリュームになったな」みたいなことになりがちなので*3、Qiitadonが呼び水になってQiitaへの記事投稿につながるかもしれないし、自分では「大したことない」と思っていたネタが思いのほか多くの人に役立った、なんてことにもなるかもしれないし。

Qiita本体は上でもちょっと触れたように、ガイドラインが必要になるほど「どこからどこまでが《技術ネタ》なんだ?」みたいな範囲の問題とか、あるいは「こんなネタ誰も必要としてないよな……」みたいな基準的なことで投稿を躊躇してしまう部分もあるかもしれないけど、Qiitadonの方は、

技術に関する話題に限らず、気軽に投稿してください:)

とのことなので、いろんな意味で基準ゆるめで使ってみればいいのではないかと思う。

ぼくは今までmstdn.jp, Pawoo, friends.nico というインスタンスを使ってみたけど、やっぱりマストドンってローカルタイムラインがけっこう特徴的というか、それを見て楽しめないと単に「500字書けるTwitter」というだけみたいになりがちなので、現在ノンプログラマーとしてプログラミング入門中の身としては、技術ネタの多いこのインスタンスは大変貴重でありがたい。

今は試験公開中ということだけど、そんなにほんと大盛り上がりとかはしなくてよいので、細く長く続いてほしいなあと思っています。

ノンプログラマーに求められるもの

ノンプログラマーがプログラミングを覚えることにより普段の業務負担を軽減させる、みたいな流れが徐々に活気を帯びてきたというか、この本なんてその象徴のようにも思えるのだけど

www.oreilly.co.jp
ふと我が身を振り返ってみると、ぼくが趣味でプログラミングをやってるのって、そういう理由というか目的というかモチベーションではあまりなくて、もちろん、そういう効果があれば嬉しいし、実際にはあちこちで発信しているように、プログラミングを習得すればするほど「今までこの作業ってVimPerlやGitを使わずにどうやってたんだっけ……思い出せないし思い出したくもない……」と思うぐらい様々な負担が軽減、またはそもそも不要になっていたりするのだけど、でも気持ち的に、なんでプログラミングをもうかれこれ4年近く続けてやってきたのかっていうと、それはたぶん

《日々の業務があまりに地味かつ長期的で、目に見える効果というものがわかりづらくて、それはまるで人が年齢を重ねるように「昨日と今日の違いがほとんど認識できない」ものだから、そういうものとはまったく逆の「一瞬で目の前の世界がガラッと変化する」ような体験を求めて、プログラミングとかテクノロジーとかに希望を託している 》

ということなんじゃないかなあ、と思った。

言い換えると、日々の業務の負担が減るっていうのはたしかに嬉しいことで、村上春樹いうところの「小確幸」なわけだけど、じゃあそれがわざわざ貴重な時間を割いてまでやる「目的」とか「欲望の対象」になるのかっていうとそれにはちょっと弱くて、欲望の対象としてはだから上記の「これまでに見たことのない風景を見てみたい! 味わったことのない気分を体験したい!」みたいな気持ちがあって、結局はそれに向かって一生懸命コンピューターに向かい合ってるという感じがする。

それはほとんど人生を賭けた、生きる意味やテーマのような営みだから、その手段はプログラミングでなくたって構わないとも言えるんだけど、さしあたって現時点ではその過程にプログラミングがあるからそれに時間をかけている、というような。

でも実際には、プログラミングというのは自分が手を動かして英数字などを打ち込んでいかなければ(少なくとも2017年の現時点では)身についていかないわけで、頭の中で「こうなってほしい!」とか念じるだけでは出来てくれない。

何を言いたいのかというと、これってやっぱり一朝一夕に習得できるものではなくて、どうしても時間はかかるということ。

で、それって何に似ているかというと、たとえばスポーツとか、語学とか、あるいは楽器の練習。高い楽器を買って、さあやるぞ! となってもそれが身につくまではずっと自分の体を動かしてくり返し練習しなければならないわけで、さらにはそれを「趣味」でやるということは、普段の仕事は仕事として頑張った上で、その合間に、つまり本来なら娯楽を楽しんだりゆっくり体を休めたりできたはずの時間をつぶしてそれをやらなきゃいけない。

さらに言うなら、結局はそんなふうにわずかな時間を繋いでやっているだけだから、プロとしてやっている人たちとの差はみるみる開いていくわけで、いやそんなの当たり前なんだけど、でもどうしたって「自分より年下のあの人はあんなに出来るのに、なんで自分の上達はこんなにも遅いんだ……」って比べて思ってしまうのもまた自然というか。

つまり、趣味でやるっていうのは結構大変。上達の実感はいつも理想の遥か後ろを遅れてやってくるし、当初憧れたようには全然できないまま時間がどんどん過ぎていく。

それでもぼくがここまでそれを続けてきたっていうのは、だから「日々の業務の負担を軽減したいから」というだけのことではなくて、その向こうにある「なんか知らんけどスゴイ体験をしたい!」という感覚に揺り動かされてきたからだと思う。

そのある種の貪欲さというか、しつこさというか、諦めの悪さみたいなものに引っ張られてダラダラやっているうちに、結果として「なんか知らんけど業務もめっちゃラクになってる〜〜!」みたいなことがあるのかなと思う。

そんなことを踏まえつつ、冒頭に挙げた本のタイトルを見て思うのは、「コンピューターに退屈な作業をさせよう」なんて考えている自分自身はけっこう楽しい(退屈ではない)のだよな、ということ。

より具体的に言うなら、コンピューターにその作業をさせるためにちまちまコードを書いていく作業はけっこう楽しい。

さらに言うなら、そのコードを書く過程で素人なら必ずハマる。何度書き直してもうまく動かない。ちょっと、というかかなりイライラする。そして自分は自分で思っていた以上に馬鹿なんじゃないか? という底知れぬ不安に襲われる。もう何度も棚から取り出しては戻した参考書の、ついさっき見たはずの正規表現のルールをもう忘れてしまって、誰にも向けられない憤りにうんざりしながらまた棚から引き出す。頭はぐちゃぐちゃに溶けて煮詰まっているけれど、なんか踏ん切りがつかなくて深夜まで同じ箇所を修正し続けてしまう。そしてあるとき、突然視界がひらけるようにプログラムが動く。…おお〜〜〜〜!!ってなる。……でも周りには誰もいないから、実際にはその叫びは頭の中だけに響いてる。深夜の静かなリビングで、自分とターミナルの黒い画面だけがある。

その無音なんだか轟音なんだかわからない一瞬の魅力に囚われてしまって、その景色をまた見たくて続けてるのかなっていう。

上では何度か「趣味」と書いたけど、だからこのような日々を送るぼくにとってはプログラミングを「趣味」と呼ぶのはちょっと違和感がある。
もちろん仕事なわけではないし、ましてや天職とか使命とかいう大げさなものでもない。

そうではなく、なんか本職でも娯楽でもない、「2つめの仕事」みたいな感じというか。
日本語ネイティブの人が英語も日常生活を送れる程度には喋れる、というときの「第二言語」としての英語みたいな。

そういえば最近、ラムダノートの鹿野さんが書いた以下の記事で、
employment.en-japan.com

IT系エンジニアとして仕事で使っているプログラミング言語ではない言語、つまり第二言語を学ぼうというシリーズ記事

というのが始まっていたけど、ぼくがここで言うのはそれの人生全般バージョンというか、能力としての第二言語、本職を支える副次的な素養みたいな。
そういうものとして、育てていけるんではないかなあ、という感覚が少しある。

その意味では、こういう自分のような人たちを「ノンプログラマー」であれ「非エンジニア」であれ、語の頭に否定形(ノンとか非とか)を付けた呼称で表すことには以前から違和感があったけど、たとえば「セカンドプログラマー」とか「オルタナプログラマー」とか、ぼくだったら編集をやっているので「エディター・プログラマー」とか、「*er Programmer」とか(読めない)、なんかそういう本職の方にフィーチャーした言い方がないかなあ、という気もしてくる。

「趣味」とか「アマチュア(愛好)」ではないのだよね。べつに愛でてるわけではなくて、自分だって早く風呂に入ってさっさと明日に備えて寝てしまいたいんだけど、どうしてもさっきwhileで無限ループした理由が腑に落ちなくて、何度もいろんな場所の変数の中身をプリントデバッグしていたらもう外が明るくなってきたとか、そういうのは愛好ではなくてもう狂気に近い。というか非常識。

上で挙げた鹿野さんの、以下のインタビューも面白かったんだけど

itpro.nikkeibp.co.jp

その最終回にあった以下のくだりが印象的で。

私がオーム社でやっていた本の作り方は、会社から見れば特殊な作り方です。私しか作れない本が増えてきてしまった。会社からは「ほかの社員もできるようにしてほしい」と言われて広めようとしたのですが、うまくいきませんでした。私たちと一緒に仕事するときはバージョン管理などの仕組みを使ってくれるのですが、そうした社員も自分から使おうとはしなかった。

この最後のところ。「自分から使おうと」するかどうか、というのは、ようは上に書いたような「気づいたら時間を忘れて夢中になってた」みたいな感覚があるかどうか、ということだと思う。

そういうのがなきゃ駄目、という話ではなくて、というか誰にだってそういうのはあるはずで、単に対象がプログラミングかそうでないか、という違いがあるだけだろう。

とくにまとめもオチもないのだけど、つまりはプログラミング、結構大変。ぼくの場合は大学も美大で、授業でプログラミングとかなかったし、初めてパソコン&ネットに触ったのは28才で、初めてハローワールド!出したのは38才。だから世代や環境が違えばまったく上とは別のことが言えるかもしれないけど、いずれにしてもこういう人がこういうことを続けるには、「目的」とか「目標」とか「覚悟」とかではなくて、自分でもコントロールできないそういう非常識さというか、過剰さみたいなものが必要なのかなという気がしている。

基本情報技術者試験の結果

元々はとくに報告する予定はなかったのだけど、ネタになりそうな結果だったので一応書いておく。

先月半ばに受けたこちらの試験。
note103.hateblo.jp

ちょうどひと月後の先週、5/17に正式な合格発表があり、成績(得点)も公開されたようだったのだけど*1、「どうせ駄目だから」と見るのは後回しにしていた。

それでもつい先ほど、ちょっと気になりはじめて見にいったら、やはり合格者一覧には自分の番号がない。

いやまあ、期待したわけではないけど・・でもちょっとは「もしかして」というのもあったかな。

いずれにしても残念。であり、まあ次に期待。

・・で、話は終わると思ったのだけど、ちょっと説明を読んでいたら試験の得点もすぐに参照できるという。

以前に受けた日商簿記の試験では、得点は後から封書で送られてきたので(といっても紙切れ一枚に最終的な合計得点があるだけで、どの問題で何点とれた、とかは一切わからない不充分なものだが)、これもそんな感じかと思っていたのだけど、さすがそこはITな試験で多少は効率化されている。

ということで、そのまま案内に沿って確認・・したら、午前試験は順当にパス。
で、午後試験が59.5点だった。

これはどういうことかと言うと、100点満点中60%以上とれば合格なので、あと0.5点とれば合格だったということだろう。

・・って、ギリギリすぎ!

いやあ・・自己採点では「多めに見積もって57点」とかだったので、その時点でも多少は「惜しかったなあ〜」なんて思っていたものの、さすがに「あと0.5点」まで迫ってるとは思わなかった。

まあ、もちろんというか、結局のところそんなギリギリのラインをうろうろしているようではそもそも駄目なんですよ、ということだとは思ってるし、基本情報なんて本当に入り口に過ぎないようなものなんだから、そんなところで一喜一憂してもしょうがないじゃん、というのもわかってる。

わかってますが、でもまあ、業務の合間にぜんぜん関係ないそれを勉強して、それなりの努力でそこまで行ったわけだから、やっぱりこれは「後悔」みたいな気分も生じざるを得ないですね・・見返りにつながらなかったことの残念さというか。
さすがに0.5点ぐらいなら、どっかもう一周見直したら拾えたかもしれないし。

次は秋かあ・・前にも書いたかもしれないけど、本当に業務ピークになる頃なので、それはそれで不安・・毎日少しずつでも精進しないと。

f:id:note103:20170522125757p:plain

(不合格の下線が胸にしみる・・そんな強調しなくても・・と思うけどこの方が誤解の余地がなくていいのかな)

*1:得点はパスワードを持った受験生のみ照会可能。

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 �_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

以上です。

(大阪の画像……)