古来より,ソースコードのインデントは人力で行われていた.エディタごとかつプログラム言語ごとにがんばってインデントのプログラムが書かれている.EmacsにRuby用のインデントのプログラムとかPerl用のインデントのプログラムがあって,Vimにも似たようなのがRuby用とかPerl用とかちまちま用意されてる.Emacsのruby-mode.elだと,カーソルがかっこの中にいたらこれをするとかで,職人っぽい.
人間がこういうのを書かなくても,周りのソースコードを解析したら,普通はこういう場面ではインデントする,というのを機械的にできるだろうと思った.
以下のPerlのコードはべつにインデントしたくないと思う.
print 1; print 2;
以下のPerlのコード見たら,2行目でインデントして,3行目で戻したくなると思う.
if ($i % 15 == 0) { print "FizzBuzz\n"; }
こうしたくなると思う.
if ($i % 15 == 0) { print "FizzBuzz\n"; }
これを機械が判定できるようにしたい.
前行の行末の1文字と,次の行の先頭のスペース以外の1文字の組に対して,インデントが変化したかどうかのデータがあれば,そのデータを使って自動的にインデントできると思った.
上の例のインデントは,以下のテーブルのルールで行われていると言える.;の次にpが来たときは変化なし,{の次にpが来たときは4つ右に行く,;の次に}が来たときは4つ左に行く.
行末の文字 | 行頭の文字 | インデントの変化 |
---|---|---|
; | p | 0 |
{ | p | 4 |
; | } | -4 |
前処理として,コードをたくさん集めて,それらのインデントの雰囲気をファイルに書き出しておき,そのファイルを使って,他のソースコードをインデントするやつを作ってみた.
Plackのソースコードを集めてインデントしてないFizzBuzzをインデントしてみる.
Plackのインデント情報を調べてファイルに書き出す.
% find ~/Plack/lib/ | grep '\.pm$' | xargs ruby learn.rb > examples/learned_plack.txt
こんな雰囲気のファイルができる.
),),-4 ,,},-4 ,,),-4 ,,],-4 ,,f,4 t,w,4 t,},-4
たとえば,),),-4は,前の行の行末が)で次の行の行頭が)なら,次の行は4文字左にインデントを戻すことが多い,という意味.
入力されたソースコードを全行調べて,2文字の組それぞれについて,最頻値のインデントの変化を保存している.
実際には),)で0の行もあっただろうけど,-4が一番多かった,という感じ.
インデントしてないソースコードこんなの.
use strict; use warnings; for my $i (1..30) { if ($i % 15 == 0) { print "FizzBuzz\n"; } elsif ($i % 3 == 0 ) { print "Fizz\n"; } elsif ($i % 5 == 0 ) { print "Buzz\n"; } else { print "$i\n"; } }
Plackのインデント情報を使ってインデントする.
% ruby format.rb examples/learned_plack.txt examples/fizz_buzz.pl
うまくインデントされた.すごい.
use strict; use warnings; for my $i (1..30) { if ($i % 15 == 0) { print "FizzBuzz\n"; } elsif ($i % 3 == 0 ) { print "Fizz\n"; } elsif ($i % 5 == 0 ) { print "Buzz\n"; } else { print "$i\n"; } }
FizzBuzzは素朴だったからうまくいったけど,普通のクラスとかをやると,失敗して,だんだん右にずれていったりして惜しい.本当はunless defined $env && ref($env) eq 'HASH';
の次の行で左に戻ってほしい.
package Plack::Request; use strict; use warnings; use 5.008_001; our $VERSION = '0.9985'; $VERSION = eval $VERSION; use HTTP::Headers; use Carp (); use Hash::MultiValue; use HTTP::Body; use Plack::Request::Upload; use Plack::TempBuffer; use URI; use URI::Escape (); sub _deprecated { my $alt = shift; my $method = (caller(1))[3]; Carp::carp("$method is deprecated. Use '$alt' instead."); } sub new { my($class, $env) = @_; Carp::croak(q{$env is required}) unless defined $env && ref($env) eq 'HASH'; bless { env => $env }, $class; } sub env { $_[0]->{env} } sub address { $_[0]->env->{REMOTE_ADDR} } sub remote_host { $_[0]->env->{REMOTE_HOST} }
Perlではうまくいったけど,Rubyではうまくいかなかった.記号がないとうまくいかなさそう.Rubyはifに記号ないので,行頭,行末にインデントの特徴があまりないと思う.
1.upto(30) { |i| if i % 15 == 0 puts 'FizBuzz' elsif i % 3 == 0 puts 'Fizz' elsif i % 5 == 0 puts 'Buzz' else puts i end }
素朴な感じに作ってみたらPerlのFizzBuzzはうまくインデントできておもしろかった.
けどRubyでは動かないとか,Perlでもどんどん右にずれていくとかで,まだ実用的でないので,もうちょっとなんとかしたい.
前行からの変化なので,間違うと下は全部ずれるので,下に行くほどずれが蓄積される.
インデントのルールを統計的に決められたら,エディタからはそのデータを使えばよいので,エディタからインデントのアルゴリズムを切り離せて便利だと思う.
うまくいったら,コードレビューの際に,コードのスタイルがここだけおかしい,とかもこれで教えてくれるようになる予定.