日別アーカイブ: 2013年12月22日

先日の #Perl入学式 での演習問題「calc_string.pl」の一風変わった解法

おがた (@xtetsuji) です。これを書いている2013年12月22日、まだ入院中です (詳細)。ベッドでブログ書くの、結構腰が疲れます…。

最近では「Perl入学式in東京」のサポーターを常連でやらせてもらっています。

先日の #5 での演習の中に「calc_string」という問題がありました。スライドの内容から引用します。

  • 引数として与えられた文字列が, 数値A 演算子 数値Bという文字列であれば, その値を計算して, 結果を返すような関数calc_stringを書いてみましょう
    • 「数値A」は任意の桁の正・負の整数とします. また, 演算子は+-*/%が使えるものとします.
    • 但し, 引数が与えられなかった場合(空の文字列の場合)は, undefを返します
    • また, 数値A 演算子 数値Bというフォーマットと一致しない場合もundefを返します
  • 関数calc_stringとwhile文を使って, Ctrlキーとdキーを押すまでの間標準入力から文字列を受け取り, 文字列に書かれた式を計算するようなコードを書いてみましょう

これについての回答は、他の生徒さんもブログにアップしたりしていて、その試行錯誤を見て初心に戻ったりしました。

私も生徒さん達が問題に取り組んでいるときに問題をといてみたのですが、マッチさせた演算子文字列で延々と条件節を書かないといけないのであれば、最初から計算式が文字列として組み立てられていることを前提に「文字列eval」したほうが、この場合はパフォーマンスを気にすることもないし簡潔になるかなと思って、Perl入学式の中では教えられなかった s/// の e オプション (eval) を使って解決してみました。しかも結果的に一風変わった形式で。

#!/usr/bin/env perl
# https://github.com/perl-entrance-org/workshop-2013-05/blob/master/slide.md#%E7%B7%B4%E7%BF%92%E5%95%8F%E9%A1%8C-1

use strict;
use warnings;

while(my $str = <STDIN>) {
    chomp $str;
    my $res = calc_strings($str);
    if ( defined $res ) {
        print "$res\n";
    } else {
        print "Input Error: $str\n";
    }
}

sub calc_strings {
    my $str = shift;
    # 文字クラス [...] の中での - は文字コード範囲になるので端っこに置く
    $str =~ s|^(\d+)\s*([-+/*])\s*(\d+)$| "$1 $2 $3" |ee
        or return undef;
    return $str;
}

ここでは Perl の simple replace s/// を使っていますが、e オプションを2回重ねています。こうすることで、置き換え後文字列に二回文字列evalがかかるというPerlの挙動があります。eオプションを重ねれば重ねるほどevalが重複してかかります。最初この挙動はPerlのバグというか意図しない挙動であったのですが、いつしか正式な仕様となりました。

  • s|^(d+)s*([-+/*])s*(d+)$| “$1 $2 $3” |ee

区切り文字を / から | に変更しています。割り算演算子としての文字列 “/” をキャプチャする必要があるのでややこしいからです。あと文字クラス […] 中では、正規表現のメタキャラはその意味を失います(一部の記号、例えばバックスラッシュや “[” などは除く)。またハイフン “-” は文字コードの範囲演算子になるので、文字クラスの列挙の最初か最後に書かないと混乱を招くことに注意しましょう(バックスラッシュでエスケープしてもよいです)。

「数字 演算子 数字」をキャプチャして、最初の置き換え後の文字列eval (e) では、これを文字列連結したPerlの文字列として評価しています。そして二回目の文字列evalで、最初に文字列連結した「計算式の文字列」をさらにPerl自身で評価させて結果を最終的な置き換え文字列としています。

ユーザの任意の文字列を文字列evalすることはセキュリティホールにつながる危険な行為であり、文字列evalはパフォーマンスにも良い影響を与えないことには注意が必要ですが、今回の例では「数字 演算子 数字」の列を正規表現できちんと検査していること、また一人で使うコマンドラインツールなのでパフォーマンス上の問題点は特に無いことで、これも一つのトリッキーな回答になっているかなと思います。

s///ee といった複数回evalの「仕様」は以前から知ってはいたのですが、実際に有用な場面で使ったのが初めてだったので、改めてまとめて解説を書いてみることにしました。