gofujita notes

on outline processing, writing, and human activities for nature


Org との日々 #11

7. 小さく試しながらコードを育てる

さて今回は、前節で紹介した Mark Longair さんの手づくり関数remove-newlines-in-regionをほんのちょっとかきかえて、望みのしごとしてくれるプログラムに少し近づける作業を紹介します。

Emacs はテキストエディタですが、 Emacs Lisp (以下、elisp) というプログラミング言語のインタープリタでもあります。elisp でコードをかき、そのコードを小さく走らせながら、その場でテストできます。

何度かかいてきましたが、Emacs や Org をつかう最大の魅力は、ユーザーが自分に合った道具として Emacs を育てられる機能を Emacs 自身が生まれながらに備えている点だと思います。あとづけではなく最初からそれを目指してデザインされ、ユーザーのつくる関数も Emacs として最初に開発者たちが用意した関数も、Emacs の上では平等にあつかわれるのです。

そうした Emacs のよさを多くの人が味わえるようになるためには、ここで紹介するような小さなヘナチョココーディングに慣れ親しんでいくことが、大切な一歩になると考えています。そして、ぼくのような初心者にとって、かいたコードの動作を小さく試せる環境こそ、プログラミングを学ぶ上でも大切なものだと考えています。

Emacs や Org で elisp を実行する方法はいくつもあるのですが、ここでは、ぼくがいちばんかんたんと思う、今開いて原稿をかいているテキストファイルに直接コードをかいて走らせるやり方を紹介します。

 

7-a. コードをコピーペイストする

まず、Mark さんの手づくり関数remove-newlines-in-regionのコードを、今開いているテキストファイル (たとえば my_outline.org) にコピーペイスト copy & paste します。Emacs のことばに倣うと saving to kill ring & yanks します (長いですね.. 笑)。


(defun remove-newlines-in-region ()
  "Removes all newlines in the region."
  (interactive)
  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n" nil t) (replace-match "" nil t))))

最初にかいたように、テキストエディタでありながら elisp のインタープリタでもある Emacs では、文章をかいている途中に同じ画面の同じファイルにコードをかき、そのコードを実行できます。わざわざ別のファイルをつくったり、コンソールパネルを開いたりする必要はありません。

(もちろん、あなたの好みにあわせて別ファイルつくってもいいですし、たとえば ielm とよばれる Emacs と対話しながらコード実行するためのモードを別のバッファ [窓] に開くこともできます)

コードを実行するには、まずコード最後の括弧の右にカーソルを置いてC-x C-eとタイプします。すると、コード全体が瞬間グリーンにハイライトされ、画面いちばん下の1行窓 echo area にこの関数名remove-newlines-in-regionが表示されれば、関数のインストール成功です。

では、実際にこの関数をつかってみましょう。

たとえば以下の形をしたテキストデータをすべて範囲選択し、この関数を実行する (M-x remove-newlines-in-region) と..

こんな風に、ひとつのパラグラフにまとまってしまいます。

 

7-b. 小さいゴール

で、この関数を自分好みに変える作業をはじめます。最初に自分の望む小さなゴールを、自分のためにクリアにしておきます。

2-d 節で紹介したように、たくさん改行しながら文章をかく (その方が文章を推敲しやすいのです) ぼくとしては、ちょっと不便。多くの改行はなくして連続した行としてつなげたいのですが、パラグラフの区切りだけは空白行として残したいからです。

上の例でいうと、テキストのふたつの塊のあいだの空白行だけは残したい。こんな形で。

 

7-c. 関数の名前を変える

最初に、ぼくの関数をつくるのだという気分を盛り上げるために、remove-newlines-in-regionという名前を変え、その関数の機能説明 (" "で囲まれたセンテンス) も作文しましょう。


(defun delete-linebreaks ()
  "Delete linebreaks but keep blanks separating paragraphs."
  (interactive)
  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n" nil t) (replace-match "" nil t))))

1行目のdefunの右にあるdelete-linebreaksが、新しい名前です (ほんとは関数名だけで機能の要点を理解できるのがベストですが、今はこれでヨシとしましょう)。

説明として「パラグラフを分ける空白行は残して、改行を削除する」とかきました。2行目です。なんかこれだけで、自分の関数をつくった気分になります (笑)。

 

7-d. おさらい

問題は、実際の仕事をするコード本体。以下の部分です。


  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n" nil t) (replace-match "" nil t))))

前節のおさらいをすると、このコード本体は、次のような構造をもっています。

  • 関数A save-restriction (その時点でフォーカス narrowing している範囲を保存)
    • 関数B narrow-to-region (編集範囲を範囲指定した先頭から末尾までに指定)
    • 関数C goto-char (カーソルを範囲指定の先頭へ移動)
    • 関数D while (関数Eが nil を返すまで関数Fを繰り返す)
      • 関数E search-forward (末尾へ向かって "\n" を探索。すべて探索したら nil を返す)
      • 関数F replace-match (関数Eの見つけた "\n" を "" (何もなし) に置換)

パラグラフを区切る空白は残したまま、改行をなくすのが目的ですから、その作業に直接関係しているのは、関数Dとその下位にある関数EとFですね。

 

7-e. 小さくつくり変える案を考える

つぎに、パラグラフを区切る空白を残すにはどうすればいいかを考えました。この空白は、return key 改行マーク (\n) を2回連続で入力することでつくられています。つまり、改行マークが2つ連続しているところになります。

ですから、改行マークが2つ連続している場所はそのまま残し、改行マークひとつしかない場合ぬ削除するという関数をつくればよいことが分ります。このアルゴリズムをテキトーな文章にするとこうなるでしょうか。

  1. カーソルを指定範囲の先頭に置く。
  2. 指定範囲内の文字列を前方へ向かって検索する。
  3. 改行マーク2つはそのまま。
  4. 改行マークひとつは削除する。

この段階で、何も考えずにコードを考える (意味不明?) と、ステップ3と4で「if.. else..」をつかって条件分岐したくなります。

でも残念ながら、elisp 初心者のぼくにとって、これは手にあまる作業。ステップ1 の while (条件が満たされるまで作業をつづけるループ) の条件式に (search-forward .. ..) が入っているからです。

この構造の中で if をつかって条件分岐するためには、(while .. ..) の下に (if .. ..) というもう1段階深い階層で条件分岐し、それぞれのふるまいをかくことになります。

elisp 初心者のぼくとしては、条件式の階層を深くするのは、まちがいが起こる元なのでできるだけ避けたい。過去に何度も、このループの中の条件式でイタイ目に会ってきました (笑)。

なので、while ループに if を入れないで済ませられる、こんな手を考えました。

 

  1. カーソルを指定範囲の先頭に置く。
  2. 指定範囲内の文字列を前方へ向かって検索する。
  3. 改行マークのふたつ連続を一時的な別文字列 (普段使われない文字列) に置換する。
  4.  

  5. カーソルを指定範囲の先頭に戻す。
  6. 指定範囲内の文字列を前方へ向かって検索する。
  7. 改行マークを削除する。
  8.  

  9. カーソルを指定範囲の先頭に戻す。
  10. 指定範囲内の文字列を前方へ向かって検索する。
  11. ステップ3の普段使われない文字列を改行マークふたつ連続に戻す。

 

このやり方も、こうした場面でよく使われるレシピみたいなものだと思います。これであれば、初心者のぼくも、とりあえず動く試作品がすぐにつくれそうです。

似た作業が繰り返し3回出てくるので、ここをスマートにまとめたくなりますが、今はそうメモにかくだけにして、まずはコレでよし。

「とにかく動く試作品を少しでも早くつくろう」。『UNIX という考え方』で Mike Gancarz さんが教えてくれたことばを思い出して、はやる心を落ちつけましょう (笑)。

 

7-e. 作業開始

まずステップ1から3まで。


(defun delete-linebreaks ()
  "Delete linebreaks but keep blanks separating paragraphs."
  (interactive)
  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n\n" nil t) (replace-match "***" nil t))))

いちばん下の行の" "の部分を2か所かき変えただけですが、これで改行マーク2つの部分が「***」に置き換えられるはずです。

試してみましょう。まずコードの最後の閉じ括弧の右にカーソルをおいてC-x C-e。画面いちばん下の1行窓 echo area にdelete-linebreaksが表示されたら、整形したいテキストデータ全体を範囲指定してM-xとタイプ。

画面の下半分に開いたバッファのいちばん上にある1行窓の「M-x」の右に「del..」とタイプすると、すぐ下に候補としてインストールされ たばかりの「オレの関数」delete-breaklinesの名前が表示されるはずです。

そして、下矢印キーを押すか、そのままフルスペルをタイプしreturn keyで関数が実行されます。

何だか変わり映えしませんが、よくみてください。「テキストデータです。」と「ひとつ前の」とのあいだの空白行が「***」に変わっています。シメシメ。

つぎにステップ 4–6 のコードを加えます。閉じる括弧の数をまちがえないように。閉じ括弧のうしろにカーソルをおけば、Emacs が対応する括弧の位置をハイライトして教えてくれます。


(defun delete-linebreaks ()
  "Delete linebreaks but keep blanks separating paragraphs."
  (interactive)
  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n\n" nil t) (replace-match "***" nil t))
    (goto-char (point-min))
    (while (search-forward "\n" nil t) (replace-match "" nil t)) ))

いちばん下の2行が、新しく加わりました。これもコピペ.. もとい、saving to kill ring & yanks して" "の中を変えただけですが、試してみましょう。

改行がなくなって、でも「***」は残ったまま。ヨシヨシ。あとは「***」を改行マークふたつ連続に戻すだけ。つまり、ステップ 7–9 のコードをかき加えます。


(defun delete-linebreaks ()
  "Delete linebreaks but keep blanks separating paragraphs."
  (interactive)
  (save-restriction
    (narrow-to-region (point) (mark))
    (goto-char (point-min))
    (while (search-forward "\n\n" nil t) (replace-match "***" nil t))
    (goto-char (point-min))
    (while (search-forward "\n" nil t) (replace-match "" nil t))
    (goto-char (point-min))
    (while (search-forward "***" nil t) (replace-match "\n\n" nil t))))

これもいちばん下に2行加わっただけですが、試しましょう。

とにかくこれで、小さなゴールである「パラグラフ間の空白行を残して、改行をすべてなくす」関数の試作品ができました。

(乾杯!)

 

7-f. いつもつかえる関数にする

さあ「オレの関数」の完成です (実はsaving to kill ring & yanks を2回と、ほんの数文字かき変えただけ.. 笑)。

しかしこのままでは、このオレ関数delete-linbreaksは、Emacs を終了してしまうと消えてなくなってしまいます。

なのでいつもこの関数がつかえるよう、起動時に自動的にこの関数をよみこむように設定しましょう。設定ファイルである.spacemacs にコードをかきこみます。

このファイルのどこにコードをかけばいいのか、もうあなたは答えをしってますね。かく場所を迷ったらまずはuser-configにかいてみる、でしたね。.spacemacsファイルにかかれている4つの関数のうち、いちばん最後にあります。

コードをかいたらC-x C-s.spacemacsを保存して、仕上げ作業もおしまい。次回から起動時に「delete-linebreaks」がインストールされ、いつでもM-xで呼び出すことができます。

とてもよく使う関数なので、ぼくはC-oといういちばんタイプしやすい形にキーバインドもしています。これもuser-configに設定をかいています。

こうして使えるようになったヘナチョコなオレ関数delete-linebreaksのおかげで、ぼくが Org でのかき心地を日々愉しめるようなったことはいうまでもありません。

遠慮せず、どんどん改行しながらフリーライティングし、そのたて長で改行だらけのテキストをひとしきり按配したら、C-oで、段落を残しながら改行を一括削除できる。

コードのちがいはごくわずかですし見かけはほとんど変わりませんが、ぼくにとって自分のやり方で文章をかきやすいテキストエディタが、はじめてこの世界に生まれたような感覚があります。