はじめに

先のエントリ公開後間もなく,miyagawaさんより次のようなご指摘をいただきました.

> ただし,冒頭のフォームのように,1つのフィールドに複数の要素が存在する場合,リストリファレンスが得られることに注意です
Plack 0.99 以降ではそのようになりません。Hash::MultiValue オブジェクトになるので、ハッシュとして扱えば最後の要素のみ、複数取得する場合は ->get_all を使用します。
comment by @miyagawa on [plack][perl] Plackにおけるファイルアップロード処理の準備メモ « 岩家ぶろぐ

私の環境に入っていたのは「0.9031」でした.で,現在「DEVELOPER RELEASE」である「0.99_5」のPlack::Requestのドキュメントを見てみたところ,次のような記述がありました.

In version 1.0, many utility methods are removed or deprecated, and most methods are made read-only.

The following methods are deprecated: hostname, url_scheme, params, query_params, body_params, cookie and raw_uri. They will be removed in the next major release.

All parameter-related methods such as parameters, body_parameters, query_parameters and uploads now contains Hash::MultiValue objects, rather than scalar or an array reference depending on the user input which is insecure. See Hash::MultiValue for more about this change.
Plack::Request – search.cpan.org

「deprecated」なものが多々ありますね.これはバージョンを上げておいた方がよさげですね.ということで,これを機に「0.99_5」をインストール.

では,Plack::Request::Uploadあたりの動作確認,と,その前に...そもそも「Hash::MultiValueオブジェクト」というものがよく分かっていませんでした.

ので,まずは,Hash::MultiValueモジュールについてざっと触れるところから始めました,以下はそのメモです.

なお,バージョンアップの頻度が高そうなので,タイトルのとおり,バージョンは「0.07」ということを明記しておきます.(他のモジュールについても,今後,バージョンを明記した方が問題は少ないかもですね.)

Hash::MultiValueオブジェクトの特徴

ざっと触ってのイメージを.

通常のハッシュ(リファレンス)では,複数の同名のキーによって値を代入する場合,最後のものが優先されます.また,keysvaluesでそのキーや値を列挙する場合,その順序が定義のとおりになる保証はありません.

my $h = {a => 1, b => 2};
$h->{a} = 3;
print $h->{a}; # 3

一方Hash::MultiValueオブジェクトは,同名のキーに複数の値が存在することや,値を設定した順序が保持されることが保証(?)されるように実装されています.一部の操作において,ハッシュリファレンスと同じように扱うこともできます.

my $h = Hash::MultiValue->new(
    a => 1,
    b => 2,
    a => 3,
);

この場合,次のようなハッシュリファレンス的なものを扱えるようになる,といったイメージでしょうか.

{
    a => (1, 3),
    b => 2,
};

また,このオブジェクトには,次のような対応と,その順序に関する情報が保持されます.

1. 'a' => 1
2. 'b' => 2
3. 'a' => 3

触ってみる

ドキュメントに記載されているメソッドをひととおり実行してみます.

new

Hash::MultiValueオブジェクトを生成します.

use Hash::MultiValue;
use Try::Tiny;
use Data::Dumper;
 
sub p { print "\n", @_, "\n"; }
sub d { print Dumper @_; }
 
my (@pairs, %h, $h);
@pairs = (
    a => 1,
    b => 2,
    a => 3,
    c => 4,
    d => 5,
    b => 6,
    a => 7,
    e => 8,
);
%h = @pairs;
$h = Hash::MultiValue->new(@pairs);
 
d \@pairs;
d \%h;
d $h;
$VAR1 = [
          'a',
          1,
          'b',
          2,
          'a',
          3,
          'c',
          4,
          'd',
          5,
          'b',
          6,
          'a',
          7,
          'e',
          8
        ];
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => 7,
          'b' => 6,
          'd' => 5
        };
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );

get

指定のキーに対応する値を取得します.対応する値が複数存在する場合は,「最後の」値が得られます.

p '# get';
d $h->get('a');
d $h->{a};
d $h->get('f');
# get
$VAR1 = 7;
$VAR1 = 7;
$VAR1 = undef;

なぜ「最後」かは,次を参照してください.ひとことで言えば,「Perlの仕様」ということでしょうか.

get_one

指定のキーに対応する値を取得します.ただし,対応する値が「単一」の場合に限ります.それ以外,つまり「値が複数存在する」場合や「値が存在しない」場合には,例外が発生します.

This method croaks if there is no value or multiple values associated with the key, so you should wrap it with eval or modules like Try::Tiny.
Hash::MultiValue – search.cpan.org

ということなので,use Try::Tinyなどしてtry {} catch {}して利用するとよさげなようです.

p '# get_one';
for my $k (qw/b d f/) {
    try{
        d $h->get_one($k);
    } catch {
        d $_;
    };
}
# get_one
$VAR1 = 'Multiple values match: b at ./sample.pl line 40
';
$VAR1 = 5;
$VAR1 = 'Key not found: f at ./sample.pl line 40
';

get_all

指定したキーに対応するすべての値を,リストとして取得します.

p '# get_all';
d [$h->get_all('a')];
d [$h->get_all('f')];
# get_all
$VAR1 = [
          1,
          3,
          7
        ];
$VAR1 = [];

keys

キーを列挙します.定義時やaddメソッド(後述)による追加の順序も保持されています.

p '# keys';
d [$h->keys];
d [keys %$h];
# keys
$VAR1 = [
          'a',
          'b',
          'a',
          'c',
          'd',
          'b',
          'a',
          'e'
        ];
$VAR1 = [
          'e',
          'c',
          'a',
          'b',
          'd'
        ];

values

値を列挙します.定義時やaddメソッド(後述)による追加の順序も保持されています.

p '# values';
d [$h->values];
d [values %$h];
# values
$VAR1 = [
          1,
          2,
          3,
          4,
          5,
          6,
          7,
          8
        ];
$VAR1 = [
          8,
          4,
          7,
          6,
          5
        ];

add

キーと値のペアを追加します.値は複数まとめて追加できます.

p '# add';
d $h->add('f', 10, 20);
d [$h->get_all('f')];
# add
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5,
                 'f' => 20
               }, 'Hash::MultiValue' );
$VAR1 = [
          10,
          20
        ];

remove

指定したキーとそれに対応する値すべてを削除します.

p '# remove';
d $h->remove('f');
d [$h->get_all('f')];
# remove
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = [];

clear

内容を空にします.オブジェクト自体は消えません.

p '# clear';
d $h->clear;
d [$h->keys];
# clear
$VAR1 = bless( {}, 'Hash::MultiValue' );
$VAR1 = [];

flatten

「キー」「値」「キー」「値」...と,保持された順序で列挙したリストが得られます.

p '# flatten';
d [$h->flatten];
# flatten
$VAR1 = [
          'a',
          1,
          'b',
          2,
          'a',
          3,
          'c',
          4,
          'd',
          5,
          'b',
          6,
          'a',
          7,
          'e',
          8
        ];

each

ペアそれぞれについて,引数として定義されるサブルーチンを実行します.サブルーチン内では,第1引数,第2引数が,実行の対象となるペアのキー,値を,それぞれ参照します.

p '# each';
$h->each(sub { d "$_[0] => $_[1]"; });
while (my ($k, $v) = each %$h) { d "$k ==> $v"; }
# each
$VAR1 = 'a => 1';
$VAR1 = 'b => 2';
$VAR1 = 'a => 3';
$VAR1 = 'c => 4';
$VAR1 = 'd => 5';
$VAR1 = 'b => 6';
$VAR1 = 'a => 7';
$VAR1 = 'e => 8';
$VAR1 = 'e ==> 8';
$VAR1 = 'c ==> 4';
$VAR1 = 'a ==> 7';
$VAR1 = 'b ==> 6';
$VAR1 = 'd ==> 5';

clone

Hash::MultiValueオブジェクトを複製します.参照先(?)もまったく別のものとなります.

p '# clone';
my ($hh1, $hh2, $hh3) = (
    $h,
    $h->clone,
    %$h,
);
d $hh1;
d $hh2;
d $hh3;
d ["$h", "$hh1", "$hh2", "$hh3"];
# clone
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = 'e';
$VAR1 = [
          'Hash::MultiValue=HASH(0x81b634)',
          'Hash::MultiValue=HASH(0x81b634)',
          'Hash::MultiValue=HASH(0x844960)',
          'e'
        ];

as_hashref

オブジェクトの持つ内容を,単なるハッシュリファレンスに変換したものが得られます.もちろん,「同じキーの重複」もなく「キー・値のペアの順序」も保証されません.

p '# as_hashref';
d $h->as_hashref;
# as_hashref
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => 7,
          'b' => 6,
          'd' => 5
        };

as_hashref_mixed,mixed

自身の内容を,値にスカラとリストリファレンスが混在したハッシュリファレンスへ変換したものが得られます.文章がなんだか変です.

p '# as_hasref_mixed, mixed';
d $h->as_hashref_mixed;
d $h->mixed;
# as_hasref_mixed, mixed
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => 5
        };
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => 5
        };

as_hashref_multi,multi

自身の内容を,すべての値がリストリファレンスとなるハッシュリファレンスへと変換したものが得られます.

p '# as_hashref_multi, multi';
d $h->as_hashref_multi;
d $h->multi;
# as_hashref_multi, multi
$VAR1 = {
          'e' => [
                 8
               ],
          'c' => [
                 4
               ],
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => [
                 5
               ]
        };
$VAR1 = {
          'e' => [
                 8
               ],
          'c' => [
                 4
               ],
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => [
                 5
               ]
        };

from_mixed

クラスメソッドとして使います.「mixed」なハッシュリファレンスを引数として与えると,そのデータを内容としたHash::MultiValueオブジェクトが得られます.as_hashref_mixedメソッドのちょうど反対の操作です.

p '# from_mixed';
my $mixed = {
    o => 1,
    p => [3, 4, 5],
    q => [7],
    r => 9,
};
$h = Hash::MultiValue->from_mixed($mixed);
d $mixed;
d $h;
d [$h->flatten];
# from_mixed
$VAR1 = {
          'p' => [
                 3,
                 4,
                 5
               ],
          'r' => 9,
          'q' => [
                 7
               ],
          'o' => 1
        };
$VAR1 = bless( {
                 'p' => 5,
                 'r' => 9,
                 'q' => 7,
                 'o' => 1
               }, 'Hash::MultiValue' );
$VAR1 = [
          'p',
          3,
          'p',
          4,
          'p',
          5,
          'r',
          9,
          'q',
          7,
          'o',
          1
        ];

「順序のあるハッシュ」という位置づけ

Hash::MultiValueって,「順序のあるハッシュ」としても使えるのかな.
Twitter / IWATA, Susumu : Hash::MultiValueって,「順序のあるハ …

@issm つかえます>Hash::MultiValue
Twitter / Tatsuhiko Miyagawa: @issm つかえます>Hash::MultiValue

値の重複はさせず,「ペアの順序を保持する」という特徴だけを利用すれば,「順序のある」ハッシュが実現できてしまうわけですね.

DBIモジュールのfetchall_hashrefメソッドあたりで何か楽しいことができないでしょうかね.

参考

おわりに

以上,Hash::MultiValueモジュール(バージョン0.07)におけるメソッドを,ざっとひととおり試してみました.

これまでも,スカラまたはリストリファレンスのいずれかが各キーの値となるようなハッシュリファレンスの処理ってコードが冗長になりがちだったため,このモジュールはいろいろな場面でオモシロク使えるんじゃないかな,と思います.これから慣れてみます.

Tie::Hash::MultiValueなるモジュールが存在していたようですが,無知でした><)

...今確認したところ,もう0.08が出てますね.

サンプルコードをまとめて

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
 
use Hash::MultiValue;
use Try::Tiny;
use Data::Dumper;
 
sub p { print "\n", @_, "\n"; }
sub d { print Dumper @_; }
 
 
my (@pairs, %h, $h);
@pairs = (
    a => 1,
    b => 2,
    a => 3,
    c => 4,
    d => 5,
    b => 6,
    a => 7,
    e => 8,
);
%h = @pairs;
$h = Hash::MultiValue->new(@pairs);
 
d \@pairs;
d \%h;
d $h;
 
p '# get';
d $h->get('a');
d $h->{a};
d $h->get('f');
 
p '# get_one';
for my $k (qw/b d f/) {
    try{
        d $h->get_one($k);
    } catch {
        d $_;
    };
}
 
p '# get_all';
d [$h->get_all('a')];
d [$h->get_all('f')];
 
p '# keys';
d [$h->keys];
d [keys %$h];
 
p '# values';
d [$h->values];
d [values %$h];
 
p '# add';
d $h->add('f', 10, 20);
d [$h->get_all('f')];
 
p '# remove';
d $h->remove('f');
d [$h->get_all('f')];
 
p '# clear';
d $h->clear;
d [$h->keys];
 
$h = Hash::MultiValue->new(@pairs);
 
p '# flatten';
d [$h->flatten];
 
p '# each';
$h->each(sub { d "$_[0] => $_[1]"; });
while (my ($k, $v) = each %$h) { d "$k ==> $v"; }
 
p '# clone';
my ($hh1, $hh2, $hh3) = (
    $h,
    $h->clone,
    %$h,
);
d $hh1;
d $hh2;
d $hh3;
d ["$h", "$hh1", "$hh2", "$hh3"];
 
p '# as_hashref';
d $h->as_hashref;
 
p '# as_hasref_mixed, mixed';
d $h->as_hashref_mixed;
d $h->mixed;
 
p '# as_hashref_multi, multi';
d $h->as_hashref_multi;
d $h->multi;
 
p '# from_mixed';
my $mixed = {
    o => 1,
    p => [3, 4, 5],
    q => [7],
    r => 9,
};
$h = Hash::MultiValue->from_mixed($mixed);
d $mixed;
d $h;
d [$h->flatten];
 
__END__

実行結果をまとめて

% sample.pl
$VAR1 = [
          'a',
          1,
          'b',
          2,
          'a',
          3,
          'c',
          4,
          'd',
          5,
          'b',
          6,
          'a',
          7,
          'e',
          8
        ];
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => 7,
          'b' => 6,
          'd' => 5
        };
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
 
# get
$VAR1 = 7;
$VAR1 = 7;
$VAR1 = undef;
 
# get_one
$VAR1 = 'Multiple values match: b at ./sample.pl line 40
';
$VAR1 = 5;
$VAR1 = 'Key not found: f at ./sample.pl line 40
';
 
# get_all
$VAR1 = [
          1,
          3,
          7
        ];
$VAR1 = [];
 
# keys
$VAR1 = [
          'a',
          'b',
          'a',
          'c',
          'd',
          'b',
          'a',
          'e'
        ];
$VAR1 = [
          'e',
          'c',
          'a',
          'b',
          'd'
        ];
 
# values
$VAR1 = [
          1,
          2,
          3,
          4,
          5,
          6,
          7,
          8
        ];
$VAR1 = [
          8,
          4,
          7,
          6,
          5
        ];
 
# add
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5,
                 'f' => 20
               }, 'Hash::MultiValue' );
$VAR1 = [
          10,
          20
        ];
 
# remove
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = [];
 
# clear
$VAR1 = bless( {}, 'Hash::MultiValue' );
$VAR1 = [];
 
# flatten
$VAR1 = [
          'a',
          1,
          'b',
          2,
          'a',
          3,
          'c',
          4,
          'd',
          5,
          'b',
          6,
          'a',
          7,
          'e',
          8
        ];
 
# each
$VAR1 = 'a => 1';
$VAR1 = 'b => 2';
$VAR1 = 'a => 3';
$VAR1 = 'c => 4';
$VAR1 = 'd => 5';
$VAR1 = 'b => 6';
$VAR1 = 'a => 7';
$VAR1 = 'e => 8';
$VAR1 = 'e ==> 8';
$VAR1 = 'c ==> 4';
$VAR1 = 'a ==> 7';
$VAR1 = 'b ==> 6';
$VAR1 = 'd ==> 5';
 
# clone
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = bless( {
                 'e' => 8,
                 'c' => 4,
                 'a' => 7,
                 'b' => 6,
                 'd' => 5
               }, 'Hash::MultiValue' );
$VAR1 = 'e';
$VAR1 = [
          'Hash::MultiValue=HASH(0x81b634)',
          'Hash::MultiValue=HASH(0x81b634)',
          'Hash::MultiValue=HASH(0x844960)',
          'e'
        ];
 
# as_hashref
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => 7,
          'b' => 6,
          'd' => 5
        };
 
# as_hasref_mixed, mixed
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => 5
        };
$VAR1 = {
          'e' => 8,
          'c' => 4,
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => 5
        };
 
# as_hashref_multi, multi
$VAR1 = {
          'e' => [
                 8
               ],
          'c' => [
                 4
               ],
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => [
                 5
               ]
        };
$VAR1 = {
          'e' => [
                 8
               ],
          'c' => [
                 4
               ],
          'a' => [
                 1,
                 3,
                 7
               ],
          'b' => [
                 2,
                 6
               ],
          'd' => [
                 5
               ]
        };
 
# from_mixed
$VAR1 = {
          'p' => [
                 3,
                 4,
                 5
               ],
          'r' => 9,
          'q' => [
                 7
               ],
          'o' => 1
        };
$VAR1 = bless( {
                 'p' => 5,
                 'r' => 9,
                 'q' => 7,
                 'o' => 1
               }, 'Hash::MultiValue' );
$VAR1 = [
          'p',
          3,
          'p',
          4,
          'p',
          5,
          'r',
          9,
          'q',
          7,
          'o',
          1
        ];
%

こちらもあわせてどうぞ

Leave a Reply

直近のつぶやきを読み込みちゅう...