るりまサーチという最近の検索技術を使ってRubyのリファレンスマニュアルを検索するWebアプリケーションがあります。表向きの存在理由は「手早く簡単にドキュメントを検索できるシステムを提供することで、Rubyユーザが楽しくプログラミングすることを妨げないようにする」ですが、実はもう一つ理由があります。それは、「Rubyのリファレンスマニュアルをよいものにしている人たちがいることに気づいてもらう」というものです。
ここを読んでいる人の中に、他の人が実装したプログラミング言語やライブラリのドキュメントを書いて、メンテナンスしている(アップデートに追従するなど)人がどれだけいるのかわかりませんが、この作業はとても大変で根気のいる作業です。しかも、その成果をなかなか実感してもらえません。Ruby本体に新しい機能(例えば、「バイト長」ではなく「文字長」を数える機能)が入ったら、プログラムが簡潔になるなど、すぐに便利さを感じることができます。しかし、ドキュメントの方は「いやぁ、今読んだドキュメントはいいドキュメントだったなぁ!」と感じることはあまりないのではないでしょうか。
るりまサーチでRubyのリファレンスマニュアルを利用する人が増え、何度もその恩恵を受ければ徐々に便利さを感じてくるのではないでしょうか。そして、そう感じた人たちがリファレンスマニュアルをよくするプロジェクトに参加して、さらによくしていくことができたらよい循環になるのではないでしょうか。「Rubyを開発している人たちだけではなく、リファレンスマニュアルをよくしている人たちがいる」、るりまサーチがそのことを知ってもらうきっかけになれば作った甲斐があるというものです。
以下は第3回フクオカRuby大賞の本審査用に作った資料です。
今日は年に一度の肉の日だからか、いろいろなソフトウェアがリリースされていますね。
このうち、Cutter 1.1.6について紹介します。
CutterはC/C++用の単体テストフレームワークです。スクリプト言語の単体テストフレームワークのように簡単にテストを書けること、テストが失敗した時にデバッグしやすいことを重視しています。どちらも「テストが苦痛」にならないために大事なことです。
Cutter 1.1.6ではテストをより頻繁に実行しやすくするための機能を強化しました。それがTDDきのたんのサポートです。
TDDきのたんとはMayu & Co.さんが描いた色違いのかさのきのこたちです。Cutter 1.1.6を使うとテストを実行するたびに愛らしいTDDきのたんに会うことができます。頻繁にテストを実行したくなりますね。
TDD(テスト駆動開発)ではテストの結果を色で表すことが多いです。グリーンならパスで、レッドなら失敗です。グリーンになったら気持ちいいよね、レッドは落ち着かないね、早くグリーンにしたいね、そんな風に色を使っています。色を使うことで、テストをすべてパスしている状態をキープしたくなる力を推進しています。
RSpecやCutterなど最近も開発が続いている単体テストフレームワークは結果を表示する時にグリーンやレッドなどの色を使っています。でも、それだけで十分でしょうか。
テストを頻繁に実行するようになると、開発の中で自然にテストを実行するようになります。それはもうlsコマンドを実行するように*1テストを実行します。そうなると、テストの実行中に違う作業をして、合間にテスト結果を見るようになります。そのため、テストの失敗に気付くのが遅れる場合があります。
それを解決するためには、これまで通り端末に色付きの結果を表示させているだけでは不十分ではないでしょうか。
デスクトップ環境では多くのアプリケーションが動いていて、常にすべてのアプリケーションを見ていることはできません。そこで必要なときにアプリケーションから「通知」してきます*2。Linuxや*BSDではDesktop Notification、Mac OS XではGrowlなどを利用します。
テスト結果も「通知」した方がよいのではないでしょうか。
そういうわけで、Cutter 1.1.6では「notify-send」コマンドを使ってTDDきのたん付きの通知を行うようになりました。test-unit 2では2ヶ月くらい前からTDDきのたん付きの通知をサポートしています。test-unit 2で利用する場合はtest-unit-notifyを使います。こちらもTDDきのたんを使っています。
まわりくどかったですね。
まわりくどくCutter 1.1.6の新機能である「テスト結果通知」を紹介しました。単体テストフレームワークに限らず、うるさくない程度に「通知」に対応するとより使いやすくなるかもしれません。
「通知」を使う場合は画像も入れることをおすすめします。画像を入れるとよりピンときます。例えば、Mayu & Co.さんのTDDきのたんは愛らしくて何度も会いたくなりますよね。それでは、テストを実行して会いにいきましょう。
注: これの更新版である2012年版があります。
他の人がEmacsを使っているのを見ていると、「もっと便利に使えるのに」と、もやっとしたり、「え、その便利な機能ってなに?」と、発見があったりします。だれかに「この設定をすると便利ですよ」と話しやすくするために、今のEmacsのおすすめ設定をここに記しておきます。
長年漬け込んできたEmacsの設定がそこそこの量になっているので、以下のようなディレクトリ構成にして分類しています。
.emacs.d |-- init.el ;; 基本的な設定を記述 |-- config ;; 特定のモードや非標準のElispの設定をこの下に置く | |-- builtins.el ;; 標準Elispの設定 | |-- packages.el ;; 非標準Elispの設定 | `-- packages ;; 非標準Elispのうち、設定が多くなるものはこの下に置く | `-- sdic.el ;; (例)非標準Elispであるsdicの設定 `-- packages ;; 非標準Elispをこの下に置く
.emacs.d/packages/の下には非標準(Emacsに付属していない)Elispをダウンロードします。これは後述するように自動化しているので、新しい環境でも、.emacs.d/init.elと.emacs.d/config/以下だけあれば同じ環境を構築できます。アップデートしたい場合は単純に.emacs.d/packages/を削除してEmacsを再起動するだけなので手間がかかりません。
それでは、まず、基本的な設定を説明します。
;;; ロードパスの追加 (setq load-path (append '("~/.emacs.d" "~/.emacs.d/packages") load-path))
;;; Localeに合わせた環境の設定 (set-locale-environment nil)
;;; キーバインド (define-key global-map (kbd "C-h") 'delete-backward-char) ; 削除 (define-key global-map (kbd "M-?") 'help-for-help) ; ヘルプ (define-key global-map (kbd "C-z") 'undo) ; undo (define-key global-map (kbd "C-c i") 'indent-region) ; インデント (define-key global-map (kbd "C-c C-i") 'hippie-expand) ; 補完 (define-key global-map (kbd "C-c ;") 'comment-dwim) ; コメントアウト (define-key global-map (kbd "C-o") 'toggle-input-method) ; 日本語入力切替 (define-key global-map (kbd "M-C-g") 'grep) ; grep (define-key global-map (kbd "C-[ M-C-g") 'goto-line) ; 指定行へ移動
便利なのがM-C-gのgrepです。grepにはまだ設定があります。
;;; 再帰的にgrep (require 'grep) (setq grep-command-before-query "grep -nH -r -e ") (defun grep-default-command () (if current-prefix-arg (let ((grep-command-before-target (concat grep-command-before-query (shell-quote-argument (grep-tag-default))))) (cons (if buffer-file-name (concat grep-command-before-target " *." (file-name-extension buffer-file-name)) (concat grep-command-before-target " .")) (+ (length grep-command-before-target) 1))) (car grep-command))) (setq grep-command (cons (concat grep-command-before-query " .") (+ (length grep-command-before-query) 1)))
-rオプションを追加して常に再帰的にgrepするようにします。grep-findなどを使い分けなくてもすみます。
;;; 画像ファイルを表示 (auto-image-file-mode t)
バッファ内で画像ファイルを表示します。
;;; メニューバーを消す (menu-bar-mode -1) ;;; ツールバーを消す (tool-bar-mode -1)
;;; カーソルの点滅を止める (blink-cursor-mode 0)
;;; evalした結果を全部表示 (setq eval-expression-print-length nil)
;;; 対応する括弧を光らせる。 (show-paren-mode 1) ;;; ウィンドウ内に収まらないときだけ括弧内も光らせる。 (setq show-paren-style 'mixed)
昔はmic-paren.elも使っていましたが、標準の機能で十分なので、もう使っていません。
;;; 行末の空白を表示 (setq-default show-trailing-whitespace t)
;;; 現在行を目立たせる (global-hl-line-mode) ;;; カーソルの位置が何文字目かを表示する (column-number-mode t) ;;; カーソルの位置が何行目かを表示する (line-number-mode t) ;;; カーソルの場所を保存する (require 'saveplace) (setq-default save-place t)
;;; 行の先頭でC-kを一回押すだけで行全体を消去する (setq kill-whole-line t) ;;; 最終行に必ず一行挿入する (setq require-final-newline t) ;;; バッファの最後でnewlineで新規行を追加するのを禁止する (setq next-line-add-newlines nil)
;;; バックアップファイルを作らない (setq backup-inhibited t) ;;; 終了時にオートセーブファイルを消す (setq delete-auto-save-files t)
;;; 補完時に大文字小文字を区別しない (setq completion-ignore-case t) (setq read-file-name-completion-ignore-case t) ;;; 部分一致の補完機能を使う ;;; p-bでprint-bufferとか (partial-completion-mode t) ;;; 補完可能なものを随時表示 ;;; 少しうるさい (icomplete-mode 1)
;;; 履歴数を大きくする (setq history-length 10000) ;;; ミニバッファの履歴を保存する (savehist-mode 1) ;;; 最近開いたファイルを保存する数を増やす (setq recentf-max-saved-items 10000)
;;; gzファイルも編集できるようにする (auto-compression-mode t)
;;; ediffを1ウィンドウで実行 (setq ediff-window-setup-function 'ediff-setup-windows-plain) ;;; diffのオプション (setq diff-switches '("-u" "-p" "-N"))
;;; diredを便利にする (require 'dired-x) ;;; diredから"r"でファイル名をインライン編集する (require 'wdired) (define-key dired-mode-map "r" 'wdired-change-to-wdired-mode)
ファイル名をそのまま変更できるのは便利です。
;;; ファイル名が重複していたらディレクトリ名を追加する。 (require 'uniquify) (setq uniquify-buffer-name-style 'post-forward-angle-brackets)
最後にconfig/以下に置いてある設定ファイルを読み込みます。
;;; 標準Elispの設定 (load "config/builtins") ;;; 非標準Elispの設定 (load "config/packages")
config/builtins.elには標準Elisp(Emacsに付属しているElisp)の設定を記述します。まだ整理しきれていないので、1つだけ紹介します。
diredで"V"を入力するとそのディレクトリで使っているバージョン 管理システム用のモードを起動します。
(defun dired-vc-status (&rest args) (interactive) (cond ((file-exists-p (concat (dired-current-directory) ".svn")) (svn-status (dired-current-directory))) ((file-exists-p (concat (dired-current-directory) ".git")) (magit-status (dired-current-directory))) (t (message "version controlled?")))) (define-key dired-mode-map "V" 'dired-vc-status)
config/packages.elには非標準Elispの設定を記述します。しかし、非標準なので、設定をする前にダウンロードしてくる必要があります。Elispを管理するElispはいくつかあるのですが、以下のような動作をするものがないため、簡単なものを自作しています。
config/packages.elの先頭に以下のような簡単なパッケージ管理システムを定義しています。以下の場所にあるElispをインストールできます。
インストールしたパッケージは~/.emacs.d/packages/以下にインストールされます。
(require 'cl) (defvar package-base-dir "~/.emacs.d/packages") (defun package-path-basename (path) (file-name-sans-extension (file-name-nondirectory path))) (defun package-directory (files) (concat package-base-dir "/" (package-path-basename (car files)))) (defun package-run-shell-command (command) (message (format "running...: %s" command)) (shell-command command)) (defun package-install-from-emacswiki (files) (shell-command (format "mkdir -p %s" (package-directory files))) (package-run-shell-command (format "wget --directory-prefix %s %s" (package-directory files) (mapconcat (lambda (name) (concat "http://www.emacswiki.org/emacs/download/" name)) files " ")))) (defun package-install-from-github (files) (package-run-shell-command (format (concat "git clone https://github.com/%s.git %s") (car files) (package-directory files)))) (defun package-install-from-repo.or.cz (files) (package-run-shell-command (format (concat "git clone git://repo.or.cz/%s.git %s") (car files) (package-directory files)))) (defun package-alist-value (alist key default-value) (if (listp alist) (let ((alist-item (assoc key alist))) (if alist-item (cdr alist-item) default-value)) default-value)) (defun package-install (type package-spec require-name &optional force) (let ((files (package-alist-value package-spec 'files (if (listp package-spec) package-spec (list package-spec)))) (base-path (package-alist-value package-spec 'base-path ".")) (additional-paths (package-alist-value package-spec 'additional-paths nil)) (install-proc (case type (emacswiki 'package-install-from-emacswiki) (github 'package-install-from-github) (repo.or.cz 'package-install-from-repo.or.cz) (t (error "unknown package type: <%s>(%s)" type package))))) (add-to-list 'load-path (format "%s/%s" (package-directory files) base-path)) (dolist (additional-path additional-paths) (add-to-list 'load-path (format "%s/%s" (package-directory files) additional-path))) (condition-case err (require require-name) (error (message (format "installing %s..." files)) (funcall install-proc files))) (require require-name)))
grepの結果を直接編集できるようになります。wdiredと合わせてC-c C-cでも編集結果を反映できるようにしています。
;;; *grep*で編集できるようにする (package-install 'emacswiki "grep-edit.el" 'grep-edit) (add-hook 'grep-setup-hook (lambda () (define-key grep-mode-map (kbd "C-c C-c") 'grep-edit-finish-edit)))
自動で補完候補をだしてくて便利です。補完候補をC-n/C-pでも選択できるようにしています。
;;; 自動補完 (package-install 'github "m2ym/auto-complete" 'auto-complete-config) (add-to-list 'ac-dictionary-directories (format "%s/auto-complete/dict" package-base-dir)) (ac-config-default) (add-hook 'auto-complete-mode-hook (lambda () (define-key ac-completing-map (kbd "C-n") 'ac-next) (define-key ac-completing-map (kbd "C-p") 'ac-previous)))
いろいろ便利に使えるらしいAnythingですが、iswitchb-modeの代わりにだけ使っています。isiwtchb-modeの代わりなのでキーバインドはC-x bだけです。
;;; Anything (let ((original-browse-url-browser-function browse-url-browser-function)) (setq anything-command-map-prefix-key "C-c C-<SPC>") (package-install 'repo.or.cz '((files . ("anything-config")) (additional-paths . ("extensions"))) 'anything-startup) (define-key global-map (kbd "C-x b") 'anything-for-files) (define-key anything-map (kbd "C-z") nil) (define-key anything-map (kbd "C-l") 'anything-execute-persistent-action) (define-key anything-map (kbd "C-o") nil) (define-key anything-map (kbd "C-M-n") 'anything-next-source) (define-key anything-map (kbd "C-M-p") 'anything-previous-source) (setq browse-url-browser-function original-browse-url-browser-function))
C-x C-tで近くにあるrun-test.shやrun-test.rbという名前のファイルを実行するツールです。
;;; テスト実行 (package-install 'github '((files . ("kou/run-test")) (base-path . "lib")) 'run-test-setting)
長年漬け込んできたEmacsの設定を紹介しました。
ここで紹介した内容はGitHubに置いておいたので、興味がある人は試してみてください。
デバッグしやすいassert_equalの書き方とデバッグしにくいassert_equalの書き方があるのは知っていますか?*1
デバッグしやすいassert_equalの書き方を2パターン紹介します。
まず、1つ目のよくみるデバッグしにくいassert_equalの書き方です。
1 2 3 4 5 |
def test_parse assert_equal(29, parse_integer("29")) # (1) assert_equal(29, parse_integer("+29")) # (2) assert_equal(-29, parse_integer("-29")) # (3) end |
これがデバッグしにくいのは、(1)が失敗したら(2)、(3)が実行されないからです。すべてのassert_equalが実行されて、どのassert_equalが失敗したかを確認することでバグの傾向を見つけやすくなります。
例えば…(1)だけが失敗するなら符号が無い場合の処理が怪しいでしょう。(3)だけが失敗するなら、マイナス記号に対応していないのかもしれません。全部失敗するなら根本的におかしいのでしょう。
このように、どのassert_equalが失敗したかがデバッグのヒントになります。よって、↑のような書き方はデバッグがしにくいassert_equalの書き方といえます。
デバッグしやすく書くと、例えば、こうなります。
1 2 3 4 5 6 7 8 9 10 11 |
def test_parse_no_sign assert_equal(29, parse_integer("29")) end def test_parse_plus_sign assert_equal(29, parse_integer("+29")) end def test_parse_mius_sign assert_equal(-29, parse_integer("-29")) end |
続いて、1つ目のよくみるデバッグしにくいassert_equalの書き方です。
1 2 3 4 5 |
def test_parse bob = MyJSONParser.parse('{"name": "bob", "age": 29}') assert_equal("bob", bob["name"]) assert_equal(29, bob["age"]) end |
1つ目の書き方のように複数のassert_equalが書いてあります。1つ目の書き方と同じように直すならこうなります。
1 2 3 4 5 6 7 8 9 |
def test_parse_string_value bob = MyJSONParser.parse('{"name": "bob", "age": 29}') assert_equal("bob", bob["name"]) end def test_parse_numeric_value bob = MyJSONParser.parse('{"name": "bob", "age": 29}') assert_equal(29, bob["age"]) end |
でも、本当にこれでよいでしょうか。この書き方では値の型だけを注目しています。値の型に注目するならば以下のように書いた方がよいでしょう。こうした方が余計なものがなくなり、より注目できています。
1 2 3 4 5 6 7 8 9 |
def test_parse_string_value bob = MyJSONParser.parse('{"name": "bob"}') assert_equal("bob", bob["name"]) end def test_parse_numeric_value anonymous = MyJSONParser.parse('{"age": 29}') assert_equal(29, anonymous["age"]) end |
もし、最初のコードが「複数のキーと値をパースできること」を確認したい場合はassert_equalを複数のテストに分割するのではなく、複数のassert_equalを1つのassert_equalにまとめるべきです。
1 2 3 4 5 |
def test_parse bob = MyJSONParser.parse('{"name": "bob", "age": 29}') assert_equal({"name" => "bob", "age" => 29}, bob) end |
1つのassert_equalにまとめると、失敗する場合でも、一部の比較だけが実行されるのではなく、すべての比較が実行されます。そのため、"name"はパースに成功しているけど"age"は失敗している、というように、失敗の傾向がわかります。失敗の傾向がわかるとデバッグしやすくなるというのは前述の通りです。
ところで、Hashのassert_equalの結果は見づらいですよね。これは、RSpecでも一緒です。
1 |
{"age" => 30, "name" => "bob"}.should == {"name" => "bob", "age" => 29} |
この結果は以下のようになります。
expected: {"name"=>"bob", "age"=>29}
got: {"age"=>30, "name"=>"bob"} (using ==)
Diff:
@@ -1,2 +1,2 @@
-{"name"=>"bob", "age"=>29}
+{"age"=>30, "name"=>"bob"}
一見しただけではどこが違うのかわかりません。
しかし、最新のtest-unit 2は一味違います。
1 2 |
assert_equal({"name" => "bob", "age" => 29}, {"age" => 30, "name" => "bob"}) |
この結果は以下のようになります。
<{"age"=>29, "name"=>"bob"}> expected but was
<{"age"=>30, "name"=>"bob"}>
diff:
? {"age"=>29, "name"=>"bob"}
? 30
違いがわかりやすいですね。
デバッグしやすいassert_equalの書き方を2パターン紹介しました。今度から、assert_equalを書くときは、単に数を増やしたりするのではなく、どうすれば有用なassert_equalになるかを考えてみてください。assert_equalが失敗したときにいかにデバッグしやすいかがヒントになるはずです。
*1 assert_equalではなくて、should ==と読み替えてもよい。