🛠

コードに30分、テストに3日。でもそれがプログラミング。

2020/10/06に公開

uswというPerlモジュールの、納得のいくバージョンをリリースしましたのでその経験を記事にしたいと思います。
この記事がtechカテゴリでは無いとはすぐに分かったのですが、「アイデア」なのか?これ。ポエムじゃないのか?という気がしますが公開します。

どういうモジュールかは記事の主旨に無関係なのですが、面白いからいろいろ畳んでおきます。

ソースコード

40行と非常に短いソースです

package usw;
use 5.012005;

our $VERSION = "0.08";

use Encode qw(is_utf8 encode_utf8 decode_utf8);
use utf8();
use strict();
use warnings();
use List::Util qw(first);

sub import {
    utf8->import;
    strict->import;
    warnings->import( 'all', FATAL => 'recursion' );

    my $encoding = $^O ne "MSWin32"    # is UNIX-like OS
        ? 'UTF-8'
        : eval { require Win32; return "cp" . Win32::GetConsoleCP() };    # is Windows
    die "install 'Win32' module before use it\n" if $@;

    $| = 1;                                                               # is this irrelevant?
    binmode \*STDIN,  ":encoding($encoding)";
    binmode \*STDOUT, ":encoding($encoding)";
    binmode \*STDERR, ":encoding($encoding)";

    $SIG{__WARN__} = \&_redecode;
    $SIG{__DIE__}  = sub { die _redecode(@_) };
    return;
}

sub _redecode {
    $_[0] =~ /^(.+) at (.+) line (\d+)\.$/;
    my @texts = split $2, $_[0];
    return is_utf8($1)
        ? $texts[0] . decode_utf8 $2. $texts[1]
        : decode_utf8 $_[0];
}

1;

技術的にすごいことは特に何もしていません。何もしていないのに、どうしても3日もかかったという話をしようと思います。

使い方
use usw;

終わり。

え?っと思った方はこちらをどうぞ。これだけで下記のコードと等価なのです。

windows以外では

 use utf8;
 use strict;
 use warnings;
 binmode \*STDIN,  ':encoding(UTF-8)';
 binmode \*STDOUT, ':encoding(UTF-8)';
 binmode \*STDERR, ':encoding(UTF-8)';

windows(例えば日本語版)では

 use utf8;
 use strict;
 use warnings;
 binmode \*STDIN,  ':encoding(cp932)';
 binmode \*STDOUT, ':encoding(cp932)';
 binmode \*STDERR, ':encoding(cp932)';
何をしているのか?

Perlにはプラグマと言って、ソースコードに関わる宣言[1]があります。
このモジュールはPerlで最頻出の三つのプラグマ[2]を1行にまとめ、その副作用[3]をコントロールするものです。

要するに、楽ができるということですね。

なんで作ったか?

無かったから。

前置きが長くなりましたが、次章からが本題です。


着想は一瞬

きっかけは木本さんのツイートです。

と、それに対する私の反応。

リンク先のモジュールは、私にはなぜか使いづらくて[4]使ってませんでした。

で、自分で使いやすいと思うのはこれだなぁ、という段階に入ります。とりあえずスマホのメモ帳にアイディアを記録しました。

実装はほぼコピペ

それで数日のうちに、実際に調べ始めちゃったんですよ。
まずは同様のことができる既製の実装がないのかmetacpanで探しました。
そしてMouseのソースから断片が分かって、意外と簡単にやってるな、ということを突き止めます。

じゃあこれ貼ってみようかー。と、ローカルの適当なファイルにソースをコピーして実行しました。
あれ?これ?思った通りに動くぞ?じゃあこれは?これは?

という感じで初期の構想の要件を満たしてしまいました。多分30分かかってないですね。
この時点でモジュールにしてリリースしてみようかなぁ、と思うようになりローカルにリポジトリを作成してテストを書き始めました。

テスト&デバッグ

ここからが3日です。でも、あっという間の3日でしたね。

動かすのは簡単。何故動くかを調べる

普段は自分使いのコードは結構、動けばそれが正解みたいな空気で開発してます。
どっかのタイミングでデバッグとテストをするので、ある程度機能がまとまるまでは細かく直しても効率が悪いというか、これです。

Done is better than perfect - Mark E. Zuckerberg

完璧主義はよくないな、動かない100のアイディアより動く1つのコードだよな、と思って自分を突き動かしています。

でもモジュールの場合、動くだけじゃダメなんですよね。
使ってもらえるかどうかは知りませんけど、利用者に不便や不利益をもたらすような仕様になっていないか?抜けや漏れがあれば無いようにと、穴を塞ぐように真逆の発想をします。

今回はお手本のコードのコピーが手元にある状態なので、まずは仕様を調べるところから着手しました。[5]
幸いなことに、Perlではドキュメントがソースの中に埋まってますので、一つのサイトを巡回するだけでググらずに用が済みます。

正しく失敗することが大事

動く理由が分かったら、失敗するテストを対で書きます。そうすることで仕様に対する理解が深まりますし、誤作動の可能性を自然と排除できるからです。また、その作業が当該のテストの正しさを証明する担保になります。
どの条件で意図した挙動になり、またはどの条件で失敗するのか、これをコードに落としてゆくと自然とコードは短くなります。
テストも初めは冗長ですが、全体を書き終えた後には、これは最適化できるな、余計だな、読みづらいな、というのが見えてきます。

テストの順番も大事

これは数学の定理を少しづつ証明する作業に似ていて、既に正しいと証明された挙動のみを使ってテストを組み、証明されていないなら基礎を証明をする、という作業になります。
CIなどに指定した順序でテストを実施させるためにテストのファイル名を何度もリネームしました。

今気づいたんですけど、(昔のBASICのように)末尾に0をつけて数え上げに使う数字の桁を増やせばもう少し楽できる?

How many codes must the tests well run?

こうして、30分で書いた40行に対し、合計19個のファイルで63個のテストを実装してようやく一つのモジュールが完成しました。ちなみにこれは最適化後に計測した数字であって、実際に何行書いて消したかはもちろん数えていませんが、延べでは見えている量の5倍くらいは書いてると思います。何故なら、以下のような経緯があるからです。

環境依存のテストを書かないのではなく、環境依存のテストを可能な限り用意する

構想当初は、自分の使うUNIX-like OSの上でだけ動けば良いと思ってましたが、やっぱりWindowsは無視できませんでした。
そこで私の取った行動がWindows用に実装をforkしてテストを別途に書くことです。この時点でテストの量が(一時的に)2倍になりました。
なお、Windowsは持ってないのでappveyorでテスト用の仮想環境を用意しました。

環境に依存しないコードに置き換える

ひと通りテストを終え、リリースをし、コードとテストをあらためて眺めていてふと閃きます。

「これ最初から一つのモジュールでよくない?」

実行環境を検出し、Windowsで挙動が違う部分のみテストを分岐できれば十分であることに気がついたのです。テストをもう一度見直し、再整理しました。

まとめ

これに3日もかけてしまったのは私の技術的知識の不足によるところが大きいのですが、かける価値のある作業であったと自信を持って言えます。
私たちはコードというわかりやすい成果物に目を奪われがちですが、テストの重要性を過小評価しては[6]いけないですよね。

終わってから爽やかに思いました。嗚呼、これがプログラミングだぞ、と。

一緒に言いたいことを書いておきましょうか。

おい、どうなってんだ?Perl

Perl使いだけ読めばいい内容です。

実装の過程で教えてもらったのですが、
非ascii(≒ユニコード)文字をパスに含むファイルでのwarndie[7][8]がエラーメッセージを自動でencodeしないのにも関わらず、(パスを含む)ファイル名だけは何故か先にencodeされている、という謎の仕様(バグでしょ?)[9]が存在しています。

これはもっと知名度があって良い気がするし、早々に直されるべき。これがなければこのモジュールはもっと短くできます

脚注
  1. Cの#include <stdio.h>みたいなもの ↩︎

  2. この記事ではそれぞれについて解説しません ↩︎

  3. この記事では副作用について解説しません ↩︎

  4. 今なら言語化できます。ソースにuse utf8;するのに、得られる文字列は完全にencodeされていて逆に一回decodeする必要があるのでした。 ↩︎

  5. Perlの複雑な言語仕様についての説明は勘弁してください ↩︎

  6. 去年のペイとか今年のコウザとかの事例を見る限り ↩︎

  7. いずれも出力先は標準エラー出力であるSTDERR ↩︎

  8. 初めて正しい脚注の使い方した気がする ↩︎

  9. githubでtravis使って公開中 ↩︎

Discussion