株式会社クリアコード > ククログ

ククログ


The Art of Readable Code

2011年11月に「理解しやすい、読みやすいコードはどのように書けばよいか」という内容の本がO'Reillyから出版されました。(英語です。)

The Art of Readable Code: Simple and Practical Techniques for Writing Better Code (Theory in Practice)
Dustin Boswell/Trevor Foucher
O'Reilly Media
(no price)

英語なので読む人はいないでしょうが紹介します。読みやすい英語で書かれているので、「読みやすいコードだけではなく英語の勉強にも興味がある」という人にはちょうどよいでしょう。ページ数も200ページにいかないぐらいとコンパクトにまとまっています。

さて内容ですが、第一部が「SURFACE-LEVEL IMPROVEMENTS」です。見た目をよくしようという話です。具体的な内容は読んでもらうとして、まず、見た目のよさから入っているところが「わかっている感」をだしていますね。

読みやすいコードにするテクニックはいろいろありますが、まずは見た目が整っていないと、どんなにテクニックを駆使していても読みづらくなってしまうものです。どんなにすっきり設計できているプログラムでも、インデントが崩れていたり、ピンとこない名前や省略しすぎた名前を使っているコードは読みづらくなってしまいます。長すぎるメソッドも見た目が悪いですね。

よいコードを書くためのもっと重厚な本はいくつも出版されていて、これだけは読んでおけ!と言われている本もいくつもあります。(この本の参考文献でもいくつも挙げられています。)ふつうのステップでは、まずこの本を読んで助走をつけてから他の本を読むとよい、ということになりそうです。

しかし、逆に他の良書と呼ばれている本を読んでからこの本を読む、あるいは、これまでたくさんコードを書いてきて自分なりにこうした方がよいというのができている人が読む、というのもよいでしょう。というのは、この本を読むことで、あなたが忘れかけていたことやあなたが知らない間にやっていたことに気づくことができるはずだからです。そうすれば、あなたがやっている「よいコードを書くためのこと」をより上手に他の人に伝えることができるようになっているはずです。また、「よいコードを書くために大事なこと」だとわかっているけど面倒だったり何かしら理由をつけて実践していないことがあるのなら、それを気づかせてくれるでしょう。

とはいえ、やはり英語だとなかなか読む気にならないことでしょう。そんなあなたにうれしいお知らせがあります。7月あたりにあの角さんの訳で翻訳版が出版されるそうです。楽しみですね。

さぁ、忘れてもよいコードを書きましょう。

2012-03-05

クリアなコードの作り方: 意図が伝わるコミットのしかた

コミットメッセージの書き方ではコミットをわかりやすくするためには以下の2つの条件を満たす必要があると書きました。

  1. コミットの内容が分かりやすく説明されていること
  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毎にコミットをわけましょう。またコミットメッセージにどんな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_SUCCESSEXIT_FAILUREという名前のついた値にするためのコミットです。もし、間違って0EXIT_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-03-13

Emacs実践入門 - おすすめEmacs設定2012

2012年3月にEmacsの入門書が技術評論社から出版されました。

Emacs実践入門 ~思考を直感的にコード化し、開発を加速する (WEB+DB PRESS plus)
大竹 智也
技術評論社
¥ 2,678

インストール方法やファイルの開き方などから始まっていて初心者向けの始まり方になっています。それでは初心者向けなのかというとそうでもなく、中盤から後半はrequireしないと使えないElispを使った拡張方法の紹介になっています。

おそらく、初心者の人は1/3か1/2くらい進んだところで一度脱落するのではないでしょうか。逆に、ある程度知っている人は中盤から後半にかけて興味のある話題が増えていくことでしょう。脱落してしまった人は、しばらく前半の機能でEmacsを使って、慣れてきてから再挑戦するとよいでしょう。

後半の拡張方法の紹介部分では多くの方法を紹介するためか、1つ1つの方法については簡単に紹介する程度にとどまっています。よりつっこんだ使い方までは踏み込んでいません。そのため、すでに最近のEmacs界隈の状況を把握している人やバリバリカスタマイズして使っている人にとっては物足りない内容かもしれません。そうでない人は、こんな方法があるのかと気づくことも多いでしょう。

ということで、今ひとつEmacsを使いこなせていない感のある人は読んでみてはいかがでしょうか。Emacsがより手になじむことになるでしょう。

おすすめEmacs設定2012

さて、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にした理由は元々使っていたパッケージ管理システムと同じことができたからです。

init.el: 基本的な設定

それでは、まず、基本的な設定を説明します。

ロードパス

以前は~/.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
;; 再帰的に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
;;; 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)
diff
;;; 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-uM-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)
Emacsサーバー

ほとんど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の設定

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")
text-mode

テキスト編集用のモード共通の設定です。

;; 2012-03-18
;; text-modeでバッファーを開いたときに行う設定
(add-hook
 'text-mode-hook
 (lambda ()
   ;; 自動で長過ぎる行を分割する
   (auto-fill-mode 1)))
cc-mode

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)))
emacs-lisp-mode

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))
config/packages.el: 非標準Elispの設定
el-get

パッケージ管理ステムとして複数のソースからパッケージをインストールできる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-edit: grep結果をインラインで編集

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)))
Auto Complete: 自動補完

自動で補完候補をだしてくれて便利です。補完候補を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

いろいろ便利に使えるらしいAnythingですが、iswitchb-modeyank-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))
Migemo: ローマ字で日本語をインクリメンタルサーチ
;; 2012-03-19
;; インストールされていたら有効にする。
(require 'migemo nil t)
ruby-mode: Ruby編集用モード

Emacsに添付されているruby-modeは古いのでRubyのリポジトリに入っているものを使います。Emacsに添付されているruby-modeでは、C-c C-eendを挿入することができなかったりします。

;; 2012-03-15
(el-get 'sync '(ruby-mode-trunk))
rabbit-mode: Rabbitスライド編集用モード

Rabbitのスライドを編集するためのモードです。

;; 2012-03-16
(el-get 'sync '(rabbit-mode))
run-test: テスト実行

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。

2012-03-20

ifとreturnの使い方

はじめに

わかりやすいコードを書くことはソフトウェア開発において大切なことです。では、具体的にわかりやすいコードとはどんなものでしょうか?その観点はいろいろなものがあります。その中で今回はifreturnの使い方に注目します。

ifreturn

プログラミング言語とは、コンピューターの作業の処理手順を書くためにあります。その処理手順は複数にわかれています。その複数の処理手順を順番に実行していくことでコンピューターは作業をこなしていきます。

プログラミング言語にはいろいろな処理手順を書くためにifreturnと呼ばれる機能があります。ある処理手順をある時だけ実行したい場合には、ifを使います。その時以外はその処理手順は実行しません。また、続きの処理手順があるがその時点で実行を中断したい場合には、returnを使います。続きの処理手順は実行しません。ifreturnと組み合わせることで、ある時だけ実行を中断することができます。

ifreturnを使うことで、特定の処理手順を実行したり、実行しなかったり、あるいは実行を中断することができるようになります。これにより、処理手順を順番に実行するだけにとどまらず、いろいろな処理手順をプログラミング言語として書くことができます。具体的には、「あの場合にはこれを実行して、その場合にはここで中断して」というように処理の流れを作ることができます。

以上の説明を元にして用語を整理します。今回の記事では、処理手順を「コード」と呼ぶことにします。そのコードの流れのことを「コードパス」と呼ぶことにします。

ifreturnの使い方次第でコードパスは自由に変えることができます。例えば、次の2つのコードは処理内容は同じですが、コードパスが異なります。

ifを使った場合:

1
2
3
if <ある場合>
  <処理手順>
end

ifreturnを使った場合:

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となっています。コードを読む時、まずはメソッド名を読みます。なので、まずメソッド名から「コメントを追加する」ためのメソッドであることを意識しながら、次にメソッドの定義を読んでいきます。この時点でコメントを追加するコードが大切だと推測しています。

悪い例ではつまづいてしまいます。メソッド名から大切だと推測したコードがぱっと見当たらず、よく読むと実際にはifelseの中に追いやられているからです。大切でなければならないコードとそのコードの実際の扱われ方が一致していないために、わかりにくいコードになっています。

いい例では、大切なコードを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に入れた方がいいでしょう。

  1. このメソッドは、データベースがあろうがなかろうが関係なしにデータベースを用意するためのメソッドです。そこを抽象化して、このメソッドを使う側はデータベースを透過的に扱えるようにしています。このメソッドを深読みすると、このメソッドが何度も使われるなかで、いつもは存在しているデータベースを用意するけど、もしなかったら作ってから用意する(初回起動時など)ということもわかります。つまりは、存在しているデータベースを開いて用意する場合が多いと考えられます。よって、よく通るコードパスはelseではなくifの中に入れたほうがいいでしょう。
  2. ifの中にデータベースを作るコードを入れる場合は、ifの条件は、if not File.exist?となり、notを入れなくてはいけません。基本的には、notを使ってまでコードパスを変える必要があるのは条件がわかりにくくなるデメリットを上回るくらいに、ifにこのコードを入れたいという強い意志をコードに込める必要がある時だけです。今回はそうではないので、notを使わず、より分かりやすい条件になるようにしたほうがいいでしょう。
  3. ifにデータベースを作るコードを、elseにデータベースを開くコードを入れて、初めて実行されるコード、2回目以降に実行されるコードと、順番を合わせた方がいいという意見があるかもしれません。たしかに、その観点からのifelseの順序はあっているように思えます。しかしその反論としては、コードをメソッドごとに読むとき、順序がそうなっているのはあまり気にしません。そうしたくなるのは書く側の立場であり読む側の立場ではありません。読む側からしてみればデータベースを用意する時にいつもすることは何なのかが気になり、それはデータベースを開くことです。それをしっかり伝えるために、ifelseのコードを実行の順序に合わせる必要はないでしょう。

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.markifの中にまったく入れていません。そうするために、object.markをする必要が無い場合になったらメソッドから抜けるようにifreturnを使ってコードパスを変えています。

エラーの時や特別な時は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を使って本流のコードパスからわかれたエラー用のコードパスを用意するとわかりやすくなります。

ifelseとはほぼ対等に扱いたいときに使うコードパスです。大切なコードとエラー処理のコードは、悪い例のように対等に扱われるべきコードでしょうか?そうでは無いはずです。大切なコードとエラー処理のコードの扱い方には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!!!"はかなり特別なコードです。コメントからわかるように一時的に追加されたコードであり今後削除されるコードです。つまりは、運用上の一時的な対応のためのコードであることが推測できます。

特別なコードが普通のコードに混じっているとわかりにくいです。

特別なコードは特別なコードとして、それ専用の特別なコードパスを用意してあげるとわかりやすくなります。また、特別なコードを追加した時、特別なコードを削除する時にも、コードレベルで普通のコードと明確に分離しているので、コードの変更時にもミスをしにくくなります。

まとめ

今回はifreturnの使い方について、それらの組み合わせからできるコードパスは大切であり、わかりやすくするためのifreturnの使い方を説明しました。

具体的な使い方として、コードパスを意識し、ifを使いすぎず、エラーの時や特別な時はreturnを使えばわかりやすくなるということを説明しました。

2012-03-28

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|