プログラムを「書き始める」「試しに実行する」コストを下げる工夫
はじめに
物事を上達するためには反復を、というのはよく聞きますが、もちろんプログラミングでも大事なのかと思います。とくに自分は「一を聞いて十を知る」ような器用なことはできないので、何度も何度もプログラムを書いて、試していました。
このような反復を支援するためには、できるかぎり「書き始めるコスト」と「実行して確認するコスト」は低い方がいいと思っています。書き始めるのがだるいと、そもそも「ちょっと書いてみようかな」となかなか思わないですし、実行するための手数が多いと、「書いて→結果を確認」の回数が減ります。
本稿では、この「書き始めるコスト」と「実行して確認するコスト」を下げる私が20年くらい行っている工夫についてご紹介します。
筆者が Ruby が好きなので、Ruby の例が多いですが、別に Ruby に限った話ではありません。
プログラミング言語による違い
たとえば、C 言語ですと、プログラムを実行するために
#include <stdio.h>
int main(int argc, char *argv){
printf("hello");
}
のように書かないといけなくて、ちょっとだけ距離があります。もちろん、テンプレートを用意したり、エディタのスニペット入力機能を有効活用する、といった工夫があり得ます。
Ruby などの言語では、
puts "hello"
とだけ書いておけば、同じようなプログラムが書けるので、精神的に楽です。
この特性は、
- 結構多くのライブラリは起動時にたいてい読み込まれている
- おまじないが不要でトップレベルにいろいろ書ける
という点から来ています。この点は利点になることもあれば、ある種の制約になるなど、欠点になることもあります。ただ、「プログラムを書き始める」というコストを下げる効果はあると思います。
一般的な実行方法
書いたプログラムを実行するには、たとえば Ruby ですと、ruby t.rb
のようにコマンドを端末で入力し実行するのが一般的です。
IDE、例えば VSCode なら F5 キーを押せば IDE 中にある端末で(デバッガつきで)実行する、というのができます。結果もその場で見ることができます。ただ、デバッガ付きなのでちょっともたつきます。
また、REPL ツール(Ruby では irb や pry)などを使って、一行一行プログラムを書いて結果を確認していくこともできると思います。
$ irb
irb(main):001:0> puts "hello"
hello
=> nil
irb(main):002:0>
1行程度のプログラムを修正しながら構成していくには適していると思いますが、例えばちょっとしたクラスの定義も書くのが面倒なので(個人の意見です)、適しているときと、そうでないときがあります。
最近だと、自分には不案内なネットワークプログラムのクライアントをどうやって書くのかな、という試行錯誤で irb は便利でした。
また、Ruby だと ruby -e "puts 'hello'"
のようにワンライナーで書いてしまうこともあります。
irb もワンライナーも、どちらも気楽なんですが、数行を超えるプログラムを書き始めると、ちょっと面倒だなぁ、という気分になります。あと、端末が開いていないといけないのはちょっと面倒です(いつも開いてるからそうでもない?)。
私のn個の工夫
私が行っている工夫についてご紹介します。私は xyzzy というエディタを好んで使っているのですが、このエディタにちょっと加えた設定で行っている工夫についてご紹介します。ただ、別にどのエディタなどでも使えるテクニックだと思います。
t.rb
というファイルでプログラムを書き始められるようにする
プログラムを書き始めるには、(irb などの REPL を使わない限り)、ファイルにプログラムを保存する必要があります。ただ、そもそもファイル名を決めるのが面倒です。そこで、私はエディタがどういう状態でも、~/src/rb/t.rb
というファイルにプログラムが書き始められるようにセットアップされるようにキーバインドを設定しました(C-c C-r)。
何を言っているか、よくわからないと思うんですが、次のような動作をします。
- エディタ上に t.rb が開いていなければエディタで開く、開いていれば、そのウィンドウ(バッファ)にフォーカスをあてる
- t.rb の先頭に
\n\n__END__
を入力する
2 が Ruby 特有の機能で(Perl 由来ですかね)、__END__
以下の記述は無視する(Ruby では DATA
というのでアクセスできます。Object::DATA (Ruby 3.1 リファレンスマニュアル) )ので、これまで書いていたプログラムを無視するようにセットアップできます。
これには、t.rb
に書き散らしたプログラムが残る、という利点があり、「このライブラリどうやって使うんだっけ」というと、過去の自分が試した結果が見つかることがあります(ないことも多いです)。
私はRubyのプログラムを書き始める場合、たいてい t.rb からはじめ、ある程度大きくなったら別ファイルに移す、というように書いてきます。
ほぼすべてのRubyプログラムはここに書き始めるのですが、例えば次のように使います。
- プログラムを書いていて、「あれ、この処理どうやって使うんだっけ」というときにちょっと実験コードを書く
- ちょっとしたバッチ処理を書く
- ファイルの移動とか
- 実験結果やログの整理とか
- 事務処理とか
- パズルなんかを解く
最初の例は、別ファイルで Ruby プログラムを書いているときでも、C-c C-r で開くことができるので、ちょこちょこ書くことができて便利です。
C-c C-r を押した直後の様子です。これ、直前には、あるプロセス ID に 1000 回 kill する、って例を書いていますね。
この t.rb、書き散らして今は12万行くらいでした。1度か2度、間違えて消してしまったことがあるので、長い年月でこの数倍は書いているんじゃないかと思います。
キー一発で実行し結果を確認する
VSCode などでの F5 と同じですが、端末で ruby t.rb
などと実行するのはだるいので、xyzzy 上でもキー一発で、現在開いているファイルの実行結果が出るようにしています(私は C-2 で起動するようにしています)。
より詳細には次のような挙動になります。
- もし変更があれば保存
- 拡張子を見て、適切なコマンドを実行(.rb なら ruby)
この場合、画面を2分割して、ruby t.rb
の結果を上側に出しています。どのバージョンの Ruby で実行したか、よくわからなくなるので、バージョンもついでに表示しています。
この辺は結構作りこみがやりやすいところでして、例えば私は複数の Ruby のバージョンで結果を見比べたいことがしばしばあるのですが、その時のためにいくつかのバージョンの実行結果をまとめて表示する機能を用意しています(私は C-3 で実行されるようにしています)。
この場合、foo => bar
という文法は Ruby 3.0 から導入されたんだな、というのがわかります。
最近、ko1/rstfilter: Show Ruby script with execution results. というライブラリを書いてみたので、C-4 でこれを使った結果を表示するようもしてみました。
(なんか警告出ていますね)
で、これと同じような体験を VSCode でもできないか、と思って
rstfilter VSCode extension による新しい Ruby の開発体験のご紹介
を書いて作ってみた次第です。VSCode だと、なんとマウスオーバーで式の結果まで見れちゃう!(記事を参照してみてください)
保存(Windows だと C-s)で動くのは、結構便利だな、と思っています。ただ、C-s でなんでも実行するのは正直やりすぎだったなと思っていて、でもほかのキーバインド何がいいかなぁ、と今考えているところです(VSCode キーバインド埋まりすぎ問題)。
終わりに
本当になんてことはない工夫なのですが(きっと同じようなことをやってる人も多いかと思います)、これらの工夫があると、「ちょっとプログラム書いてみよ」、って思いやすくなって、そうするとちょっとしたことでもプログラム書いちゃろ、ってなり、その積み重ねがトレーニングにつながったりするのかな、などとも考えます。
私の場合だと
- あ、これどう書くんだろ、とか、ちょっとこの処理かいちゃろ、と思う
- (*1) xyzzy を起動 / 起動していたらフォアグラウンドに(ランチャでここも速くしたい)
- (*2) C-c C-r で t.rb を編集可能に
- プログラムをちょろちょろ書く
- (*3) C-2 で実行して結果を確認
(*) の3手間でプログラムを書き始めて、確認までいけます。それぞれの手順は一瞬なので、だいたい「手間」の部分は一瞬です。
私は試してみないとわからない人間なので、これらの工夫が大変役にたちました(し、今でも役にたっています)。いま、プログラムを書き始めるにちょっと面倒だなあ、などと抵抗がある方は、ご自分の環境で工夫ができないか試してみてください。
Appendix
.xyzzy に書いた設定です。いや、適当すぎるんですが...。
;; C-c C-r で t.rb を開いて \n\n__END__ を入力
(global-set-key
'(#\C-c #\C-r)
#'(lambda () (interactive)
(let*
((file "~/src/rb/t.rb"))
(if (or
(and (get-file-buffer file)
(set-buffer (get-file-buffer file)))
(find-file file))
;; insert
(progn
(goto-char 0)
(insert "\n\n__END__\n")
(goto-char 0))
))))
;; 拡張子を見て何かする
(defun my-execute-nanika2 (list)
"拡張子やモードを見て、対応したプログラムを走らせて見たり 2"
(let* ((buffer (selected-buffer))
(rfile (or (get-buffer-file-name) ""))
(file (concat (pathname-name rfile) "." (pathname-type rfile)))
(type (and file (pathname-type file)))
(f #'(lambda (a b) (eq 0 (string-matchp b a))))
(exec (and type (assoc type list :test f))))
(if (not exec)
(progn
(setq exec (assoc mode-name list :test f))
(and exec (setq type mode-name))))
(and exec (setq exec (cdr exec)))
(cond
((stringp exec)
(progn
(if (buffer-modified-p) (save-buffer)) ; セーブしてから
; (execute-subprocess (format nil exec file))
(execute-subprocess-wsl (format nil exec file))
(set-window (get-buffer-window buffer))))
((consp exec)
(if (buffer-modified-p) (save-buffer)) ; セーブしてから
(funcall (eval exec))
(set-window (get-buffer-window buffer)))
((file-readable-p ".nanika.cmd")
(if (buffer-modified-p) (save-buffer)) ; セーブしてから
(execute-subprocess ".nanika.cmd")
(set-window (get-buffer-window buffer)))
(t (message (concat "やりたいことがねーっす : " (string type))))
)))
(global-set-key '#\C-2
#'(lambda () (interactive)
(my-execute-nanika2
'(("tex" . "platex ~A")
("rb" . "c:/ko1/ruby/v3/install/master/bin/ruby -v ~A")
; ("rb" . "c:/ko1/ruby/v3/install/master/bin/ruby -wv ~A")
;("ruby". "ruby ~A")
;("ruby". "ruby -wd ~A")
("html". "start ~A")
("pl" . "perl ~A")
("py" . "sh -c \"python ~A\"")
("scm" . "gosh -i < ~A")
("txt" . "t2n ~A")
("lisp". #'(lambda () (eval-buffer (selected-buffer))))
("lua" . "lua ~A")
; ("hs" . "\"C:/Program Files (x86)/Haskell Platform/2013.2.0.0/bin/runghc\" ~A")
("hs" . "start ~A")
("scala" . "c:/ko1/app/scala-2.7.7.final/bin/scala ~A")
("sml" . "\"c:/Program Files (x86)/SMLNJ/bin/sml.bat\" ~A")
("gnuplot" . "gnuplot ~A")
))))
Discussion