textlintで日本語テキストの文字校正を試してみた

はじめに

textlintについては少し前から時々名前を聞くなと思っていましたが、自分に関わりがありそうなものとして意識したきっかけは、@t_wada さんによる以下のツイートだったと思います。

これを見た時点では、まあそういうこともあるだろうな、という程度の軽い感想でしたが、その後じわじわと「気になる」感じが増してきて、ようやく週末に少し時間を取れそうだったので、とりあえず1〜2日トライしてみたのが以下の話です。

先にぼくの背景について簡単に記しておくと、ぼくは3年前のちょうど今頃からプログラミングに入門し、最初はRubyJavaScriptを触っていたのだけど、同年8月頃にたまたまPerlを教えてくれる人たちに出会って、以後このブログに書いてきたようなことをしている非エンジニアの編集者です。

※その他の履歴についてはこちらをどうぞ。 → Profile

プログラミング歴3年とは言っても、普段の仕事ではとくにプログラミングを活用したり学習したりする機会もなく、ようやくチョット使えるようになってきたのもPerlVimぐらいで、今回のテーマであるtextlintまわりで使われているJavaScriptなどの知識はほとんど無いので、いろいろ勘違いしているところもあるかと思いますが、その際にはコメント欄やTwitterにて適宜ご指摘ください。

導入

さて、上記のような興味とともに最初にチェックしたのは、textlint作者である @azu さんの以下のブログ記事でした。
efcl.info

レポジトリにも導入のチュートリアルがあるので、どちらを見ながらスタートするか少し迷いましたが、
github.com

日本語で書かれている前者の記事をぼんやり目で追いながら、順にコマンドを打ち込んでいったらいつの間にか最初の環境づくりはほぼ完了していました。

※具体的には、textlint本体とmax-ten, spellcheck-tech-word, no-mix-dearu-desumasuというルール群をインストールして、試しに動かしてみるまで。

ちなみに、とりあえずtextlintを使ってどのような校正が成されるのか体験してみたい、という目的であれば、その記事でも紹介されているように Chrome拡張を使うという選択肢もあります。

ぼくの場合は、以前にVimJSONシンタックスチェックを設定した際、少しだけnpmを触ったことがあって、

note103.hateblo.jp

かつ、このときにはけっこうハマったので、その復習もかねてコマンドラインからのインストール&実行を試すことにしました。

一瞬話がそれますが、僕がこういうことをやる一番の目的は、「文字校正さえできればいい」といった最終形を求めてのことではなく、それができるまでの過程を体験しながら、それが置かれているシステムの裏側(仕組み)を知りたいということ、またそれを身につけて自分一人でも手元でイチから再現できるようにしたい、といった点にあるようで、それがこういうトライの原動力になっているようにも思えます。

textlintrcを設置

本題に戻ると、上記の導入記事で紹介されている最初のルール群を入れた後は、そのまま案内に沿って textlintrc というvimrc的なものを作りました。

書式はJSONYAML、JSモジュール(って何?)のいずれでも良いようですが、ここは素直に @azu さんが例示していた以下の形で。

{
  "rules": {
    "max-ten": {
      "max": 3
    },
    "spellcheck-tech-word": true,
    "no-mix-dearu-desumasu": true
  }
}

これはJSON形式だと思いますが(いちいち自信がない)、JavaScriptで使うようなコメントアウトが使用可能なので後々大変助かりました。コメントアウト便利。

で、これを書いたら「.textlintrc」としてホームディレクトリに設置します。
といっても、実際にはvimrcやbashrcと同様、ファイル本体はドットファイルを集めるディレクトリに入れて、そこからシンボリックリンクでホームディレクトリへリンクしています。

$ ln -s /path/to/dotfiles/textlintrc ~/.textlintrc

このtextlintrcを設定して何が嬉しいのかというと、たとえばこれを設定せずに上記のルールを使おうとしたら、

$ textlint --rule no-mix-dearu-desumasu --rule max-ten --rule spellcheck-tech-word README.md

というふうに、けっこう壮大な引数をタイプする必要がありますが、textlintrcを設定しておけば、

$ textlint README.md

だけで済みます。(上記二つのサンプルコードは @azu さんのブログより)

これは使わない手はないですね。

感覚的には、vimrcに各種Vimプラグインやそれぞれの設定を記述していくことに近いと感じました。

最初のつまずき

環境設定はそんなところで、ここからは実際のテキストを使ってツールを動かしていきます。

サンプルテキストには、ぼくらの夏目漱石&ぼくらの青空文庫から「草枕」をお借りしました。

山路やまみちを登りながら、こう考えた。
 智ちに働けば角かどが立つ。情じょうに棹さおさせば流される。意地を通とおせば窮屈きゅうくつだ。とかくに人の世は住みにくい。
 住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
(略)

時間の都合上、ブラウザでそのままコピペしただけなのでルビもそのまま残っていますが、サンプルなのでご容赦ください。

全文だとさすがに長すぎるので、冒頭の一部を「kusamakura.txt」というファイルに保存して、さっそく実行。

f:id:note103:20160612202105g:plain

ん……? なんとなく良いようにも見えるけど、最後のほうがなんか変?

これってそもそもこういう表示をするツールだということなのか、それともまだ鋭意開発中のものだから環境によってはちゃんと動かない、ということなのか……? といろいろ考えてしまいました。
これが今回最初のつまずき。

これに対しては、その後「とりあえずlessで見てみるか……」と思いついたのが幸いして。

f:id:note103:20160612202146g:plain

今度は最後まで表示されました。さっきのはやはり途中で切れていたようです。

見たところ、漱石先生は一文あたりの読点がけっこう多いようですね……。

prh を使ってみる | 2度目のつまずき

基本的な動き方は確認できました。ここからは、これをどう応用できるのか、どんなバリエーションを付けていけるのか、といったことを考えていくわけですが、このツールではあまりにもいろんなことが出来そうで、また実際「こんなこともできるよ!」という説明も膨大にあるようで、もはやワクワクするのを通り越してウツになりそう、できれば最小限のことだけをとりあえずできるようになりたい……などと思いつつ、それでもとりあえずこれだけはやってみたい! と思ったのがこちら。

efcl.info

ここで紹介されている @vvakameさんは技術(書)界隈では有名な方で、ぼくも以前にRe:VIEWの開発者カンファレンスに参加した際に発表を拝見しましたが、

※参考: 書籍執筆支援システム「ReVIEW」に触ってみた話(&リンク集) - the code to rock

このtextlint-rule-prhというのは、その@vvakameさんによるproofread-helper(以下「prh」)というツールをtextlintから使うというもの。
github.com

ということで、さっそく案内にあるとおりにあれこれ設定して実行してみます。

f:id:note103:20160612035210g:plain

おぉっと、盛大なエラー……。
メッセージをコピペすると、こんな感じ。

Unhandled rejection Error: Error while loading rule 'prh': ENOENT: no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'
    at Error (native)
    at Object.fs.openSync (fs.js:634:18)
    at Object.fs.readFileSync (fs.js:502:33)
    at Object.fromYAMLFilePath (/usr/local/lib/node_modules/textlint-rule-prh/node_modules/prh/lib/index.js:9:22)
    at createPrhEngine (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:30:35)
    at reporter (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:79:26)
    (略)

本当はもう少し長いエラーですが、注目すべきはたぶん1行目のこれで。

no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'

「/Users/kadomatsuhiroaki」というのはぼくのMacのホームディレクトリ。そして「/sample/textlint」というのは今作業をしているディレクトリです。

そのつなぎ目がなんか変ですね。

/textlint/~/prh.yml

これは何かというと、おそらくtextlint-rule-prhの説明を見ながら書いた、textlintrc内の以下の記述が影響しているようです。

    "prh": {
      "rulePaths": [
        "~/prh.yml"
      ]
    },

ぼくとしては、「~/prh.yml」と書くことによって、「ホームディレクトリ直下にprh.ymlというファイルを置いたよ」とコンピューターに指示したつもりでしたが、上のエラーから察するに、そういう意味では受け取られておらず、コードを実行しているカレントディレクトリからの相対パスとして「~/prh.yml」が受け取られているようです。

しかしこの仕様だと、ファイルを実行するディレクトリごとに異なるprh.ymlを設置することになるので、本当にそうなのかなあ? としばらくハマりました。(使い方や理解の仕方が間違っているのかなと)

僕の感覚では、こういう多種多様なファイルを処理するツールの場合、作業場所もコロコロ変わると思うので、そのつどこういう必須データを生成するという発想はなかったのですが、ただあらためて考えてみると、校正ルールというのは文書の内容や方針によって変わるほうが自然とも言えて、毎回まったく同じというのも確かに不便なのかもしれません。

ということで、これについてはtextlintrcを以下のように直し、

    "prh": {
      "rulePaths": [
        "./prh.yml"  //<= 「~」を「.」に
      ]
    },

基本的にはつねに作業するディレクトリにprh.ymlを生成・設置することにしました。(生成方法については後出のtx.shをご参照)

追記: 絶対パスにも対応して頂きました

上記の「~/prh.yml と記述してもホームディレクトリだと認識してもらえない」という件に関して、作者の @azu さんが早々に対応してくださいました。

textlint-rule-prhの v3.1.0以降とのこと。

これにより、自分は基本的にprhの辞書はいつも同じでよい、という場合は絶対パスで以下のようにしておいて、

    "prh": {
      "rulePaths": [
        "~/path/to/prh.yml"
      ]
    },

プロジェクトごとに辞書を管理したい場合はこれまで通り、たとえば以下のようにしておけば大丈夫です。

    "prh": {
      "rulePaths": [
        "./prh.yml"
      ]
    },

なお、後出のコード「tx.sh」では、後者の設定を前提としていますのでご留意ください。

@azu さん、ありがとうございました!!

(追記ここまで)

prh を使ってみる(2) | 辞書を選ぶ

さて、そのprhですが、初期状態では以下のような辞書が用意されています。

prh.yml
review-ignore.yml
target.yml
techbooster.yml
WEB+DB_PRESS.yml

といっても、このすべてが同等に作られているということではなくて、とくに実用的なのは以下の二つのようです。

techbooster.yml
WEB+DB_PRESS.yml

前者の「techbooster」というのは、Androidを初めとする技術系のサークル「TechBooster(テックブースター)」の方針に沿って作られた辞書とのこと。

同サークルの名前は電子書籍の分野でもよく聞きますが、@vvakameさんとも関わりが深いようで、prh的にもこの辞書が一押しだそうです。

WEB+DB PRESS」は言わずと知れた技術誌の雄ですが、同誌編集者の稲尾さんが公開された用語統一ルールなどをもとに作られているようです。

複数の辞書をマージして使うこともできるようですが、素直なぼくとしては(そしてYAML力のないぼくとしては)、ひとまず一押しの「techbooster.yml」を使ってみます。

先の作業ディレクトリに prh.yml という名前でコピーして*1、実行。

$ textlint kusamakura.txt | less

結果。

/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt
3:57 ✓ error 出来る => できる prh
5:4 ✓ error 事 => こと prh
5:77 ✓ error 出来て => できて prh
6:293 ✓ error 故に => ゆえに prh
6:498 ✓ error ―― => ── spellcheck-tech-word
7:89 ✓ error ―― => ── spellcheck-tech-word
9:141 ✓ error 一つ => ひとつ prh
(略)

ふむ。いい感じにprh名義のエラー(というか指摘)が出てきましたね。だんだん使い方がわかってきた気がします。

結果をテキストファイルに書き出す

ここまでの作業を一旦ふり返ると、まずチェックしたいファイルと、準拠したいルールを用意して、そのチェックを通した「結果」を受け取る、ということまで出来ました。

しかしながら、この段階ではまだ毎回ターミナル上のlessを目視しては「ふむふむ」と確認しているだけなので、あまり実用的ではないですね。

ということで、次はこの出力されたエラーをテキストファイルに書き出してみようと考えました。

ただ、ちょっと冷静に考えてみると、上のような出力結果を受け取ったところで、結局それをまた元の文書とひとつひとつ突き合わせて、どこがどう間違っているのか確認していくという修行のような作業が待っているわけで、それはそれで大変そうです。

一方、textlintのオプション機能として、出力フォーマットを指定することができて、たとえば以下のようにすると……

$ textlint -f pretty-error kusamakura.txt | less

こんな感じに出てきます。

f:id:note103:20160612025026g:plain

ええと、例が悪くて何がいいんだかわかりづらいですが、行の中のどの辺がエラーなのか、というのが(本当は)見やすくなっています。

じゃあ、このフォーマットでテキストファイルに書き出してみようと、素朴なぼくとしては以下のコマンドを叩くわけですが、

$ textlint -f pretty-error kusamakura.txt > pretty-error.txt

こんな感じで出てきます。

[4m/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt[24m
[90m...[39m
[31m-  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
[39m[32m+  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えができる。
[39m[90m 人の世を作ったものは神でもなければ鬼でもない。やはり向う三軒両隣りょうどなりにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国はあるまい。(略)

おぉぅ……。「[39m[32m」とか「[39m[31m」とかいう文字化け的な何かが出てきました。

ということで、これについてはPerl正規表現を使って整形していくことにしましょう。

追記: [39m[32m などを消す方法

こちらについても、その後 @azu さんから対応方法を教えて頂きました。

なるほど……じつは本記事には書かなかったのですが、この表示についてはけっこう試行錯誤しまして。一生懸命 less の設定をいじったりしていたのですが、まったく直らず諦めたのでした。
textlint コマンドのほうにこういったオプションを付ければ良かったのですね……。

ANSI escape code というものもこの機会に知ることができて大変勉強になりました。
これまたいろいろ検索したのですが、まったく引っかからなかったので……。

上記の方法を併用すれば、以下に出てくるコードもよりシンプルに書けそうですが、記事としてはこのオプションを知らない前提で話を進めていきますので、その旨ご留意ください。

あらためまして @azu さん、ありがとうございました!!

(追記ここまで)

コードを書く

あらためて、この段階で実現したいことを整理すると、

  1. ターミナルでコマンド+ファイル名を打ち込むと、
  2. 通常の結果と、pretty-errorフォーマットで出力された結果がそれぞれテキストファイルに書き出される

という状態を求めています。

そして、それを実現するために書いたシェルスクリプトPerlスクリプトが以下です。

tx.sh
#!/bin/sh

if [ ! -f "$1" ] ; then  # 引数で指定したファイルがカレントディレクトリになければ実行しない
    echo "No $1."
else
    if [ ! -f "prh.yml" ] ; then  # カレントディレクトリにprh.ymlがなければ元の置場からコピーしてくる
        cp /path/to/techbooster.yml ./prh.yml;
    fi

    textlint "$1" > textlint_error.txt  # 通常のtextlintをして結果をtextlint_error.txtに書き出し
    cat textlint_error.txt  # 結果をターミナルにも出力

    textlint -f pretty-error "$1" > textlint_pretty_error.txt  # pretty-errorフォーマットで出力し、一時ファイルに書き出し
    perl /path/to/textlint_pretty_error_tidy.pl textlint_pretty_error.txt > textlint_pretty_error_tidied.txt  # 後出のPerlスクリプトで整形し別ファイルへ書き出し
    rm -f textlint_pretty_error.txt  # 一時ファイルを削除
fi
textlint_pretty_error_tidy.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use File::Slurp;

my $data = read_file($ARGV[0]);  # 一時ファイルを読み込み
my @data = split/\n/, $data;

# 不要な文字列をカット
for (@data) {
    chomp $_;
    $_ =~ s/\\[90m//g;
    $_ =~ s/\\[31m//g;
    $_ =~ s/\\[0m//g;
    $_ =~ s/.*?([^\/]+\.[^\/]+)$/$1/g;  # ファイル名が絶対パスで出てくるのでbasenameのみにする
}

say for @data;

そしてすかさず、上記のシェルスクリプトを「tx」というコマンドで、マシンのどこからでも使えるようにしてみます。

$ chmod a+x tx.sh
$ ln -s /path/to/tx.sh /usr/local/bin/tx

シェルスクリプトでは引数から対象ファイルを受け取るようにしているので、以下のように入力すれば、

$ tx kusamakura.txt

あとはシェルスクリプトPerlと結託してあれこれやった挙句、作業ディレクトリ内に以下のファイルを生成してくれます。

textlint_error.txt
textlint_pretty_error_tidied.txt

Vimから使えるようにする

長くなりましたが、この勢いでもうひと山のぼります。

目当てのファイルを生成するところまで来ましたが、これでも今ひとつ不便です。
というのも、この方法だとtextlintを実行するたびにターミナルに行く必要がありますし、実行後には書き出したファイルをわざわざ開く手間が生じるからです。

では逆に考えろ、ということで、どうなったら満足なのか想像力を働かせてみると、

  1. Vimで文書を編集しているときにコマンド(マッピング)を叩くと、
  2. 上記の2ファイル(エラー結果を書き出したもの)が生成されて、
  3. 元バッファに加えて計3つのバッファが分割画面で表示される。

みたいな状況が良さそうなので、上のシェルスクリプトを活用して以下の関数をvimrcに書いてみました。

function! s:TextLintErrors()
  execute ":! tx %"
  execute ":sp textlint_error.txt"
  execute ":sp textlint_pretty_error_tidied.txt"
endfunction
nnoremap <silent> <Leader>tl :<C-u>call <SID>TextLintErrors()<CR><CR>

実行。

f:id:note103:20160612171920g:plain

できました。

動画だけだとわかりづらいので簡単に説明しますと、

  1. 最初に1画面まるまる「草枕」の状態
  2. 次に実行中の状況が流れていく様子(カクカク動きながら例の文字化けテキストも出てくる)
  3. 最後に画面が3分割された状態(下2つの出力結果をざっとスクロールしながら覗いていく)

という順に進んでいます。

展望とまとめ

ということで、かなり駆け足&欲張り&行き当たりばったりでしたが、自分なりにtextlintに触れた経過を書いてみました。

実質ほぼ1日半ぐらいの間に試したことなので、見落としや根本的な勘違いなどもあるかもしれませんし、実用性の面では、初めのほうで触れたブラウザ拡張などを用いたほうがずっと便利かもしれません。
io-monad.hatenablog.com

それでも、この機会にいろいろと学べたことは多く(とくにほとんど忘れていたシェルスクリプトの書き方とか)、個人的には良い勉強になりました。

今後の目標としては、上で紹介した設定やルールの使い方ではまだ不足が多いので、textlintには他にどんな機能があるのか、そしてそれをどう利用していけるのか、そのチューニングの方法なども含めてもう少し研究してみたいと思っています。

ちなみに、以下を見るとすでに少なからぬルールが揃いつつあるようなので、

この辺の状況も、自分なりに整理・把握していきたいところです。

ざっと触ってみた感触として、textlintは非常にプラガブルな思想にもとづくツールで、使い手が自由にカスタマイズしながら使えることが大きな魅力であると感じました。

こういうツールに対しては、「自然言語ってのはコードの正誤ほど簡単に白黒つけられないんだよな〜」といった否定的な見解が避けがたく出てくるものだと思いますが、そうであるからこそ、「唯一の正解」を設けない、誰もがそれぞれの用途に応じて一から設定していけるこのあり方には共感します。

職業としてこういう道具を必要とする人々(執筆家・編集者・校正者など)に届くまではもうしばらく時間が必要かもしれませんが、様々な専門領域の文書作成者たちが、それぞれのルールを一定の方式に沿って共有空間へ提供し、誰もがそれを使い、また自分なりにカスタマイズし……などという未来がくれば、それは大変喜ばしいことだと思います。

ぼくがその中でどのような役割を果たせるかはわかりませんが、そのような希望を抱いた数日でした。

*1:textlintrcのほうのファイル(辞書)名を書き換えてもいいと思います。

Webページ・ソーシャル文字校正サービスの案

前提

充実したブログ記事を読んでいるときなどに、ちょっとした誤字や「ここ入れ替えたほうが読みやすいんだけどな〜」と思える記述にぶつかることがあります。

いわゆる職業病ということかもしれませんが(編集の仕事をしているので)、これはたんに読んでいる僕がストレスを抱えて終わり(気にしなければOK)という問題というよりは、そこをちょっと直すだけでその記事の寿命も延びるというか、よりいろんな人に伝わりやすくなるだろうという意味で、なんとかスマートな形で教えてあげたいなあ、とそのたび思ったりします。

もちろん、その指摘が執筆者に受け入れられなければそれはそれで良くて、しかしもしそのミスが執筆者の意図しないもので、「直せたら直したい」ものなら、そこのマッチング(指摘したい人とされたい人の)には価値があるだろうと思います。

さて、とはいえそんな些細なミスを、コメント欄やTwitterなどで直接伝えるのもどうなのか、という問題があります。
この場合の一番大きなネックは、いかにおせっかいな人であっても、その目的に比して指摘するまでのコスト(かかる時間・精神的負担など)が高すぎる、ということです。

それなら気軽にはてブで指摘したら? という気もしますが、それはそれでちょっと感じ悪そうというか、そもそもはてブというのは当のWebページ(やブログ)の執筆者とコミュニケーションをとる目的では作られていないので*1、このようなややセンシティブな(人によっては「否定された!」とネガティブに受け取りかねない)やり取りには向いていないとも思えます。

よって、ひとことで言うと、このような欲求に対する解は今のところないですね。

と同時に、それならどんな感じならイイの? ということを書いてみたいと思います。

1. 対象を記録する

Webページの誤植等に気づくわけですから、舞台はブラウザです。
よって、すぐに思い浮かぶ記録方法としてはブックマークレットか、各種ブラウザの拡張機能を使うことが考えられます。

ここでぼくがイメージするのは、たとえばはてなブックマークのそれです。

対象の文字列、あるいは文章を選択し、その状態でボタン(ブックマークレット)をクリックします。あるいは、選択した状態で右クリックをして、その選択肢を選ぶのでもいいと思います。

2. 修正案を書き込む

はてなブックマークGoogle Chrome 拡張だと、ボタンをクリックしたときにコメントを記入する欄が出てきます。
ここでもそのような欄を出して、修正案を書き込めるようにしましょう。

ただし、この段階では「修正案」以外の記述は行いません。
たとえば、本来「リンゴ」と書きたかったのであろう場所が「リゴン」となっていたなら、まず「リゴン」を選択しておいて、修正案の欄には「リンゴ」とだけ書き込みます。

3. 備考を加える

それだけで問題が伝わりそうなら「2」で終わりでも良いですが、ひと言コメントを付け加えたくなることもあるでしょう。
たとえば、「その表現も間違いではないけど、今の業界の標準はこちらだよ」とか。

つまり、修正案を見るだけでは「なぜ」「どこが間違っているのか」が伝わらない可能性もあるので、そういう場合には備考欄で補足します。

よって、「1」で出てくる記入欄には、修正案用と、備考用の2つが必要になります。

4. 閲覧&コミュニケーション場所へ

指摘する側のアクションは「1」〜「3」で完了です。
この後はその内容を使うフェーズに入ります。

これは、はてなブックマークのトップページのようなバリエーションに富んだ感じよりは、Hacker Newsのような超シンプルな構成のほうが良いように感じます。
純粋に直近の投稿から順に、過去へ向かって1カラムで下へ下へ伸びていく感じ。

しかしそれだと、どこに目当てのWebページに関する情報があるかわかりづらいですから、基本的には検索欄を使うようにします。

検索欄に対象となるWebサイトのトップページを打ち込むと、そのサイト内のWebページに関する情報がダララっと出てくるようにします。
で、それをRSSフィードで購読できるようにしましょう。

そうすれば、フィードをフィードリーダーで読んだり、あるいは各種連携サービスを通してSlackやメールなどにプッシュ通知することが可能になります。

5. 問題解消へ

自分のサイト/Webページに関する指摘を見つけたら、それに関する個別ページで指摘内容を吟味したり、コメント欄で掲示板的に意見を交換をできるようにしましょう。

この個別ページは、Stack Overflow みたいな感じがカッコイイかもしれません。

まあ、Vote 機能は不要かもしれませんが、お題となる「現状・修正案・備考」の情報が1セットでページ上部にあり、そこからぶら下がるようにコメントが続いていく、というイメージです。

結論が出たら、当該サイトの管理者はその個別ページをアーカイブに入れることができます。
あ、いやその認証には手間がかかるか……まあ、単に放置しておくだけでもいいかもしれませんが。

まとめ

案の終盤はけっこう雑になりましたが、要点としては「ブックマークレット的なもので最小限の修正要素をクリップし、1つの場所に溜めていける&誰もがそこにアクセスできる」という状況があればいいのになあ、ということでした。

日本語表現のありがちな間違いや、英語の添削など、当事者以外にもそれなりに役立つ場所になるのでは、と夢想しています。

*1:コメントのやり取りができない、文字数が100文字まで、などの制約により。

変数は「箱」か?

ここ数日、業務が落ち着く頃合いを見計らっては週末に行われるPerl入学式の資料に手を入れていました。

資料は校長の @papix さんがざーっと一気に作ってくれて、ぼくがやったのはちょこちょこっとしたリファクタリングというか、文字校正みたいなものでしたが、それだけでもいろいろ勉強になって楽しかったです。
(あいにく、ぼくは当日参加できないのですが)

さて、その資料を作る際にはメンバーの方々とあーでもないこーでもないと様々な議論をしていたのですが、その一つに「変数を『箱』に喩えて説明しているけど、これでいいのか?」という話がありました。

「箱」か「名札」か

プログラミング入門者に「変数」の概念をどう伝えるか、といったときに、そこでどんな喩えを用いるか、というのはなかなか伝統的なイシューだと思われるのですが、ぼく自身の結論から言うと、現代では「箱」よりは「名札」とするほうが妥当かなと思っています。

なぜなら、「箱」にモノを入れた場合、その箱を同時に使えるのは1回きりになるからです。

具体的な例をひとつ考えると、たとえば「ミスター」と書かれた箱に「長嶋茂雄」氏が入っているとします。
言うまでもなく、長嶋さんは世界に一人だけなので、その箱も一つしか作れません。

しかし実際には、変数というのは一度に何回も使えるものなので、

ミスターといえば巨人。ミスターといえば背番号3。ミスターといえばサード。ミスターといえば……

みたいに使いたいわけですが、この「ミスター」を箱として考えてしまうと、2つめ以降の「ミスター」に置ける「箱+長嶋さん」が存在せず、喩えとして破綻します。

言い換えれば、「箱」に喩えても成立する状況というのは、その変数を一回しか呼び出さないときだけです。

ミスターといえば巨人。以上!

一方、もしこれが「名札」であれば、このような破綻は起きません。
名札はいくらでも量産でき、実体(長嶋さん)は一つ存在すれば充分だからです。

よって、どちらが適切か、といえば「名札」の方だと思います。

入門書の現状

しかしながら、身の回りにある入門書を見ると、どちらかと言うと「箱」の例を使うもののほうが多いようです。

ざっと見ただけでも、以下がそうでした。

一方、「名札」を使っているものは以下でした。

これとほぼ同じ意味で、ドットインストールは「ラベル」と言っています。
(特定の言語によらないのでトップページにリンクしておきます)

また、とくに何かに喩えていないものもあります。

その他、ちょっと変わっているというか、さすがと思ったのは深沢千尋さんの以下の2冊。

こちらでは「中学校の数学で出てくるxとかyのようなもの」と説明されています。なるほど。

くだらない話?

ところで、上記の「箱」組の最後に挙げた前橋和弥さんの『センス・オブ・プログラミング!』では、変数を何に喩えるかというそのものズバリの話題があり、以下のように書かれていました。

本書に限らず、変数を「箱のようなもの」と説明している入門書は多いものです。
ただ、これは妥当なたとえではないという人もいます。その主張は、

変数が「箱のようなもの」で、その箱に値を入れることが「代入」であるとするなら、その「箱」からまた別の「箱」に代入を行ったとき、前の「箱」は空になってしまうはずではないか。

というものです。
これは確かに一理あります。
(略)
こう考えると、変数は「箱のようなもの」というより、「メモ用紙のようなもの」とでも考えたほうがよいのかもしれません。
(略)
しかし――変数が「箱」であるのか「メモ用紙」であるのか、そんなくだらない話を真剣に議論してもしょうがないでしょう。たとえ話は所詮たとえ話です。「変数」が実際にどのようなものかを知りたければ、結局、「メモリ」の話を抜きにすることはできないと思います。

(p33-34・太字原文ママ

この結論には、正直、残念な思いがしました。本書は非常に面白く、現時点を含めて数年にわたって少しずつ読み続けているのですが、それでもこのくだりの意見には賛同できません。

なぜなら、ここで「くだらない」と表現されているささいな矛盾に引っかかるのは、著者にイチャモンを付けたいだけの性格の悪い誰かではなく(まあ、そういう人もいるかもしれませんが)、同書が対象にしている、ぼくのような入門者だからです。

そのような入門者を示す良い例として、@uzulla さんがブログに書かれた以下の話があります。

実際に聞かれた事ですが、たとえばfor文は以下のようなものですが、$i、というのがよくわからないといわれました。

for($i=0;$i>10;$i++){
    echo $i;
}

どこの本にも$iとかいてあるので、これは$iでないといけないと勘違いする人とかいます。

「なぜ $i なんだろう?」という疑問は、非入門者にはなかなか湧いてこないものかもしれませんが、入門者はこういう「どうでもいいこと」にバリバリ引っかかります。

「さっきまで $foo だった変数名がなぜ今度は $hoge なんだろう? 何か意味があるのだろうか?」とか思います。

そういう状況が生じてしまうのは、教える側が「そんなことはどうでもいい」と思っているがゆえに、その点をテキトウに説明してしまっているからです。

逆に、もし教える側が「そうそう、こういう細かいところで初心者は引っかかるんだよな〜」ということをわかっていて、またそうした疑問を尊重していれば、変数名を付ける際に foo と hoge を無規則に混在させるようなことはしないでしょうし、上のうずらさんの例で言えば、「ここにある $i というのはとくに意味はありません。$j でも $k でも $abcdefg でもいいです。でも、 i が打ちやすいのでそうしておきます」といった説明を事前にできるかもしれません。
教わる側としても、先にそれを聞いていればわざわざそんな質問をする必要はなくなります。

ここで言いたいことは、そういったことを「くだらない」と思うのは教える側でしかなく、教わる側にはそれがくだらないかどうかすらわからない、ということです。

そして、もし最初から疑問が生じづらい(矛盾の少ない)喩えを用意できていれば、それによって教える側も教わる側も、貴重な時間や労力を節約できるはずだということです。

直感的かどうか

さてしかし、ここまで「名札」推しのぼくでしたが、冒頭で紹介したPerl入学式の最新の講義資料では、それまで採用されていた「箱」の例を直しませんでした。(よって今も「箱」のまま)

その理由は、第一には「面倒だから」で、何が面倒なのかというと、この資料は基本的に複数人の合議のもとに作られているので(資料の作成メンバーは東京だけでなく大阪にもいて)、あまりホイホイ自分勝手に変えられない、ということがあります。

変えられないと言っても、べつにメンバー間の雰囲気が悪くて提案できない、といったことではなく、実際には今回も「どっちかというと箱より名札のほうがいいし、今後のトレンドはそうなっていくと思う」などと偉そうなことを普通に伝えましたし、メンバーもまたそうした意見に耳を傾ける度量を持ってはいるのですが、それでも意思決定にかかる時間が少し負担になりそうだったので、無難に現状維持を選択しました。

逆に言うと、もしこれがぼく個人の文責によるものなら、迷わず「名札」にしたと思います。

そして、直さなかった理由はもう一つあって、それは「箱」のほうが直感的というか、映像的に状況をとらえやすい、と感じるからです。

ぼく自身、入門時期には上に列記したような「箱」に喩えている本を通して変数の雰囲気を把握しましたし(とくに結城浩さんの本ではイラストが明快で直感的だった)、確かにその喩えはある種の人の頭の中ではゆくゆく破綻していきますが、同時にそれは致命的な破綻でもなく、学習に支障をきたすようなものではないとも思います。

言い換えれば、一時的に必要なハシゴとして、とりあえずその時に用が足りる道具として「箱」を使うのはアリかなと思っています。

まとめ

え、じゃあ結局何に喩えればいいと思ってるの? と聞かれたら、もし「箱」とそれ以外の何かから選ぶのであれば「名札(ラベル)」がいいと思います。

しかし一方で、そもそも何かに喩える必要があるのか? という自問も必要だと思います。

たとえば、上に挙げた『かんたんPerl』や、同著者による以下の本では、

変数の説明の最中にいきなりパソコンのメモリ(実物)の写真を登場させて、「この中にはトランジスターがびっしり植えられていて……変数を用意するとそのぶんの領域を確保してくれて……」みたいな大変具体的な話が展開されていたりします。
喩える気ゼロ。という気もしますが、これもまた一つの解だと思います。

変数は「箱」か? という問いに対しては、そうとも言えるし、そうじゃないとも言えます。

しかしいずれにせよ、大切なことは、「それが入門者にとって効果的なのかどうか」を考え続けることだと思います。
「とりあえずこんな風に説明しておけばいい」という標準的な例を洗練させながら、最終的には、その瞬間に目の前にいる「教わる人」に最適な説明を考えていくべきでしょう。

*1:手元にあるのは第3版なので厳密にはこの第5版については不明です。

最近のC言語に関するVimシンタックスチェック状況

以下の記事で紹介したC言語シンタックスチェックの方法について、続報。

note103.hateblo.jp

その段階では、このように記述していたのだけど。

Plug 'rhysd/vim-clang-format'

let g:clang_format#code_style = 'WebKit'
let g:clang_format#style_options = {
            \ "AccessModifierOffset" : -4,
            \ "AllowShortIfStatementsOnASingleLine" : "true",
            \ "AlwaysBreakTemplateDeclarations" : "true",
            \ "Standard" : "C++11",
            \ "BreakBeforeBraces" : "Stroustrup",
            \ }

nnoremap <Leader>ct :ClangFormat<CR>

現在はこのような感じになっている。

Plug 'rhysd/vim-clang-format'

let g:clang_format#code_style = 'LLVM'
let g:clang_format#style_options = {
            \ "AccessModifierOffset" : -4,
            \ "AllowShortIfStatementsOnASingleLine" : "true",
            \ "AlwaysBreakTemplateDeclarations" : "true",
            \ "Standard" : "C++11",
            \ "BreakBeforeBraces" : "Stroustrup",
            \ }

nnoremap <Leader>ct :ClangFormatAutoToggle<CR>

差分。

-let g:clang_format#code_style = 'WebKit'
+let g:clang_format#code_style = 'LLVM'

-nnoremap <Leader>ct :ClangFormat<CR>
+nnoremap <Leader>ct :ClangFormatAutoToggle<CR>

どうやら今参考にしている本の書式は、WebKit の方ではなく LLVM というものが近いみたいだったので、それに変更。

それから、:ClangFormat というのはわざわざ打たなくても保存するたびに勝手に整形してくれるので&逆にそれがけっこう煩わしくなるケースもあって、一時的にチェックを解除できるようにオン・オフのトグルを使えるようにした。

これでまた一つ恒常的な学習を行える環境に近づけた。

VimでMarkdownをカンタンにHTML化する

note103.hateblo.jp

以前に上の記事を書いたのは、もう1年以上前のこと。だから、というわけでもないのだけど、少し方法がアップデートされているので差分を記してみておきます。

まず、以前の方法を簡単にまとめると、

  1. Markdown記法で何か書く。
  2. 自作の md2html.pl で変換。
  3. HTML化された文書の出来上がり。

というもので、シェル上のコマンドで言うと、こんな感じ*1

$ perl md2html.pl in.md > out.html

それがその後、Perl界の先輩である @karupanerura さんから、それだったらこう書けるよ! と教えて頂きまして。

$ cat in.md | Markdown.pl > out.html

つまり、わざわざそういうスクリプトを使わなくても、Perlの Text::Markdown モジュールをインストール済みなら Markdown.pl コマンドを使えるので、それを通せば変換できるよ、と。

それで、その後はありがたく1年ほどその方法にて変換作業をしていたのですが、ふとこれ、Vim のほうにショートカットを仕込めるはずだなあ、と思いまして。

以下のようにマッピングしてみました。

nnoremap <Leader>mh :%! Markdown.pl %<CR>

使っているところ。

f:id:note103:20160605153705g:plain

変換後に、Vimプラグインの previm を使ってブラウザで確認しています。

で、とりあえずこれだけでもOKといえばOKなのですが、僕の性格的にはソースのファイル(バッファ)を上書きしてしまうのは結構抵抗があるので、変換後のHTMLは別のバッファに保存して、元のファイルは 残しておけるように以下のような関数を vimrc に設定してみました。

function! s:Markdown2HTML()
  execute ":%y"
  execute ":new"
  execute ":0put +"
  execute ":w ".strftime('%Y-%m-%d-%H-%M').".html"
  execute ":%! Markdown.pl %"
  execute ":w"
endfunction
nnoremap <Leader>mh :<C-u>call <SID>Markdown2HTML()<CR><CR>

ん〜む……なんだかむっちゃ冗長感溢れていますが、リファクタリングはまた学習の進度に沿ってということで……。
とりあえずやっていることとしては、

":%y" <= バッファ全体をコピー
":new" <= 新規バッファを水平分割で作成
":0put +" <= コピーしておいたテキストを新規バッファへペースト
":w ".strftime('%Y-%m-%d-%H-%M').".html" <= 新規バッファを現在日時分秒を元にしたファイル名で保存(この時はまだMarkdown書式)
":%! Markdown.pl %" <= 保存したバッファでテキストをMarkdownからHTMLへ変換
":w" <= HTMLファイルを保存

という感じです。

動いている様子は、こんな。

f:id:note103:20160605153635g:plain

便利!!

成果

さて上記でサンプル的に使ったMarkdownファイルですが、じつは本日パブリッシュしたこちらの記事でした。

perl-entrance.blog.jp

冒頭に示した記事でも書いたように、Perl入学式では各回のレポートを参加したサポーターさんに書いてもらって、その校正&ブログUPは可能な範囲でぼくが担当したりしています。

この新たに手に入れた Vim & Perl ワザを使って、今後ますます同ブログをカンタンに更新していけそうです。

*1:厳密にはちょっと当時の設定と違うのだけど、実際の構造を把握しやすいように少しアレンジした。

Perlで配列を1行ずつカンタンに出力したい

Perlの小ネタシリーズ。

と言いつつ、文章はそこそこ長いです。

課題

さっそくですが、以下のような配列があるとして、

my @colors = qw/red blue orange green yellow/;

このまま出力すると、

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my @colors = qw/red blue orange green yellow/;

say @colors;

こんな感じになる。

redblueorangegreenyellow

これだとなんだかわからないので、要素間に改行を入れて、以下のように配列の中身を見やすくしたい、ということがよくある。

red
blue
orange
green
yellow

そのための方法として、初心者的に最初に思いつくのは、これとか。

for (@colors) {
    say $_;
}

あるいはこれとか。

say join "\n", @colors;

あるいは最初のやつを後置にするとか。

say $_ for (@colors);

で、これまではこういう場合、一番よく使うのは2番めの join を使う方法だったのだけど、昨日ふと思ったのが「いや、面倒すぎるだろ」ということで。

たしかに一回一回はさほどのタイプ数でもないのだけど、チリも積もればというやつで、これを一生繰り返し続けたら一体何タイプになるのだ、と。

というか、その join 方式にしても地味にダブルクオートやバックスラッシュなども入っていて、案外コストは高いとも言えるし。
今まで見てみぬふりをしてきたけど、これはちょっと改善しないとマズイよなあ……と。

で、普通こういうときというのはすでに誰かが同じことを考えていて、かつすでに解決策を講じてくれていて、さらにはすでにその方法を広く公開・共有してくれていたりするのだけど、寡聞にしてぼくはその辺のことをよく知らない。
皆さん、どうしてるんでしょうね? こういうの。

とりあえずなんとなーく、おぼろげな記憶というか印象として、あれ、こういうモジュールあったような……と思って、List::Util というのを当たってみたのだけど、
List::Util - search.cpan.org

ん〜、機能はむっちゃ充実しているのだけど、そして今あらためて見ながら「うわ、こんな機能あったんだ…… unique とか今度からはこれ使おう」などと思ったのだけど、当の求める機能はなさそう。(たぶん)

ちなみに、同モジュールについては以下に日本語での平易な解説があって、大変参考になりました。

検討

さて、ではあらためて、それならどうすればいいのか。

もしこれがターミナルから使うbashのコマンドとかだったら、これまでに結構似たようなことをやってきているので(ディレクトリ移動やファイルのリネームをするツールなど)、その応用でいけそうな気もするのだけど、今回はちょっと毛色が違うのでどうしたらいいのだか……。

それで、パッと思ったのはとりあえず自分で専用のモジュールを書いて、毎回それを use などで読み込んで使えるようにする、ということかなと。

もちろん、というか特定のモジュール化をしてしまうと、それを使ったコードは汎用性が低くなるというか、他の人とコードの共有をしづらくなってしまう可能性が高まるのだけど、ここでの目的はあくまで「配列を手軽に改行しながら表示したい!」という、個人的かつ一時的なデバッグ的な欲望にもとづくものなので、その限りにおいては「自分しか使わないモジュール」に依拠してしまってもとりあえず構わないかな、と。

ただし、それでも残る懸念としては、今回のモジュールは特定のファイル内でのみ使うものではなくて、コンピュータ内のどこでPerlを書いても使えるようにしたいという前提があるので、たぶん例の @INC 問題でハマる可能性がかなり高そう……

ということで、これまで幾度となく挑んでは敗れてきた @INC 系の情報に関してもあちこち調べて回った末、とりあえず至った結論は以下のようなものでした。
時系列で記してみます。

試行

とりあえずモジュール書く

まず、今回求めている機能は非常にシンプルなもので、こんな感じ。

sub foo {
    my @foo = @_;
    for (@foo) {
        say $_;
    }
}

んで、使うときには、

foo @colors;

とかする。だいぶシンプル!

名前を決める

さてそれで、そのとりあえず「foo」としているところ、つまりモジュール名、および関数名をどうするか……

命名の条件としては、そもそもタイプ数を少なくしたいので数文字で固めたい。
次に、内容をある程度想起させるもので。

今回の場合、配列に改行を挟みたいということなので、キャリッジリターンこと「cr」とか。あるいは「改行」で辞書を見ると「newline」とあるので「nl」とか?

ん〜、しかし「cr」は若干打ちづらいし、「nl」はそれよりは打ちやすいけど、見栄えがパッとしない……ああ、じゃあ「n」とか?

ということで、モジュールはこんな感じに。

n.pm
package n;
use strict;
use warnings;
use feature 'say';
use parent 'exporter';

our @export = qw(n);

sub n {
    my @n = @_;
    for (@n) {
        say $_;
    }
}

1;

はい。モジュール(パッケージ)名も関数名も「n」にしました。

初めてのエクスポート

上のコードで、自分的には初めてやったことがひとつあります。

use parent 'Exporter';
our @EXPORT = qw(n);

この2行を入れることによって、本来なら

n::n @colors;

としなければならないところが

n @colors;

というふうに関数呼び出しのみで済むようになっている。

この辺の情報は、平松さん( id:kaz_hiramatsu )の『雅なPerl入門 第3版』で学びました。(P120-121)
kazhiramatsu.hatenablog.com

Expoter モジュールや @EXPORT はいまだに全然理解できていないのだけど、やりたいことはできつつあるのでひとまずヨシ。

Minilla

では次。上のモジュールを書いただけだと、それを use しても読み込んでくれない&上記のとおり、今回は特定のディレクトリ内だけではなく、コンピュータ上のどこでPerlのコードを書いてもそのモジュールを使えるようにしたいので、たぶん通常のCPANモジュールをインストールするようなことをしなきゃいけない気がする。

しかし、どうやってそのモジュールを「インストール」すればいいのか……知らない。わからない。

ということで、これはけっこう調べる&理解するのに時間がかかったのだけど、最初に試したのは Minilla で。

この Minilla についても、上記の『雅なPerl入門』で詳しく解説されていてとても参考になりましたが、とりあえずこれを使って、

$ minil new n

で新規のレポジトリを作って、そこに上記の n.pm を入れて、同ディレクトリ内で

$ minil install

としたら……使えた!!

のですが、なんか違う……いや、ちゃんと動くのは動いたので、それで解決って言いたい気もするのだけど、第一にMinillaってそういうためのソフトウェアではなさそうだし、第二に(違和感としてはこっちのほうが重要だったのだけど)、今回やりたいことをやるためとしてはちょっと大げさすぎるような。

ということで、ひとまずやりたいことは実現できたものの、別の方法を探してもうしばらく旅を続ける……

環境変数 PERL5LIB を使う

それで訪れた次の試行。この章題は、以下の記事中からいただきました。
qiita.com

いつもお世話になっています @xtetsuji さんのQiita記事ですね。

そしてそれに近いネタとして、ほぼ同時点でたまたま検索して辿りついたのが以下。
perl-users.jp

じつはというか、前者のてつじさんの記事も、後者の記事にインスパイアされて&アップデート的な意味も含めて書かれたもののようで、だからというか相乗効果的にお二方の言わんとすることを理解しやすかったように思います。

それでその、肝心の目的の実現方法についてですが、どうやら任意のモジュールを読み込み可能な状態とする(@INC の中にそのモジュールを追加する)ためには、いくつかの選択肢があって、そのひとつとして、「PERL5LIB」という環境変数に、任意のパスを追加すればいいという。

正直なところ、ぼくはこのパス(PATH/環境変数)というやつもまともに理解できていなくて、しばらく前に読んだ良書『新しいLinuxの教科書』でようやくその一端に触れられた感じはあるのだけど、

新しいLinuxの教科書

新しいLinuxの教科書

とはいえ、まだあんまり腑に落ちてはいない。

いないのだけど、とりあえず bash_profile にこんなふうに記入したうえで、

export PERL5LIB=/path/to/n.pm

(実際には「/path/to/」の部分にはモジュールまでのディレクトリパスを書いている)

source コマンドでリロードすると、

source ~/.bash_profile

なんとモジュールを読み込むようになった。

以下、その模様。

f:id:note103:20160605004050g:plain

キタ!!!

オチ

さて、ここまでやってくる中で、初めはたんに「いちいち改行処理するのがメンドイ!」というだけの怠惰なモチベーションに突き動かされて旅に出てはみたものの、その過程で様々な思いがけない知見を得ることができまして、やはり学習楽しい! プログラミング楽しいな! と思うばかりだったのですが、その途上でひとつ気づいてしまったのが、上述の @xtetsuji さんの記事中にて、以下のような記述がありまして。

Debian wheezy のシステムPerl (最初からシステムに入っていたり、ディストリビューション公式のパッケージとして提供されているPerl) では以下のようなコマンドを打つと、以下のような出力が返ってきます

$ perl -E 'say for @INC'
/etc/perl
/usr/local/lib/perl/5.14.2
/usr/local/share/perl/5.14.2
/usr/lib/perl5
/usr/share/perl5
/usr/lib/perl/5.14
/usr/share/perl/5.14
/usr/local/lib/site_perl
.

ふむふむ……ん? このコード部分の1行目、

$ perl -E 'say for @INC'

これって、もしかすると……

my @colors = qw/red blue orange green yellow/;

say for @colors;  #<= 入れてみた

実行。

red
blue
orange
green
yellow

ああ〜。

for文の頭に、joinどころかスカラー変数すら置かずに「say」するだけでいいんだ〜……へえ〜……。
もしも最初にこれを知っていたら、ぼくたぶんこの試み一切やってなかったですね(笑)。

上のGIF動画を作りながらも思っていましたが、いくら関数名やモジュール名が1文字でラクだと言っても、いちいちファイルごとに use するのはやはり普通に手間だし。

まあ、とはいえ、「自作モジュールをひとまず自分だけで自由に使うにはどうすればいいか」という問題に対しては大きな前進を遂げることができたので、それでヨシとします……!

付録

元々は想定していなかったけど、当然のようにスカラー変数も出力できますね。

my $foo = 'bar';
n $foo;

f:id:note103:20160605093532g:plain

Cool!

って、自分だけが見る前提のプリントデバッグぐらいでしか使えませんが……。

Perlの正規表現で後ろからマッチさせる

わかってみると単純ながら、けっこうハマったのでメモ。

以下のような文字列があるとして、

my $fruits = '/apple/orange/grape/lemon/banana/';

このうち「lemon」の一つ前にある果物だけを取り出したい、とする。

ただし、上記ではその果物が「grape」であり、その前の果物が「orange」であることが明らかだけど、設問上はそこは不明確というか、可変で、さらに言えばその前の「apple」の前にもまだ他の果物がどんどん入ってくるかもしれない、とする。

つまり確実にわかっているのは、「lemonの一つ前にある果物を取り出したい」ということだけ。

となると、「orange」と「lemon」で対象を挟むことはできないし、前から数えていくつめ、と指定することもできない。
よって「lemon」のほう、後ろからマッチさせる方向で考える。

で、最初はこういうのを書いたんだけど、

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my $fruits = '/apple/orange/grape/lemon/banana/';

if ($fruits =~ /\/(.+)\/lemon/) {
    say $1;
}

それだと、結果はこうなる。

apple/orange/grape

いわゆる最長マッチ、欲張りなマッチ状態ですね。

んで、じゃあ最短マッチ(非欲張りマッチ)ってどうやるんだっけ、と思ってこのようにやると、

my $fruits = '/apple/orange/grape/lemon/banana/';

if ($fruits =~ /\/(.+?)\/lemon/) {  #<= 「+」の後に「?」を追加
    say $1;
}

実行。

apple/orange/grape

変わらない。そういう問題ではなかったみたい。

ここまで来て、ん〜あれ、正規表現って後ろからマッチしていくこと自体そもそもできないんだっけ・・?? とかしばらく考え込んでしまったのだけど、結論としてはこんな。

my $fruits = '/apple/orange/grape/lemon/banana/';

if ($fruits =~ /.+\/(.+)\/lemon/) {
    say $1;
}

実行。

grape

はい。
ということで、パターンの頭に「.+」を付けただけでした。「.*」でも構わない。

またひとつ ぱーる の どうぐ を てにいれた! という感じでした。