2011年11月に「理解しやすい、読みやすいコードはどのように書けばよいか」という内容の本がO'Reillyから出版されました。(英語です。)
英語なので読む人はいないでしょうが紹介します。読みやすい英語で書かれているので、「読みやすいコードだけではなく英語の勉強にも興味がある」という人にはちょうどよいでしょう。ページ数も200ページにいかないぐらいとコンパクトにまとまっています。
さて内容ですが、第一部が「SURFACE-LEVEL IMPROVEMENTS」です。見た目をよくしようという話です。具体的な内容は読んでもらうとして、まず、見た目のよさから入っているところが「わかっている感」をだしていますね。
読みやすいコードにするテクニックはいろいろありますが、まずは見た目が整っていないと、どんなにテクニックを駆使していても読みづらくなってしまうものです。どんなにすっきり設計できているプログラムでも、インデントが崩れていたり、ピンとこない名前や省略しすぎた名前を使っているコードは読みづらくなってしまいます。長すぎるメソッドも見た目が悪いですね。
よいコードを書くためのもっと重厚な本はいくつも出版されていて、これだけは読んでおけ!と言われている本もいくつもあります。(この本の参考文献でもいくつも挙げられています。)ふつうのステップでは、まずこの本を読んで助走をつけてから他の本を読むとよい、ということになりそうです。
しかし、逆に他の良書と呼ばれている本を読んでからこの本を読む、あるいは、これまでたくさんコードを書いてきて自分なりにこうした方がよいというのができている人が読む、というのもよいでしょう。というのは、この本を読むことで、あなたが忘れかけていたことやあなたが知らない間にやっていたことに気づくことができるはずだからです。そうすれば、あなたがやっている「よいコードを書くためのこと」をより上手に他の人に伝えることができるようになっているはずです。また、「よいコードを書くために大事なこと」だとわかっているけど面倒だったり何かしら理由をつけて実践していないことがあるのなら、それを気づかせてくれるでしょう。
とはいえ、やはり英語だとなかなか読む気にならないことでしょう。そんなあなたにうれしいお知らせがあります。7月あたりにあの角さんの訳で翻訳版が出版されるそうです。楽しみですね。
さぁ、忘れてもよいコードを書きましょう。
コミットメッセージの書き方ではコミットをわかりやすくするためには以下の2つの条件を満たす必要があると書きました。
このうち「コミットの内容が分かりやすく説明されていること」についてはすでに説明済みです。今回は「コミットの内容が小さくまとまっていること」について説明します。
単純にコミットの内容を小さくするだけではわかりやすくなりません。それでは、どのような基準で小さくすればよいのでしょうか。
よく言われることは1つのコミットには1つの小さな論理的にまとまった変更だけにする、というものです。たしかにこれは重要です。しかし、これだけを基準とすると、人によっては大きめなコミットになってしまいます。人それぞれで論理的なまとまりの大きさが異なるからです。
1つのコミットでどうすればよいかを考えるのではなく、一連のコミットでどうすればよいかを考えましょう。そうすれば、1つのコミットにどこまで含めればよいかを考えやすくなります。感覚的に言うと「コミットの流れを見ているだけでペアプログラミングしている気分になる」コミットが小さくまとまっているコミットです。ここをめざしてください。
これを支援するためにはどのような開発環境がよいのかについてはここでは省略します*1。
いくつか小さくまとまったコミットの具体例を紹介します。
名前の変更やコードの移動などのリファクタリングをした後に変更したコードの周辺だけインデントが崩れることがあります。このようなときはインデントだけを直すコミットをします。
よいコミット:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
diff --git a/lib/test/unit/pending.rb b/lib/test/unit/pending.rb index 75cc8cb..75b1914 100644 --- a/lib/test/unit/pending.rb +++ b/lib/test/unit/pending.rb @@ -112,8 +112,8 @@ module Test def handle_pended_error(exception) return false unless exception.is_a?(PendedError) pending = Pending.new(name, - filter_backtrace(exception.backtrace), - exception.message) + filter_backtrace(exception.backtrace), + exception.message) add_pending(pending) true end |
もし、まわりにtypoなどがあってもそれをこのコミットに含めてはいけません。ペアプログラミングをしているときのことを思い出してください。1度に1つの作業しかできませんよね。
また、複数のファイルや複数のクラスなど、変更が複数の塊にまたがる場合は別々のコミットにしましょう。ペアプログラミングをしているときは、インデントの修正でコードが壊れていないことを確認するために、それぞれの塊を修正するごとにテストを実行しますよね。
indent
コマンドを使うなど、一括で機械的にインデントを直す場合は1つのコミットにまとめても構いません。ただし、そのときはコミットメッセージに実行したコマンドラインを残しておくとよいでしょう。
たくさんコードを書いているとtypoはよくあることです。typoを直すときは同じtypo毎にコミットをわけましょう。またコミットメッセージにどんなtypoを直したかも書いておくとdiffを見た人に親切です*2。
よいコミット:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
diff --git a/lib/test/unit/notification.rb b/lib/test/unit/notification.rb index 48ba3f6..c9c89b6 100644 --- a/lib/test/unit/notification.rb +++ b/lib/test/unit/notification.rb @@ -79,12 +79,12 @@ module Test module NotificationHandler class << self def included(base) - base.exception_handler(:handle_Notified_error) + base.exception_handler(:handle_notified_error) end end private - def handle_Notified_error(exception) + def handle_notified_error(exception) return false unless exception.is_a?(NotifiedError) notification = Notification.new(name, filter_backtrace(exception.backtrace), |
typoの修正コミットに他の変更を混ぜるのはやめましょう。以下はtypoの修正とエラーメッセージの修正を1度にコミットしている悪い例です。
悪いコミット:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
diff --git a/Rakefile b/Rakefile index e3e73cf..bfcbe04 100644 --- a/Rakefile +++ b/Rakefile @@ -292,7 +292,7 @@ namespace :release do empty_options << "OLD_RELEASE_DATE" if old_release_date.nil? unless empty_options.empty? - raise ArgumentError, "Specify option(s) of #{empty_options.join(",")}." + raise ArgumentError, "Specify option(s) of #{empty_options.join(", ")}." end indexes = ["doc/html/index.html", "doc/html/index.html.ja"] @@ -302,7 +302,7 @@ namespace :release do [old_release_date, new_release_date]].each do |old, new| replaced_content = replaced_content.gsub(/#{Regexp.escape(old)}/, new) if /\./ =~ old - old_undnerscore = old.gsub(/\./, '-') + old_underscore = old.gsub(/\./, '-') new_underscore = new.gsub(/\./, '-') replaced_content = replaced_content.gsub(/#{Regexp.escape(old_underscore)}/, |
最初のhunkはjoin
の引数にスペースを追加しているだけでtypoの修正ではありません。もし、コミットメッセージに「Fix typos」などと書かれていれば最初のhunkにもtypoがあるのではないかと思ってしまうでしょう*3。
マジックナンバーに名前をつけるときは1つのコミットで1つのマジックナンバーだけに名前をつけましょう。
以下は、C言語のプログラムの終了コードを0
と-1
というマジックナンバーから、EXIT_SUCCESS
とEXIT_FAILURE
という名前のついた値にするためのコミットです。もし、間違って0
をEXIT_FAILURE
に置き換えていても気づかないでしょう。
悪いコミット:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
diff --git a/src/groonga.c b/src/groonga.c index fca1755..d193d15 100644 --- a/src/groonga.c +++ b/src/groonga.c @@ -1938,10 +1938,10 @@ do_daemon(char *path) break; case -1: perror("fork"); - return -1; + return EXIT_FAILURE; default: wait(NULL); - return 0; + return EXIT_SUCCESS; } if (pidfile_path) { pidfile = fopen(pidfile_path, "w"); @@ -1951,7 +1951,7 @@ do_daemon(char *path) break; case -1: perror("fork"); - return -1; + return EXIT_FAILURE; default: if (!pidfile) { fprintf(stderr, "%d\n", pid); @@ -1959,7 +1959,7 @@ do_daemon(char *path) fprintf(pidfile, "%d\n", pid); fclose(pidfile); } - _exit(0); + _exit(EXIT_SUCCESS); } { int null_fd = GRN_OPEN("/dev/null", O_RDWR, 0); @@ -2587,7 +2587,7 @@ main(int argc, char **argv) line_editor_init(argc, argv); } #endif - if (grn_init()) { return -1; } + if (grn_init()) { return EXIT_FAILURE; } grn_set_default_encoding(enc); |
しかし、以下のようにEXIT_SUCCESS
への置き換えとEXIT_FAILURE
への置き換えを別のコミットにしたらどうでしょうか。これなら間違って置き換えていても気づきやすいですね。ペアプログラミングをしているときでも、EXIT_SUCCESS
への置き換えとEXIT_FAILURE
への置き換えを同時にやっていると、ペアの人が間違いに気づきにくくなりますよね。
よいコミット1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
diff --git a/src/groonga.c b/src/groonga.c index fca1755..2731006 100644 --- a/src/groonga.c +++ b/src/groonga.c @@ -1941,7 +1941,7 @@ do_daemon(char *path) return -1; default: wait(NULL); - return 0; + return EXIT_SUCCESS; } if (pidfile_path) { pidfile = fopen(pidfile_path, "w"); @@ -1959,7 +1959,7 @@ do_daemon(char *path) fprintf(pidfile, "%d\n", pid); fclose(pidfile); } - _exit(0); + _exit(EXIT_SUCCESS); } { int null_fd = GRN_OPEN("/dev/null", O_RDWR, 0); |
よいコミット2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
diff --git a/src/groonga.c b/src/groonga.c index 2731006..d193d15 100644 --- a/src/groonga.c +++ b/src/groonga.c @@ -1938,7 +1938,7 @@ do_daemon(char *path) break; case -1: perror("fork"); - return -1; + return EXIT_FAILURE; default: wait(NULL); return EXIT_SUCCESS; @@ -1951,7 +1951,7 @@ do_daemon(char *path) break; case -1: perror("fork"); - return -1; + return EXIT_FAILURE; default: if (!pidfile) { fprintf(stderr, "%d\n", pid); @@ -2587,7 +2587,7 @@ main(int argc, char **argv) line_editor_init(argc, argv); } #endif - if (grn_init()) { return -1; } + if (grn_init()) { return EXIT_FAILURE; } grn_set_default_encoding(enc); |
最初は単なるちょっとしたコードだったものが他のコードでも使いたくなるくらい便利なコードに育っていくことはよくあります。そのようなとき、ライブラリとして使えるようにモジュールに入れたりしますね。
例えば、以下のようなちょっとしたログ出力メソッドがあったとします。
1 2 3 |
def log(tag, message) puts("[#{tag}] #{message}") end |
これをそのまま他のコードでも使おうとすると、トップレベルにlog
メソッドが定義されてしまい、行儀がよくありませんね。このようなときは以下のようにモジュールの中に入れたりします。
1 2 3 4 5 6 |
module Logger module_function def log(tag, message) puts("[#{tag}] #{message}") end end |
このときは以下のように2つのコミットにわけます。
まず、モジュールで囲みます。しかし、まだ元のメソッドはインデントしません。
よいコミット1:
1 2 3 4 5 6 7 8 9 10 11 |
diff --git a/logger.rb b/logger.rb index 1c7c4f0..7a3ed06 100644 --- a/logger.rb +++ b/logger.rb @@ -1,3 +1,6 @@ +module Logger + module_function def log(tag, message) puts("[#{tag}] #{message}") end +end |
次にモジュールの中身をインデントします。
よいコミット2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
diff --git a/logger.rb b/logger.rb index 7a3ed06..293a335 100644 --- a/logger.rb +++ b/logger.rb @@ -1,6 +1,6 @@ module Logger module_function -def log(tag, message) - puts("[#{tag}] #{message}") -end + def log(tag, message) + puts("[#{tag}] #{message}") + end end |
このように分けることで、たとえ一緒に同じ作業をしていなくても、一連のコミットを見るだけで何をしようとしているかが伝わります。
1つのコミットのことだけを考えていると同時にコミットしたくなりますが、一連のコミットを考えるとこのように表現することもできます。意図が伝わるコミットです。
コミットの内容を小さくまとめるにはどうしたらよいかの指針とその具体例をいくつか紹介しました。
1つ1つのコミットの積み重ねでクリアなコードが作られていきます。もちろん、1つのコミットは大切にしますが、一連のコミットも大切にして、意図が伝わるコミットにしましょう。コミットを見ることで、チームのみんなにどのように開発しているかが伝わるようなコミットにしていきましょう。
なお、フリーソフトウェアの開発のように、世界中の様々な場所・様々な時間に開発が行われているような場合はこのような意図が伝わるコミットのしかたがより重要になります。信頼されるようなコミットを重ねていきましょう。
*1 例えば「diff入りのコミットメールを送る」という方法があります。
*2 余談ですが、typoを直すコミットメッセージ中でtypoすることはよくある話です。
*3 「Fix a typo」なら最初のhunkにはないと思うかもしれません。しかし、「そしたら最初のhunkはなんだろう?」ということになるのでそれでもよくありません。
2012年3月にEmacsの入門書が技術評論社から出版されました。
Emacs実践入門 ~思考を直感的にコード化し、開発を加速する (WEB+DB PRESS plus)
技術評論社
¥ 2,678
インストール方法やファイルの開き方などから始まっていて初心者向けの始まり方になっています。それでは初心者向けなのかというとそうでもなく、中盤から後半はrequire
しないと使えないElispを使った拡張方法の紹介になっています。
おそらく、初心者の人は1/3か1/2くらい進んだところで一度脱落するのではないでしょうか。逆に、ある程度知っている人は中盤から後半にかけて興味のある話題が増えていくことでしょう。脱落してしまった人は、しばらく前半の機能でEmacsを使って、慣れてきてから再挑戦するとよいでしょう。
後半の拡張方法の紹介部分では多くの方法を紹介するためか、1つ1つの方法については簡単に紹介する程度にとどまっています。よりつっこんだ使い方までは踏み込んでいません。そのため、すでに最近のEmacs界隈の状況を把握している人やバリバリカスタマイズして使っている人にとっては物足りない内容かもしれません。そうでない人は、こんな方法があるのかと気づくことも多いでしょう。
ということで、今ひとつEmacsを使いこなせていない感のある人は読んでみてはいかがでしょうか。Emacsがより手になじむことになるでしょう。
さて、Emacsが手になじむようになるには自分がEmacsに慣れるだけではなく、Emacsにも自分に歩み寄ってもらうことが近道です。そのためにEmacsの設定をカスタマイズします。
約1年前におすすめEmacs設定を紹介しました。ここで紹介した設定は基本的なものだけに限定していましたが、より細かい設定やrequire
しないと使えないElispの設定も増えています。
それでは、1年経ったおすすめEmacs設定を紹介します。
まず、ディレクトリ構成が変わりました。設定ファイルのディレクトリ構成はEmacs実践入門でも提案されていますが、ここではまた違った構成にしています。Emacs実践入門でも「筆者もこの設計がベストだとは思っておらず、より良い配置を模索中です。ぜひもっと優れた設計を考えてみてください。」*1と書かれているので、自分になじむ構成を見つけてください。
.emacs.d |-- init.el ;; 基本的な設定を記述 |-- local.el ;; (カスタマイズ用) |-- config ;; 特定のモードや非標準のElispの設定をこの下に置く | |-- builtins.el ;; 標準Elispの設定 | |-- builtins ;; 標準Elispのうち、設定が多くなるものはこの下に置く | | |-- local.el ;; (カスタマイズ用) | | `-- cc-mode.el ;; (例)標準Elispであるcc-modeの設定 | |-- packages.el ;; 非標準Elispの設定 | |-- packages ;; 非標準Elispのうち、設定が多くなるものはこの下に置く | | |-- local.el ;; (カスタマイズ用) | | `-- sdic.el ;; (例)非標準Elispであるsdicの設定 | `-- el-get ;; el-getの設定はこの下に置く | |-- recipies ;; el-getのレシピはこの下に置く | `-- local-recipies ;; (カスタマイズ用) `-- el-get ;; el-get管理のパッケージをこの下に置く
1年前まではpackage.el
という名前の独自のパッケージ管理システムを使っていたのですが、同じ名前のパッケージ管理システムがEmacs 24に標準搭載されることになったため、el-getに乗り換えました。el-getにした理由は元々使っていたパッケージ管理システムと同じことができたからです。
それでは、まず、基本的な設定を説明します。
以前は~/.emacs.d/packages
もパスに入っていましたが、el-get管理になったので除きました。
;;; ロードパスの追加 (setq load-path (append '("~/.emacs.d") load-path))
;;; Localeに合わせた環境の設定 (set-locale-environment nil)
C-hの設定をdefine-key
ではなくkeyboard-translate
を使うようにしました。c-electric-backspace
を明示的にdefine-key
しなくてもよいことに気づいたからです。しかし、キー入力中にC-h
を押してもキーバインド一覧が出てこないのは不便なのでdefine-key
に戻すかもしれません。
また、ウィンドウ移動用のキーバインドも追加しました。
;; C-hでバックスペース ;; 2012-03-18 (keyboard-translate ?\C-h ?\C-?) ;; 基本 (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 "M-C-g") 'grep) ; grep (define-key global-map (kbd "C-[ M-C-g") 'goto-line) ; 指定行へ移動 ;; ウィンドウ移動 ;; 2011-02-17 ;; 次のウィンドウへ移動 (define-key global-map (kbd "C-M-n") 'next-multiframe-window) ;; 前のウィンドウへ移動 (define-key global-map (kbd "C-M-p") 'previous-multiframe-window)
便利なのがM-C-g
のgrepです。grepにはまだ設定があります。
;; 再帰的にgrep ;; 2011-02-18 (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も使っていましたが、標準の機能で十分なので、もう使っていません。
1年前はshow-trailing-whitespace
を使っていましたが、より多くの空白を視覚化できるwhitespace-mode
を使うようにしました。
;; 2011-10-27 ;; 空白や長すぎる行を視覚化する。 (require 'whitespace) ;; 1行が80桁を超えたら長すぎると判断する。 (setq whitespace-line-column 80) (setq whitespace-style '(face ; faceを使って視覚化する。 trailing ; 行末の空白を対象とする。 lines-tail ; 長すぎる行のうち ; whitespace-line-column以降のみを ; 対象とする。 space-before-tab ; タブの前にあるスペースを対象とする。 space-after-tab)) ; タブの後にあるスペースを対象とする。 ;; デフォルトで視覚化を有効にする。 (global-whitespace-mode 1)
;;; 現在行を目立たせる (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)
見直したら1年前の設定には抜けていたので追記しました。
ファイルの先頭に#!...
があるファイルを保存すると実行権をつけます。
;; 2012-03-15 (add-hook 'after-save-hook 'executable-make-buffer-file-executable-if-script-p)
M-u
やM-l
だけで十分なら必要ないでしょうが、たまに使いたくなるときがあるのです。
;;; リージョンの大文字小文字変換を有効にする。 ;; C-x C-u -> upcase ;; C-x C-l -> downcase ;; 2011-03-09 (put 'upcase-region 'disabled nil) (put 'downcase-region 'disabled nil)
ウィンドウの上部に現在の関数名を表示します。残念ながら大きい関数を編集しなければいけなくなったときに、今どこにいるかがわかりやすくなって便利です。
;; 2011-03-15 (which-function-mode 1)
ほとんどemacsclientは使いませんが、いつでもつながるようにはしています。
;; emacsclientで接続できるようにする。 ;; 2011-06-14 (server-start)
最後にconfig/以下に置いてある設定ファイルを読み込みます。~/.emacs.d/config/local.el
があればそれも読み込みます。local.el
はリポジトリに入っていないファイルです。このおすすめ設定を使う場合はlocal.el
を自作してそこでカスタマイズしてください。
;; 標準Elispの設定 (load "config/builtins") ;; 非標準Elispの設定 (load "config/packages") ;; 個別の設定があったら読み込む ;; 2012-02-15 (condition-case err (load "config/local") (error))
config/builtins.elには標準Elisp(Emacsに付属しているElisp)の設定を記述します。
diredで"V"を入力するとそのディレクトリで使っているバージョン管理システム用のモードを起動します。1年前のものより賢く検出するようになっています。
;; diredから適切なバージョン管理システムの*-statusを起動 (defun dired-vc-status (&rest args) (interactive) (let ((path (find-path-in-parents (dired-current-directory) '(".svn" ".git")))) (cond ((null path) (message "not version controlled.")) ((string-match-p "\\.svn$" path) (svn-status (file-name-directory path))) ((string-match-p "\\.git$" path) (magit-status (file-name-directory path)))))) (define-key dired-mode-map "V" 'dired-vc-status) ;; directoryの中にbase-names内のパスが含まれていたらその絶対パスを返す。 ;; 含まれていなかったらdirectoryの親のディレクトリを再帰的に探す。 ;; 2011-03-19 (defun find-path-in-parents (directory base-names) (or (find-if 'file-exists-p (mapcar (lambda (base-name) (concat directory base-name)) base-names)) (if (string= directory "/") nil (let ((parent-directory (substring directory 0 -1))) (find-path-in-parents parent-directory base-names)))))
自動でスペルチェックを実行します。スペルミスの単語は色が変わるのですぐに気づけます。
;; 2011-03-09 (setq-default flyspell-mode t) (setq ispell-dictionary "american")
テキスト編集用のモード共通の設定です。
;; 2012-03-18 ;; text-modeでバッファーを開いたときに行う設定 (add-hook 'text-mode-hook (lambda () ;; 自動で長過ぎる行を分割する (auto-fill-mode 1)))
C言語と同じような構文のプログラミング言語用の設定です。
;; 2012-03-18 ;; c-modeやc++-modeなどcc-modeベースのモード共通の設定 (add-hook 'c-mode-common-hook (lambda () ;; BSDスタイルをベースにする (c-set-style "bsd") ;; スペースでインデントをする (setq indent-tabs-mode nil) ;; インデント幅を2にする (setq c-basic-offset 2) ;; 自動改行(auto-new-line)と ;; 連続する空白の一括削除(hungry-delete)を ;; 有効にする (c-toggle-auto-hungry-state 1) ;; CamelCaseの語でも単語単位に分解して編集する ;; GtkWindow => Gtk Window ;; EmacsFrameClass => Emacs Frame Class ;; NSGraphicsContext => NS Graphics Context (subword-mode 1)))
Elispを編集するときの設定です。
;; 2012-03-18 ;; emacs-lisp-modeでバッファーを開いたときに行う設定 (add-hook 'emacs-lisp-mode-hook (lambda () ;; スペースでインデントをする (setq indent-tabs-mode nil)))
最後に~/.emacs.d/config/buitins/local.el
があればそれもそれも読み込みます。local.el
はリポジトリに入っていないファイルです。このおすすめ設定を使う場合はlocal.el
を自作してそこでカスタマイズしてください。
;; 個別の設定があったら読み込む ;; 2012-03-18 (condition-case err (load "config/builtins/local") (error))
パッケージ管理ステムとして複数のソースからパッケージをインストールできるel-getを使います。el-getがない場合は自動でインストールします。
;; 2012-03-15 (add-to-list 'load-path "~/.emacs.d/el-get/el-get") (unless (require 'el-get nil t) (with-current-buffer (url-retrieve-synchronously "https://raw.github.com/dimitri/el-get/master/el-get-install.el") (end-of-buffer) (eval-print-last-sexp))) ;; レシピ置き場 (add-to-list 'el-get-recipe-path (concat (file-name-directory load-file-name) "/el-get/recipes")) ;; 追加のレシピ置き場 (add-to-list 'el-get-recipe-path "~/.emacs.d/config/el-get/local-recipes")
レシピは~/.emacs.d/config/el-get/recipies/
に置いています。レシピを追加したい場合は~/.emacs.d/config/el-get/local-recipies/
ディレクトリを作ってその下に*.rcp
というファイルを作ってください。
grepの結果を直接編集できるようになります。wdiredと合わせてC-c C-cでも編集結果を反映できるようにしています。
;;; *grep*で編集できるようにする (el-get 'sync '(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
でも選択できるようにしています。
;;; 自動補完 (el-get 'sync '(auto-complete)) (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
とyank-pop
の代わりにだけ使っています。imenu
の代わりにも使ってみようとしています。
;;; Anything (let ((original-browse-url-browser-function browse-url-browser-function)) (el-get 'sync '(anything)) (require 'anything-config) (anything-set-anything-command-map-prefix-key 'anything-command-map-prefix-key "C-c C-<SPC>") (define-key global-map (kbd "C-x b") 'anything-for-files) (define-key global-map (kbd "C-x g") 'anything-imenu) ; experimental (define-key global-map (kbd "M-y") 'anything-show-kill-ring) (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))
;; 2012-03-19 ;; インストールされていたら有効にする。 (require 'migemo nil t)
Emacsに添付されているruby-modeは古いのでRubyのリポジトリに入っているものを使います。Emacsに添付されているruby-modeでは、C-c C-e
でend
を挿入することができなかったりします。
;; 2012-03-15 (el-get 'sync '(ruby-mode-trunk))
Rabbitのスライドを編集するためのモードです。
;; 2012-03-16 (el-get 'sync '(rabbit-mode))
C-x C-t
で近くにあるrun-test.shやrun-test.rbという名前のファイルを実行するツールです。
;;; テスト実行 (el-get 'sync '(run-test))
最後に~/.emacs.d/config/packages/local.el
があればそれもそれも読み込みます。local.el
はリポジトリに入っていないファイルです。このおすすめ設定を使う場合はlocal.el
を自作してそこでカスタマイズしてください。
;; 個別の設定があったら読み込む ;; 2012-03-15 (condition-case err (load "config/packages/local") (error))
Emacs実践入門と1年経ったおすすめのEmacsの設定を紹介しました。
ここで紹介した内容はGitHubに置いておいたので、興味がある人は試してみてください。使い方はREADMEを参照してください。ここで紹介した内容が難しい場合はEmacs実践入門を読んでみるとよいかもしれません。
Emacs実践入門 ~思考を直感的にコード化し、開発を加速する (WEB+DB PRESS plus)
技術評論社
¥ 2,678
*1 60ページの注2。
わかりやすいコードを書くことはソフトウェア開発において大切なことです。では、具体的にわかりやすいコードとはどんなものでしょうか?その観点はいろいろなものがあります。その中で今回はif
とreturn
の使い方に注目します。
if
とreturn
プログラミング言語とは、コンピューターの作業の処理手順を書くためにあります。その処理手順は複数にわかれています。その複数の処理手順を順番に実行していくことでコンピューターは作業をこなしていきます。
プログラミング言語にはいろいろな処理手順を書くためにif
とreturn
と呼ばれる機能があります。ある処理手順をある時だけ実行したい場合には、if
を使います。その時以外はその処理手順は実行しません。また、続きの処理手順があるがその時点で実行を中断したい場合には、return
を使います。続きの処理手順は実行しません。if
とreturn
と組み合わせることで、ある時だけ実行を中断することができます。
if
やreturn
を使うことで、特定の処理手順を実行したり、実行しなかったり、あるいは実行を中断することができるようになります。これにより、処理手順を順番に実行するだけにとどまらず、いろいろな処理手順をプログラミング言語として書くことができます。具体的には、「あの場合にはこれを実行して、その場合にはここで中断して」というように処理の流れを作ることができます。
以上の説明を元にして用語を整理します。今回の記事では、処理手順を「コード」と呼ぶことにします。そのコードの流れのことを「コードパス」と呼ぶことにします。
if
とreturn
の使い方次第でコードパスは自由に変えることができます。例えば、次の2つのコードは処理内容は同じですが、コードパスが異なります。
if
を使った場合:
1 2 3 |
if <ある場合> <処理手順> end |
if
とreturn
を使った場合:
1 2 3 |
return if <ある場合でない時> <処理手順> |
コードパスは、コードを読む時の流れでもあります。コードは読まれるものであり、わかりやすさが大切です。そうなると、わかりやすいコードを書くためにはわかりやすいコードパスが大切ということになります。今回の記事ではどのようなコードパスにしたらわかりやすくなるかについて説明します。
自然な流れに沿っていないコードパスになっているとわかりにくくなります。コードを読む側の立場に立ち、コードパスをしっかりと意識する必要があります。
メソッドごとでみたときにそのコードパスが自然な流れかを考えるとわかりやすくなります。メソッドごとでみる理由は、人が文章を読む時に文ごとに理解していくように、コードを読む時はメソッドごとに理解していくからです。
return
を使ったほうが良い場合悪い例:
1 2 3 4 5 6 7 8 |
def add_comment(post, user, text) if post.hidden_from?(user) report_access_error else comment = Comment.create(text) post.add(comment) end end |
良い例:
1 2 3 4 5 6 7 8 9 |
def add_comment(post, user, text) if post.hidden_from?(user) report_access_error return end comment = Comment.create(text) post.add(comment) end |
ここでのメソッド名はadd_comment
となっています。コードを読む時、まずはメソッド名を読みます。なので、まずメソッド名から「コメントを追加する」ためのメソッドであることを意識しながら、次にメソッドの定義を読んでいきます。この時点でコメントを追加するコードが大切だと推測しています。
悪い例ではつまづいてしまいます。メソッド名から大切だと推測したコードがぱっと見当たらず、よく読むと実際にはif
のelse
の中に追いやられているからです。大切でなければならないコードとそのコードの実際の扱われ方が一致していないために、わかりにくいコードになっています。
いい例では、大切なコードをelse
から出し、return
で中断されない限り必ず実行されるようにコードパスを変え、大切なコード相応の扱いにしています。
if
を使ったほうが良い場合悪い例:
1 2 3 4 5 6 7 |
def prepare_database(path) if not File.exist?(path) return Database.create(path) end Database.open(path) end |
良い例:
1 2 3 4 5 6 7 |
def prepare_database(path) if File.exist?(path) Database.open(path) else Database.create(path) end end |
ここでのメソッド名はprepare_database
となっています。まずメソッド名から「path
にあるデータベースを使うために用意する」ためのメソッドだと読み取ります。
悪い例ではつまづいてしまいます。なぜならば、メソッド名から推測されるコードの大切さと実際のコードの扱いが違うからです。なぜかpath
にあるファイルが存在しないとデータベースを作り中断しています。データベースを用意するメソッドとしては、データベースを作るのは問題無いはずであり、あえてファイルが存在しない時に限ってデータベースを作って中断する必要はありません。このままでは、あえてこのようなコードパスになっている理由を勘ぐってしまいます。
いい例では、ファイルが存在する場合と存在しない場合の2通りあるデータベースを用意するという処理を対等に扱っています。このメソッドの場合、データベースを開く場合と作る場合は対等の扱われるべきであり、その観点からいえば、開くためのコードをif
に入れるかelse
に入れるは重要ではありません。しかし、次の3つの補助的な理由により良い例のように開くためのコードをif
に入れた方がいいでしょう。
else
ではなくif
の中に入れたほうがいいでしょう。if
の中にデータベースを作るコードを入れる場合は、if
の条件は、if not File.exist?
となり、not
を入れなくてはいけません。基本的には、not
を使ってまでコードパスを変える必要があるのは条件がわかりにくくなるデメリットを上回るくらいに、if
にこのコードを入れたいという強い意志をコードに込める必要がある時だけです。今回はそうではないので、not
を使わず、より分かりやすい条件になるようにしたほうがいいでしょう。if
にデータベースを作るコードを、else
にデータベースを開くコードを入れて、初めて実行されるコード、2回目以降に実行されるコードと、順番を合わせた方がいいという意見があるかもしれません。たしかに、その観点からのif
とelse
の順序はあっているように思えます。しかしその反論としては、コードをメソッドごとに読むとき、順序がそうなっているのはあまり気にしません。そうしたくなるのは書く側の立場であり読む側の立場ではありません。読む側からしてみればデータベースを用意する時にいつもすることは何なのかが気になり、それはデータベースを開くことです。それをしっかり伝えるために、if
とelse
のコードを実行の順序に合わせる必要はないでしょう。if
を使い過ぎないif
を使うと、コードパスに別の流れが作られます。さらに別のif
を使うことで、さらに別の流れが作られます。コードパスの流れが大きいとき(if
のコードが長い)やまた流れが多いとき(if
が多い)、コードはわかりにくくなります。
流れの大きさや多さを極力抑えることでわかりやすいコードになります。コーディングスタイルとしてインデントが深ければ深いほど分かりにくくなるのでそれを避けるというのはよくあります。それはこのことです。
悪い例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def mark_object(collector, object) if not object.marked? if collector.owning?(object) heap_list = collector.ensure_heap_list if heap_list.in_current_heap?(object) instance_variables.each do |instance_variable| mark_object(collector, instance_variable) end object.mark end end end end |
良い例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def mark_object(collector, object) return if object.marked? return if not collector.owning?(object) heap_list = collector.ensure_heap_list return if not heap_list.in_current_heap?(object) instance_variables.each do |instance_variable| mark_object(collector, instance_variable) end object.mark end |
悪い例ではつまづいてしまいます。if
が多くて大きいからです。このメソッドで大切なのはobject.mark
ですが、それがif
の中へ中へと押し込まれています。ちなみに、大切なコードがelse
ではなくif
の中にあるという観点では良いコードです。
良い例では、object.mark
をif
の中にまったく入れていません。そうするために、object.mark
をする必要が無い場合になったらメソッドから抜けるようにif
とreturn
を使ってコードパスを変えています。
return
を使う悪い例:
1 2 3 4 5 6 7 8 9 10 |
def add_comment(post, user, text) if not post.hidden_from?(user) comment = Comment.create(text) if commnet.valid? post.add(comment) else report_invalid_comment(comment) end end end |
良い例:
1 2 3 4 5 6 7 8 9 10 11 |
def add_comment(post, user, text) return if post.hidden_from?(user) comment = Comment.create(text) if not comment.valid? report_invalid_comment(comment) return end post.add(comment) end |
大切なコードが想定する前提と違う場合になるのは、エラーとなります。悪い例では、エラーを拾うためにif
を使って、本来大切なコードをif
の奥深くにしまいこんでいるためわかりにくいです。
エラーになった場合には、return
を使って本流のコードパスからわかれたエラー用のコードパスを用意するとわかりやすくなります。
if
とelse
とはほぼ対等に扱いたいときに使うコードパスです。大切なコードとエラー処理のコードは、悪い例のように対等に扱われるべきコードでしょうか?そうでは無いはずです。大切なコードとエラー処理のコードの扱い方にはreturn
を使ってしっかりとした差をつけるべきです。
また、悪い例と比較し良い例ではエラー処理をする必要がある場所に気づきやすくなります。悪い例では暗黙的にあるelseがエラー処理の場所ですが、良い例では明示的にあるreturn
がエラー処理の場所だからです。これも良い例の分かりやすさの一つです。
悪い例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Item def banner(user) if Time.now >= Config.urgent_clearing_sale_day # TEMPORARY CODE; REMOVE THIS AFTER THE SALE "99% OFF!!!" elsif discount? "10% OFF!" elsif recommended?(user) "Recommended" else nil end end end |
良い例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Item def banner(user) if Time.now >= Config.urgent_clearing_sale_day # TEMPORARY CODE; REMOVE THIS AFTER THE SALE return "99% OFF!!!" end if discount? "10% OFF!" elsif recommended?(user) "Recommended" else nil end end end |
この例ではショッピングサイトを想定しています。"99% OFF!!!"
はかなり特別なコードです。コメントからわかるように一時的に追加されたコードであり今後削除されるコードです。つまりは、運用上の一時的な対応のためのコードであることが推測できます。
特別なコードが普通のコードに混じっているとわかりにくいです。
特別なコードは特別なコードとして、それ専用の特別なコードパスを用意してあげるとわかりやすくなります。また、特別なコードを追加した時、特別なコードを削除する時にも、コードレベルで普通のコードと明確に分離しているので、コードの変更時にもミスをしにくくなります。
今回はif
とreturn
の使い方について、それらの組み合わせからできるコードパスは大切であり、わかりやすくするためのif
とreturn
の使い方を説明しました。
具体的な使い方として、コードパスを意識し、if
を使いすぎず、エラーの時や特別な時はreturn
を使えばわかりやすくなるということを説明しました。