読者です 読者をやめる 読者になる 読者になる

CodeIQ Blog

自分の実力を知りたいITエンジニア向けの、実務スキル評価サービス「CodeIQ(コードアイキュー)」の公式ブログです。

エンジニア夏祭り2013「はてなインターンの課題を解いてみよう」Perl解答編 #engineer #matsuri #hatena #perl #夏祭り #夏の想い出

CodeIQ×はてな エンジニア夏祭り2013

CodeIQの中の人、OL元帥です。

「はてな」さんとCodeIQがタッグを組んだ『CodeIQ×はてな エンジニア夏祭り2013』
第1夜「はてなからの挑戦状」
f:id:codeiq:20130725181204j:plain

はてなサマーインターン2013事務局さんから届きました、
Perlの問題の解答・解説を公開します!


f:id:codeiq:20130809120819g:plain



◇◇◇◇◇◇◇◇◇◇


課題の実装のポイントとして、以下の4点を上げます。

  1. Logクラスの実装
  2. method, path, protocol メソッドの実装: req の値を解釈して必要な値を取り出す
  3. uri メソッドの実装: 他フィールドの値を参照して必要な値を組み立てる
  4. time メソッドの実装: 日付操作を行う(Perlモジュールを利用する)

まず Log クラスを実装し、2, 3, 4のメソッドを順に実装していきます。

クラスの実装

perl のオブジェクト指向について詳しくは 続・はじめてのPerl や Hatena-Textbook などを参照してください。

  • 本課題では、Logクラスのひな形が既に用意されているので、それをそのまま使ってもらって問題ないです。
package Log;
use strict;
use warnings;

sub new {
    my ($class, %args) = @_;
    return bless \%args, $class;
}

sub protocol {
}

...

1;
  • 注意点
  • Perlのクラス(=パッケージ) は読み込み時に評価され、真値を返さなければいけないという決まりがあります。最後の行の 1; はそのための記述で、一見無意味に見えますが省略することはできません。
  • Perl では言語としてコンストラクタ機構がサポートされている訳ではなく、オブジェクトを生成するメソッドの名前は new である必要はありません。便宜上この解説では new メソッドをコンストラクタと呼びます。
method, path, protocol: req の値を解釈して必要な値を取り出す

req は [method] [path] [protocol] が空白文字区切りで並んでいるので、空白文字を区切り文字として req を分割し必要な値を取得します。

寄せられた回答でも実装が分かれていたポイントとして、reqフィールドをパースするタイミングがあります。これについては以下の二つのパターンが一般的なのではないかと思います。

  • コンストラクタ内でパースする
  • req をパースした値が最初に必要となったときにパースする

どちらが良いというわけではなくケースバイケースだと思いますが、パースした値が必ず使われる訳ではないときにコンストラクタ内でパースすると無駄な計算をしてしまうことになります。(実際にはこのくらいの処理であれば(よほど反復しない限り)パフォーマンスに影響はありませんが、より負荷の高い処理を行う場合に重要になってきます)

今回は必要になったときにパースする方針で実装していきます。
Perl で文字列の分割には split 関数を使います。

  • perl の標準関数の使い方を調べるには、perldoc -f split などとすると良いでしょう
sub _req {
    my ($self) = @_;
    return $self->{_req_struct} //= do {
        my $req = $self->{req};
        my ($method, $path, $protocol) = split ' ', $req;
        +{
            method   => $method,
            path     => $path,
            protocol => $protocol,
        };
    };
}

sub method {
    my ($self) = @_;
    return $self->_req->{method};
}

sub protocol {
...

値が必要になったときにパースするコードの例です。

Perlの関数には仮引数はなく、引数は特殊変数 @_ に格納されて受け渡されます。また、メソッド呼び出し時には @_ の先頭要素にオブジェクト自身(いわゆるthis)が格納されています。

  • オブジェクト自身を指す this のような参照は Perl にはありません。

呼び出すたびにパースしていては無駄なので、一度パースした結果はプライベートなフィールドに取って置き、二回目以降呼び出されたときはその結果を返すようにします。この例では _req_struct というフィールドに結果を格納しています。

  • $self->{_hoge} //= do { ... }; というのはよく使われるイディオムで、_hoge フィールドが定義されていればそれを返し、そうでない場合はdoブロックを実行してその結果を _hoge フィールドに格納する(そしてそれを返す)というコードです。これによって最初の一回はdoブロックを実行し、その後は一度計算した結果を用いることができます。
  • method, path, protocol の各メソッドは内部で _req を呼び出すようにします。どれかが呼び出された時点で req がパースされてキャッシュされるので、三つとも呼び出した場合でもパースする回数は一回だけで済みます。
uri: 他フィールドの値を参照して必要な値を組み立てる

これもいつ組み立てるかはケースバイケースですが、今回は上と同じように呼び出し時に組み立てることにします。

sub uri {
    my ($self) = @_;
    return $self->{_uri} //= do {
        my ($prtcl) = split '/', $self->protocol;
        lc($prtcl) . '://' . $self->host . '/' . $self->path
    };
}

先ほどのreqをパースするコードと似ていますね。doブロックの中だけが異なっています。
doブロックの中では、自身の protocol, host, path メソッドを呼び出してuriを組み立てています。lc は大文字を小文字に変換する関数です。

  • 上述の通り、protocolとpathメソッドを呼び出していてもreqがパースされる回数は一度だけになります。
time: 日付操作を行う

ここでは日付操作を行います。今回の問題は DateTime モジュールを使えば簡単にこなせるようになっていますので、DateTime モジュールを利用して実装していきます。

エポック秒からDateTimeオブジェクトを生成するには、from_epochメソッドを利用します。

  • Perl モジュールの使い方は、コンソールで 'perldoc モジュール名' と打つとリファレンンスを引くことができます。または http://www.cpan.org/ から検索しても良いでしょう。
use DateTime;

sub time {
    my ($self) = @_;
    my $date = DateTime->from_epoch(epoch => $self->{epoch})
    return $slef->{_time} //= "$date";
}

ここでも毎回変換するのは無駄なので、 _time フィールドに結果をキャッシュします。

さて、from_epoch メソッドで生成した DateTime オブジェクトを、YYYY-MM-DDThh:mm:ss のフォーマットの文字列に変換するにはどうすればいいでしょうか。
DateTime オブジェクトには年・月・日などをそれぞれ取得するメソッドがあるので、それらを使って組み立ててもよいのですが、実はこのフォーマット、DateTimeオブジェクトを文字列コンテキストで評価した場合に得られるフォーマットそのものです。なので$dateを文字列コンテキストで評価してあげれば、それで完成になります。
文字列コンテキスト、あまり聞き慣れない言葉ですね。Perlはコンテキストにより値の評価され方が変わります。代表的な文字列コンテキストは、文字列結合演算子による演算と、変数の文字列内での展開です。上では文字列内での変数展開を利用しています。

完成
  • 以上で今回の課題は完成です。完成したコードを下に載せておきます。
package Log;
use DateTime;

sub new {
    my ($class, %args) = @_;
    return bless \%args, __PACKAGE__;
}

sub _req {
    my ($self) = @_;
    return $self->{_req_struct} //= do {
        my $req = $self->{req};
        my ($method, $path, $protocol) = split ' ', $req;
        +{
            method   => $method,
            path     => $path,
            protocol => $protocol,
        };
    };
}

sub protocol {
    my ($self) = @_;
    return $self->_req->{protocol};
}

sub method {
    my ($self) = @_;
    return $self->_req->{method};
}

sub path {
    my ($self) = @_;
    return $self->_req->{path};
}

sub uri {
    my ($self) = @_;
    return $self->{_uri} //= do {
        my ($prtcl) = split '/', $self->protocol;
        lc($prtcl) . '://' . $self->host . '/' . $self->path;
    };
}

sub time {
    my ($self) = @_;
    return $self->{_time} //= DateTime->from_epoch(epoch => $self->{epoch}).q();
}

sub status {
    my ($self) = @_;
    return $self->{status};
}

...

1;

ついでに課題で指定していないフィールドにもアクセサなど実装したりするとより便利そうですね!
この課題は今年のはてなインターン生にインターン事前課題として解いてもらったものの一部です。
もし興味があれば、続きも解いてみて下さい。続きは hatena/Hatena-Intern-Exercise2013 · GitHub にあります。



◇◇◇◇◇◇◇◇◇◇



いかがでしたか?
企業のインターン課題を体験できるのは面白いですね!続きも気になります~。

さて、次回「tagomoris」さんの解答・解説の公開は、8月19日(月)の予定です!少々お待ち下さいね♪



CodeIQ × はてな エンジニア夏祭り19113 第2夜 ブログでわっしょい開催中!
f:id:codeiq:20130806104304g:plain
「納涼!ほんとにあった怖いコード」「CodeIQの問題・パズルを考えよう!」のいずれかのお題でブログを書くと、豪華審査員が選評してくれます。
詳しくはキャンペーンページヘ!



エンジニアのための新しい転職活動!CodeIQのウチに来ない?の特集ページを見る