overload - Perl の演算子のオーバーロードを行うパッケージ
package SomeThing;
use overload
'+' => \&myadd,
'-' => \&mysub;
# etc
...
package main;
$a = new SomeThing 57;
$b=5+$a;
...
if (overload::Overloaded $b) {...}
...
$strval = overload::StrVal $b;
(オーバーロード関数の宣言)
コンパイル指示子
package Number;
use overload
"+" => \&add,
"*=" => "muas";
では、加法の関数 Number::add() と「クラス」Number (あるいは、
その基底クラスの 1 つ) の中の乗法の代入形式 *= のメソッド muas() を
宣言しています。
この指示子の引数は (key, value) のペアです。
この value としては、&{ ... } の中で使用できるものが
すべてを指定できますから、サブルーチン名、
サブルーチンへのリファレンス、無名のサブルーチンといったものが
すべて使えます。
文字列として指定された値はサブルーチンではなく、
メソッドとして解釈されることに注意してください。
key として有効な値は以下に述べます。
$a+$b を実行するときに、$a がパッケージ Number 内に
bless されたオブジェクトへのリファレンスである場合か、
$a がそのようなマスマジカルな加法を用意しているパッケージの
オブジェクトでなくても、$b が Number へのリファレンスである場合に、
サブルーチン add が呼び出されます。
これは、$a+=7 とか $a++ といった、シチュエーションでも呼ばれます。
MAGIC AUTOGENERATION の節を参照してください。
(「マスマジカル」という言葉は、オーバーロードされた
算術演算子によって起動されるメソッドを指しています。)
オーバーロードは @ISA 階層による継承を遵守するので、上述の宣言は
Number から継承される全てのパッケージの + と *= のオーバーロードも
引き起こすことになります。
(二項演算子の呼び出し規約)
use overload ... 指示子で指定される関数は、
3 つ (唯一特別な場合があって、
その時は 4 つ (Last Resortの節を参照) ) の引数で呼び出されます。
対応する演算子が、二項演算子であれば、最初の 2 つの引数は、
その演算子の 2 つの引数です。
しかしながら、通常のオブジェクトメソッドの呼び出し規約によって、
最初の引数は、常にそのパッケージのオブジェクトでなければなりませんので、
7+$a のような場合には、引数の順序の入れ替えが行なわれます。
これは、加法のメソッドを実装する時には、
おそらく問題にはならないものですが、減法のメソッドにとっては、
引数を入替えるか否かは、非常に重大な問題です。
メソッド側では、この引数の入れ替えについての情報を 3 つめの引数を
調べることで、確かめることができます。
この引数は、3 種類の値をとります:
(偽)
引数の順序は、現在の演算子でのものと同じです。
(真)
引数は、逆になっています。
undef
現在の演算子は、($a+=7 のような) 代入形式のものですが、
代わりに普通の関数が呼ばれます。
この付加的な情報は、何らかの最適化を行なうときに
使用できます。
Calling Conventions for Mutators と比較してください。
(単項演算子の呼び出し規約)
単項演算子は、2 番目の引数が undef の二項演算子であると
考えられます。
つまり、{"++"} をオーバーロードする関数は、
$a++ が実行されるときに、($a, undef, '') という引数で呼び出されます。
(ミューテータの呼び出し規約)
2 種類のミューテータは異なった呼び出し規約を持ちます:
++ and --
(++ と --)
これらの演算子を実装するルーチンは、実際はこれらの引数を 変更(mutate) することを想定しています。 それで、$obj が数値へのリファレンスと仮定すると、
sub incr { my $n = $ {$_[0]}; ++$n; $_[0] = bless \$n}
はオーバーロードした ++ の適切な実装です。以下のものは
sub incr { ++$ {$_[0]} ; shift }
事前インクリメントと事後インクリメントの場合は OK であることに 注意してください。 事後インクリメントの場合、コピーが実行されます。 Copy Constructor を参照してください。)
x= and other assignment versions
(x= とその他の代入演算子)
これらのメソッドについて何も特別なものはありません。 引数の値を変更するかもしれませんし、そのままかもしれません。 結果は、もしこの値と違うなら左側の値に代入されることになります。
これは、オーバーロードした += と + として同じメソッドを使うことを
許します。
これは 許されます が、推奨されません; なぜなら "Fallback" の
意味論によって Perl はもし += がオーバーロードされていなければ
とにかく + を呼び出すからです。
警告: 演算の代入版の存在によって、代入コンテキストで呼び出される
ルーチンは自己参照構造を作るかもしれません。
現在のところ、Perl は循環が 明示的に 破壊されるまで自己参照構造を
解放しません。
構造をたどっていくときにも問題になるかもしれません。
以下のようにすると、
use overload '+' => sub { bless [ \$_[0], \$_[1] ] };
は自ら災いを招くことになります; なぜなら $obj += $foo というコードでは
サブルーチンは $obj = add($obj, $foo, undef) あるいは $obj = [\$obj,
\$foo] として呼び出されるからです。
もしこのようなサブルーチンを使うことが重要な最適化なら、
非「最適化」版で明示的に += をオーバーロードするか、
もし not defined $_[2] なら非最適化版に切り替えることができます
(Calling Conventions for Binary Operations を参照してください)。
たとえスクリプトで 明示的に 代入系の演算子を使っていなくても、
オプティマイザによって生成されるかもしれません。
例えば、",$obj," や ',' . $obj . ',' は両方とも以下のように
最適化されるかもしれません
my $tmp = ',' . $obj; $tmp .= ',';
(オーバーロードできる操作)
以下のシンボルが use overload 指示子で指定できます:
(算術演算子)
"+", "+=", "-", "-=", "*", "*=", "/", "/=", "%", "%=",
"**", "**=", "<<", "<<=", ">>", ">>=", "x", "x=", ".", ".=",
これらの演算子について、代入形式のものが存在しないときには、代わりに
非代入形式のものが呼ばれます。
演算子 "+", "-", "+=", "-=" に対するメソッドは、
インクリメント演算子やデクリメント演算子を自動生成するために
呼ばれることがあります。
演算子 "-" は、単項のマイナスや abs のメソッドがないときに
自動生成するために使われます。
これらの置換に関する詳細については "MAGIC AUTOGENERATION", "Calling Conventions for Mutators", "Calling Conventions for Binary Operations" を参照して下さい。
(比較演算子)
"<", "<=", ">", ">=", "==", "!=", "<=>",
"lt", "le", "gt", "ge", "eq", "ne", "cmp",
ある演算子が無い場合にも、対応する「スペースシップ」形式が使えるならば、
代わりに使うことができます。
配列のソートのときには、use overload のもとの cmp を使って値を
比較します。
(ビット演算子)
"&", "^", "|", "neg", "!", "~",
"neg" は、単項のマイナスを表わします。
neg のメソッドが指定されていないときには、
引き算のメソッドを使って、自動生成されます。
"!" のメソッドが指定されていないときには、
"bool", "\"\"", "0+" のいずれかのメソッドを使って
自動生成されます。
(インクリメントとデクリメント)
"++", "--",
未定義であれば、足し算と引き算のメソッドが代わりに使われます。 これらの演算子は、前置としても後置としても使われます。
(超越関数)
"atan2", "cos", "sin", "exp", "abs", "log", "sqrt",
abs がないときには、"<" か "<=<>" のメソッドを、
単項のマイナスか引き算のメソッドと組み合わせて、
自動生成されます。
(ブール変換、文字列変換、数値変換)
"bool", "\"\"", "0+",
これらの中でオーバーロードしていないものがあっても、残りが一つで
も定義してあれば、それを代わりに使うことができます。
bool は、(while のような) フロー制御演算子や、
三項演算子 "?:" で使われます。
これらの関数は、任意の Perl 値を返すことができます。
この値に対応する演算子もオーバーロードされている場合には、
その演算子がその時の値を使って、再度呼び出されることになります。
特別な場合として、オーバーロードがオブジェクト自身を返した場合、
これが直接使われます。
オブジェクトを返すオーバーロードされた変換はおそらくバグです; なぜなら
おそらく YourPackage=HASH(0x8172b34) のようなものを受け取るからです。
(反復子)
"<>"
オーバーロードされないと、引数はファイルハンドルかグロブに変換されます
(文字列化が必要かもしれません)。
同じオーバーロードは ファイルハンドル読み込み 構文 <$var> と
グロブ 構文 <${var}> の両方でも起こります。
(デリファレンス)
'${}', '@{}', '%{}', '&{}', '*{}'.
オーバーロードされないと、引数は そのままで デリファレンスされ、 正しい型になるべきです。 これらの関数は正しい型のリファレンスか、オーバーロードされた デリファレンスと共に他のオブジェクトが返されるべきです。
特別な場合として、オーバーロードがオブジェクト自身を返した場合、 (正しい型なら)これが直接使われます。
デリファレンス演算子は "nomethod" を渡されないように明示的に 指定されなければなりません。
(特殊 key)
"nomethod", "fallback", "=",
SPECIAL SYMBOLS FOR use overload を参照してください。
存在しないメソッドの自動生成についての説明は "Fallback" を参照して下さい。
上述のテーブルのコンピュータ可読形式は、ハッシュ %overload::ops で 利用可能です; 値は空白で区切られた名前のリストです:
with_assign => '+ - * / % ** << >> x .',
assign => '+= -= *= /= %= **= <<= >>= x= .=',
num_comparison => '< <= > >= == !=',
'3way_comparison'=> '<=> cmp',
str_comparison => 'lt le gt ge eq ne',
binary => '& | ^',
unary => 'neg ! ~',
mutators => '++ --',
func => 'atan2 cos sin exp abs log sqrt',
conversion => 'bool "" 0+',
iterators => '<>',
dereferencing => '${} @{} %{} &{} *{}',
special => 'nomethod fallback ='
(継承とオーバーロード)
継承はオーバーロードに二つの方法で関わります。
use overload directive
(use overload 指示子の値としての文字列)
もし以下での value が
use overload key => value;
文字列の場合、メソッド名として解釈されます。
(操作のオーバーロードは派生クラスによって継承される)
オーバーロードされたクラスから派生したクラスもオーバーロードされます。 オーバーロードされたメソッドの集合は全ての祖先のオーバーロードされた メソッドの結合です。 もしあるメソッドが複数の祖先によってオーバーロードされているなら、 どれが使われるかは通常の継承ルールによって決定されます:
もし A が B を C を (この順序で) 継承していて、B が + を
\&D::plus_sub でオーバーロードし、C が + を "plus_meth" で
オーバーロードしている場合、パッケージ A でオブジェクトのために
演算 + を実装するために、サブルーチン D::plus_sub が呼び出されます。
fallback キーの値はサブルーチンではないので、この継承は上述のルールには
従いません。
現在の実装では、最初にオーバーロードした祖先の fallback の値が
使われますが、これは偶然で、変更される予定です。
use overload(use overload の特殊シンボル)
ここまでに説明してきたものの他に、3 つの key が Perl に認識されます。
(最後の手段)
"nomethod" は、4 つのパラメータを持つ関数へのリファレンスが引き続きます。
これが定義されていれば、オーバーロードの仕組みで、
何らかの演算子に対するメソッドを見つけることができなかったときに、
呼び出されます。
この関数の最初の 3 つの引数は、本来、
呼ばれるはずだったメソッドに対する引数と一致し、4 番目の引数は、
見つからなかったメソッドに対応するシンボルとなります。
いくつかのメソッドが試されている場合には、最後のものが使われます。
たとえば、1-$a であれば、
"nomethod" => "nomethodMethod" の組が use overload 指示子で
指定されていれば:
&nomethodMethod($a,1,1,"-")
と同様です。
"nomethod" の機構は デリファレンス演算子 ( ${} @{} %{} &{} *{} ) では
使用されません。
何らかの演算子が見つからず、"nomethod" に結び付けられた関数もない
場合には、("fallback" が use overload 指示子のキーとして
指定されていない限り) die() による例外が発生します。
(フォールバック)
"fallback" は、特定の演算子に対するメソッドが見つからない場合の
動作を規定します。
"fallback" の value によって、3 つの場合があります:
undef
Perl は、代替のメソッドを使うことを試みます
(MAGIC AUTOGENERATION の節を参照してください)。
それもダメならば、"nomethod" を呼び出そうとします。
これも無い場合には、例外が発生することになります。
(真)
undef の場合と同じですが、例外を発生させません。
この場合、黙って、もし use overload がなかったときに、
行なってであろう動作に戻されることになります。
(定義済みだが「偽」)
マジック自動生成は行ないません。
Perl は、まず "nomethod" の実行を試みて、
これがなければ、例外を発生させます。
注意: @ISA 経由の "fallback" 継承はまだ行われません。
"Inheritance and overloading" を参照して下さい。
(コピーコンストラクタ)
"=" の値は、3 引数の関数へのリファレンスです。
つまり、use overload の他の値と似ているように見えます。
しかし、これは Perl の代入演算子をオーバーロードしません。
これは「ラクダの毛(Camel hair)」に対抗しています。
この演算は、以下のような、他のリファレンスとオブジェクトを共有する リファレンスに対して、ミューテータを使うときに呼び出されます。
$a=$b; ++$a;
これを、$a を変更し、$b を変更しないようにするために、$$a のコピーを作り、
この新しいオブジェクトへのリファレンスが $a に代入されます。
この操作は、++$a の実行中に (すなわち、その前に
$$a が $$b に一致します)、行われます。
これは++ が '++' か '+=' (か nomethod) のメソッドを通じて
表現されているときにだけ行なわれます。
この演算子が、非ミューテータ "+" を使って記述されている場合、
$a=$b; $a=$a+1;
$a は $$a の新しいコピーのリファレンスではありません。
上記のコードが実行されたときに $$a は左辺値としては現れていないからです。
コピーコンストラクタが、いくつかのミューテータの実行中に必要となって、
'=' が指定されていないときには、そのオブジェクトが
単なるスカラであれば、文字列コピーとして自動生成されます。
(例)
以下の記述で
$a=$b;
Something else which does not modify $a or $b....
++$a;
実際に実行されるコードは以下のようになります
$a=$b;
Something else which does not modify $a or $b....
$a = $a->clone(undef,"");
$a->incr(undef,"");
(もし $b がマスマジカルで、'++' が \&incr でオーバーロードされていて、
'=' が \&clone でオーバーロードされていれば。)
同じ振る舞いは $b = $a++ で引き起こされます; これは
$b = $a; ++$a と同義として扱われます。
(マジック自動生成)
演算子に対するメソッドが見つからず、"fallback" が
「真」か「未定義」であれば、Perl は、定義されている演算子を
もとに、見つからなかった演算子の代わりのメソッドを自動生成しようと試みます。
以下の演算子に対して、自動生成代替メソッドが行なえます:
(算術演算子の代入形式)
"+=" メソッドが定義されていないとき、
$a+=$b は、"+" メソッドを使うことができます。
(変換演算子)
文字列、数値、ブール値変換は、すべてが定義されてはいないとき、 互いに別のもので計算されます。
(インクリメントとデクリメント)
演算 ++$a は、$a+=1 か $a+1 で、演算 $a-- は、
$a-=1 か $a-1 で表現することができます。
abs($a)
abs($a) は、$a<0 と -$a (または 0-$a) で表現できます。
(単項のマイナス)
単項のマイナスは、引き算を使って表現できます。
(否定)
! と not はブール値変換、文字列変換、数値変換を使って
表現できます。
(連結)
連結は、文字列変換を使って表現できます。
(比較演算子)
比較演算は、それぞれに対応する「スペースシップ」演算
(<=<> か cmp) を用いて表現することができます:
<, >, <=, >=, ==, != in terms of <=>
lt, gt, le, ge, eq, ne in terms of cmp
(反復子)
<> in terms of builtin operations
(デリファレンス)
${} @{} %{} &{} *{} in terms of builtin operations
(コピー演算子)
コピー演算は被参照した値が、リファレンスではないスカラであれば、 その値への代入という形で表現できます。
(オーバーロードが失われるとき)
比較演算子に対する制限は、たとえば、`cmp' が bless された
リファレンスを返さなければならないとしても、自動生成された関数
`lt' は、`cmp' の結果の数値に基づく標準の論理値だけを
作り出します。
特に、この場合には、(ときには別の変換で表わされた)
数値変換が使えないといけません。
同様に、.= 演算子や x= 演算子も、文字列変換による代替が起これば、
マスマジカルな性質がなくなります。
マスマジカルなオブジェクトを chop() すると、文字列になり、 マスマジカルな性質はなくなります。 同じことは、他の演算でも起こります。
(実行時オーバーロード)
全ての use 指示子はコンパイル時に実行されるので、実行時にオーバーロードを
変更する唯一の方法は以下のものです
eval 'use overload "+" => \&addmethod';
また、以下のようにもできます
eval 'no overload "+", "--", "<="';
しかし実行時にこのような構文を使うのは問題が多いです。
(パブリック関数)
パッケージ overload.pm は以下のパブリック関数を提供します:
文字列化のオーバーロードがないものとしたときの arg の文字列値を
与えます。
arg が何らかの操作のオーバーロードの影響下にあるなら真を返します。
op を実装しているメソッドへのリファレンス、あるいは undef を
返します。
(定数のオーバーロード)
アプリケーションによっては Perl パーザが定数をいじりすぎる場合があります。 この処理を overload::constant() 関数と overload::remove_constant() 関数を 使ってフックできます。
これらの関数は引数としてハッシュを取ります。 ハッシュのキーとして認識されるのは以下のものです。
整数定数をオーバーロードします。
浮動小数点数定数をオーバーロードします。
8 進および 16 進定数をオーバーロードします。
q-クォートされた文字列、qq- および qx-クォートされた文字列の
定数片、ヒヤドキュメントをオーバーロードします。
正規表現の定数片をオーバーロードします。
対応する値は 3 つの引数を取る関数へのリファレンスへのリファレンスです:
最初のものは定数の 初期 文字列形式、2 番目は Perl がこの定数を
どのように解釈するか、3 番目は定数をどのように使うかです。
初期文字列形式は文字列デリミタを含んでおらず、バックスラッシュ-デリミタの
組み合わせでのバックスラッシュは削除されている(従ってデリミタの値は
この文字列を処理するには適切ではない)ことに注意してください。
この関数の返り値はこの定数が Perl によってどのように解釈されるかです。
3 番目の引数は、オーバーロードされた q- か qr- 定数以外では未定義値、
(文字列、正規表現、シングルクォートヒヤドキュメントによる)
シングルクォートコンテキストの場合は q、tr/y 演算子の引数の
場合は tr、s 演算子の右側の場合は s、その他では qq になります。
式 "ab$cd,," は単に 'ab' . $cd . ',,' のショートカットなので、
オーバーロード定数文字列は妥当なオーバーロードされた結合演算子を
装備していると想定していて、さもなければ不合理な結果となります。
同様に、負数は正数の否定として扱われます。
import() と unimport() メソッド以外から overload::constant() や overload::remove_constant() を呼び出すのはおそらく無意味であることに 注意してください。 これらのメソッドからは、以下のようにして呼び出されます
sub import {
shift;
return unless @_;
die "unknown import: @_" unless @_ == 1 and $_[0] eq ':constant';
overload::constant integer => sub {Math::BigInt->new(shift)};
}
バグ 現在のところ、定数のオーバーロード性は eval '...' に伝播しません。
(実装)
以下はすぐに変更される可能性があります。
すべての演算のためのメソッドのテーブルは、該当パッケージの
シンボルテーブルに対するマジックとしてキャッシュされます。
このキャッシュは use overload, no overload, 新しい関数定義、
@ISA の変更のいずれかの処理の間に無効化されます。
しかし、この無効化はパッケージに対する次の bless までは
実行されずに残されます。
つまり、オーバーロード構造を動的に変更したいならば、テーブルを
更新するために、(意味の無い) bless を行なう必要があります。
(すべての SV 風のものは、マジックキューを持っており、マジックが キューのエントリになっています。 これによって、1 つの変数が、同時に複数のマジックの形式に 関ることができるのです。 たとえば、環境変数は普段、%ENV マジックと「汚染」マジックの 2 つの形式を一度に持っています。 しかし、オーバーロードを実装しているマジックは隠してあるものに 適用され、これはめったに直接使うことはないため、 Perl の速度を低下させないはずです。)
オブジェクトがオーバーロードを使うパッケージに属するならば、 そのオブジェクトには、特別なフラグが用意されます。 つまり、オーバーロードされていない算術演算を行なうときの、 スピードに対する影響は、このフラグのチェックのみです。
実際、use overload が存在しなければ、オーバーロード可能な演算に
対するオーバヘッドはほとんど無く、ほとんどのプログラムで、
認識できるようなパフォーマスの低下はないはずです。
あるパッケージでオーバーロードが使われても、
対象の引数がオーバーロードを使ったパッケージに属していない場合には、
オーバヘッドの最小限にする最大限の努力が為されました。
疑わしいときには、use overload がある場合と無い場合で、
スピードのテストをしてください。 これまでのところ、Perl が
最適化を指定してコンパイル場合には、顕著なスピードの低下の報告は
あがっていません。
オーバーロードが使われないときには、データの大きさには影響しません。
あるパッケージでオーバーロードを使うときの唯一のサイズペナルティは、
全てのパッケージが次のパッケージへの bless 時に
マジックを求めることです。
このマジックはオーバーロードを使わないパッケージの場合は 3 ワード長で、
オーバーロードを使うパッケージの場合はキャッシュテーブルを運びます。
$a=$b のようなコピーは、表層的なものです。
しかし、$a++ のように、$b (または、$a) が参照するオブジェクトへの
代入を意味する演算の前に、1 層深度のコピーが行なわれます。
この動作は、
自分でコピーコンストラクタを定義することによって変更することが
できます ("Copy Constructor"の項を参照してください)。
明示的にサポートされていないメソッドに対する引数は、 定数であることが期待されます (が、強制はされません)。
(比喩の衝突)
なぜオーバーロードされた = の意味論がこんなに直感的でないかを
不思議に思う人がいるかもしれません。
もし直感的でない ように見える なら、比喩の衝突に影響されています。
Perl のオブジェクトの比喩はこれです:
オブジェクトは bless されたデータへのリファレンス
そして算術の比喩はこれです:
オブジェクトはそれ自体が一つのもの。
= をオーバーロードする 主な 問題は、$a と $b がオブジェクトのとき、
$a = $b という代入はこれらの比喩からは異なった動作を暗示するという
事実です。
Perl 的な考え方は、$a は $b がリファレンスしている何かへのリファレンスに
なることを暗示します。
算術的な考え方は、$a と $b が別々の実体であることは維持したまま、
「オブジェクト」$a の値は、オブジェクト $b の値になることを暗示します。
ミューテータがないなら、これは違いはありません。
Perl 式の代入の後、$a でリファレンスされているデータを変更する演算は
$b でリファレンスされているデータも変更します。
事実上、$a = $b の後では $a と $b の値は 区別ができない ものに
なります。
一方、算術記法を使っている人は誰でも、算術の比喩の表現力を知っています。 オーバーロードはこの比喩を有効にするのは難しい一方、Perl 的な方法を 出来るだけ保存した形で動作します。 2 つの矛盾する比喩を自由に混ぜることは不可能なので、 オーバーロードは 全てのミューテータがオーバーロードされた アクセス経由でのみ呼び出される限りにおいて 算術的な方法で 書くことができます。 これを行う方法は Copy Constructor に記述されています。
あるミューテータメソッドがオーバーロードされた値に直接適用される場合、 同じ値をリファレンスしているほかの値と 明示的にリンクを切る 必要が あるかもしれません:
$a = new Data 23;
...
$b = $a; # $b is "linked" to $a
...
$a = $a->clone; # Unlink $b from $a
$a->increment_by(4);
オーバーロードされたアクセスはこれを透過的にすることに注意してください:
$a = new Data 23;
$b = $a; # $b is "linked" to $a
$a += 4; # would unlink $b automagically
しかし、以下のようにしても、
$a = new Data 23;
$a = 4; # Now $a is a plain 4, not 'Data'
$a の「オブジェクト性」は保存されません。 しかし、Perl にはオブジェクトへの代入を望み通りのやり方にする方法が あります。 これは単にオーバーロードではなく、tie() するインターフェースです (perlfunc/tie を参照してください)。 オブジェクト自身を返す FETCH() メソッドと、オブジェクトの値を変更する STORE() メソッドを追加することで、少なくとも最初から tie() されている 変数に対しては、完全性に対して算術の比喩を再現できます。
(このバグの回避策が必要かもしれないことに注意してください; "BUGS" を 参照してください。)
(レシピ集)
どうかこれに引き続く例を追加してください!
(2 面スカラ)
これを two_face.pm として Perl ライブラリディレクトリに置きます:
package two_face; # Scalars with separate string and
# numeric values.
sub new { my $p = shift; bless [@_], $p }
use overload '""' => \&str, '0+' => \&num, fallback => 1;
sub num {shift->[1]}
sub str {shift->[0]}
以下のようにして使います:
require two_face;
my $seven = new two_face ("vii", 7);
printf "seven=$seven, seven=%d, eight=%d\n", $seven, $seven+1;
print "seven contains `i'\n" if $seven =~ /i/;
(2 行目は文字列値と数値の両方を持つスカラを作ります。) これは以下を出力します:
seven=vii, seven=7, eight=8 seven contains `i'
(2 面リファレンス)
Perl 組み込みの 擬似ハッシュ 型のような、 配列リファレンスとハッシュリファレンスの両方としてアクセス可能な オブジェクトを作りたいとします。 添え字 0 を普通の要素として扱える擬似ハッシュよりもよいものを作りましょう。
package two_refs;
use overload '%{}' => \&gethash, '@{}' => sub { $ {shift()} };
sub new {
my $p = shift;
bless \ [@_], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key] = shift;
}
sub FETCH {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key];
}
これで配列とハッシュの両方の文法を使ってオブジェクトにアクセスできます:
my $bar = new two_refs 3,4,5,6;
$bar->[2] = 11;
$bar->{two} == 11 or die 'bad hash fetch';
この例のいくつかの重要な機能に注意してください。
まず、$bar の 実際の 型はスカラリファレンスで、スカラデリファレンスは
オーバーロードしていません。
従って、単に(関数の中でオーバーロードデリファレンスをしている方法である)
$$bar とすることで、実際の $bar のオーバーロードされていない中身を
得ることができます。
同様に、TIEHASH() メソッドで返されるオブジェクトはスカラリファレンスです。
2 番目に、ハッシュ文法が使われるたびに新しい tie されたハッシュを作ります。 これにより、メモリリークを引き起こすことになるリファレンスループの 可能性について心配しなくても良くなります。
これらの問題の両方は修正できます。 例えば、ハッシュ自身として 実装されている オブジェクトへの リファレンスに対するハッシュのデリファレンスをオーバーロードしたい場合、 回避する必要がある唯一の問題は、(オーバーロードされた デリファレンス演算子によって提供される 仮想 ハッシュではなく) この 実際の ハッシュにどうやってアクセスするかです。 これは可能なフェッチルーチンの一つです:
sub access_hash {
my ($self, $key) = (shift, shift);
my $class = ref $self;
bless $self, 'overload::dummy'; # Disable overloading of %{}
my $out = $self->{$key};
bless $self, $class; # Restore overloading
$out;
}
アクセス毎に tie されたハッシュの生成をしないようにするには、 リファレンスの非円形の構造を許すための追加のレベルの間接化を行います:
package two_refs1;
use overload '%{}' => sub { ${shift()}->[1] },
'@{}' => sub { ${shift()}->[0] };
sub new {
my $p = shift;
my $a = [@_];
my %h;
tie %h, $p, $a;
bless \ [$a, \%h], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key] = shift;
}
sub FETCH {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key];
}
ここでもし $bar がこのようにオーバーロードされると、$baz は
実際の配列とアクセスハッシュへのリファレンスを保持している中間の配列への
リファレンスへのリファレンスです。
アクセスハッシュへの tie() したオブジェクトは 実際の配列への
リファレンスへのリファレンスなので、
リファレンスのループはありません。
two_refs1 クラスに bless された両方の「オブジェクト」は配列への
リファレンスへのリファレンスなので、スカラ へのリファレンスです。
従って、アクセサ式 $$foo->[$ind] はオーバーロードされた演算を
伴いません。
(シンボリック計算機)
この symbolic.pm をあなたの Perl ライブラリディレクトリに入れてください:
package symbolic; # Primitive symbolic calculator use overload nomethod => \&wrap;
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
このモジュールは、オーバーロードするモジュールとしてはかなり変わっています:
通常のオーバーロード演算子は何も提供せず、その代わりに
Last Resort 演算子 nomethod を提供します。
この例で、対応するサブルーチンはオブジェクトに対して行われた演算を
カプセル化したオブジェクトを返します:
new symbolic 3 は ['n', 3] を含み、2 + new symbolic 3 は
['+', 2, ['n', 3]] を含みます。
以下は、上述のパッケージを使って外接 8 角形の辺を「計算する」スクリプトの 例です:
require symbolic; my $iter = 1; # 2**($iter+2) = 8 my $side = new symbolic 1; my $cnt = $iter;
while ($cnt--) {
$side = (sqrt(1 + $side**2) - 1)/$side;
}
print "OK\n";
$side の値は
['/', ['-', ['sqrt', ['+', 1, ['**', ['n', 1], 2]], undef], 1], ['n', 1]]
素晴らしい小さいスクリプトを使ってこの値を得ることは出来ましたが、
この値を 使う 単純な方法はないことに注意してください。
実際この値はデバッガ (perldebug を参照してください) で検査できますが、
bareStringify オプションがセットされていて、p コマンド
経由でないときのみです。
もしこの値を表示しようとすると、オーバーロードされた演算子
"" が呼び出され、これは nomethod 演算子を呼び出します。
この演算子の結果として再び文字列化が行われますが、この結果は再び
symbolic 型なので、無限ループを引き起こします。
symbolic.pm モジュールに整形表示メソッドを追加します:
sub pretty {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
$a = $a->pretty if ref $a;
$b = $b->pretty if ref $b;
"[$meth $a $b]";
}
これでスクリプトの末尾に以下のように書けます
print "side = ", $side->pretty, "\n";
メソッド pretty はオブジェクト-文字列変換を行うので、
このメソッドを使って演算子 "" をオーバーロードするのが自然です。
しかし、このようなメソッドの内部では
オブジェクトの 要素 である $a や $b を整形表示する必要はありません。
上述のサブルーチンで、"[$meth $a $b]" は、なんらかの文字列と要素 $a および
$b の連結です。
もしこれらの要素がオーバーロードを使っていると、連結演算子は
オーバーロードされた演算子 . を探します; もし存在しなければ、
オーバーロードされた演算子 "" を探します。
従って以下を使えば十分です
use overload nomethod => \&wrap, '""' => \&str;
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
"[$meth $a $b]";
}
これでスクリプトの末尾を以下のように書き換えられます
print "side = $side\n";
以下のように出力されます
side = [/ [- [sqrt [+ 1 [** [n 1 u] 2]] u] 1] [n 1 u]]
そして全ての可能なメソッドを使ってデバッガで値を検査できます。
まだ何かがおかしいです: スクリプトのループ変数 $cnt を考えてみます。
これは数値であり、オブジェクトではありません。
この値の型を symbolic にはできません; なぜならそうするとループが
終了しないからです。
確かに、循環を終了させるために、$cnt は偽になる必要があります。
しかし、偽かどうかをチェックする演算子 bool が
(今回はオーバーロードされた "" 経由で) オーバーロードされていて、
長い文字列を返すので、型 symbolic のあらゆるオブジェクトは真です。
これを乗り越えるために、オブジェクトを 0 と比較する方法が必要です。
実際、これは数値変換ルーチンを書くより簡単です。
以下はそのようなルーチンを追加した (そして str() を少し修正した) symbolic.pm の内容です:
package symbolic; # Primitive symbolic calculator
use overload
nomethod => \&wrap, '""' => \&str, '0+' => \#
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
if (defined $b) {
"[$meth $a $b]";
} else {
"[$meth $a]";
}
}
my %subr = ( n => sub {$_[0]},
sqrt => sub {sqrt $_[0]},
'-' => sub {shift() - shift()},
'+' => sub {shift() + shift()},
'/' => sub {shift() / shift()},
'*' => sub {shift() * shift()},
'**' => sub {shift() ** shift()},
);
sub num {
my ($meth, $a, $b) = @{+shift};
my $subr = $subr{$meth}
or die "Do not know how to ($meth) in symbolic";
$a = $a->num if ref $a eq __PACKAGE__;
$b = $b->num if ref $b eq __PACKAGE__;
$subr->($a,$b);
}
全ての数値変換の作業は %subr と num() で行われます。 もちろん、%subr は不完全で、以下の例で使われる演算子のみを含んでいます。 これは追加点の質問です: なぜ num() で明示的な再帰が必要なのでしょう? (答えはこの章の最後にあります。)
このモジュールは以下のようにして使います:
require symbolic; my $iter = new symbolic 2; # 16-gon my $side = new symbolic 1; my $cnt = $iter;
while ($cnt) {
$cnt = $cnt - 1; # Mutator `--' not implemented
$side = (sqrt(1 + $side**2) - 1)/$side;
}
printf "%s=%f\n", $side, $side;
printf "pi=%f\n", $side*(2**($iter+2));
これは(たくさんの改行を除くと)以下のものを表示します
[/ [- [sqrt [+ 1 [** [/ [- [sqrt [+ 1 [** [n 1] 2]]] 1]
[n 1]] 2]]] 1]
[/ [- [sqrt [+ 1 [** [n 1] 2]]] 1] [n 1]]]=0.198912
pi=3.182598
上述のモジュールはとても原始的なものです。
ミューテータメソッド (++, -= and so on) を実装していませんし、
ディープコピー(ミューテータがなければ不要です!)もできませんし、
例で使う算術演算しか実装していません。
ほとんどの算術演算の実装は簡単です; 単に演算のテーブルを使って、 %subr を埋めているコードを以下のように変更するだけです
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
print "defining `$op'\n";
$subr{$op} = eval "sub {$op shift()}";
}
Calling Conventions for Mutators によって、%subr の += エントリを
埋めることと、コピーコンストラクタを定義すること (これは '+=' の実装が
引数を変更しないことを Perl が知る方法はないために必要です;
Copy Constructor と比較してください) のほかに、+= とその親類が
動作するために必要なことは何もありません。
コピーコンストラクタを実行するには、use overload の行に
'=' => \&cpy を追加して、以下のコードを書きます
(このコードは、ミューテータは 1 レベルの深さのみの変更を行うので、
再帰的コピーは不要であることを仮定しています):
sub cpy {
my $self = shift;
bless [@$self], ref $self;
}
++ と -- が動作するようにするには、実際のミューテータを、
直接あるいは nomethod で実装する必要があります。
私達は物事を nomethod で続けると決めたので、以下を
if ($meth eq '++' or $meth eq '--') {
@$obj = ($meth, (bless [@$obj]), 1); # Avoid circular referencen
return $obj;
}
wrap() の最初の行の後に追加します。 これは最も効果的な実装というわけではないので、代わりに
sub inc { $_[0] = bless ['++', shift, 1]; }
とすることを考えるかもしれません。
最後の意見として、以下のようなもので %subr を埋めることができることに 注意してください
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
$subr{$op} = eval "sub {$op shift()}";
}
$subr{'++'} = $subr{'+'};
$subr{'--'} = $subr{'-'};
これは、原始的なシンボリック計算機を 50 行の Perl コードで実装完了します。 部分式の数値はキャッシュされないので、計算機はとても遅いです。
これが課題の答えです: str() の場合、オーバーロードした . 演算子は
すでに存在するオーバーロードした演算子 "" にフォールバックするので、
明示的な再帰をする必要はありません。
オーバーロードした算術演算子は、明示的に fallback が要求されない限り
フォールバック しません 。
従って、明示的な再帰なしでは num() は ['+', $a, $b] を $a + $b に
変換し、これは単に num() の引数を再ビルドします。
もしなぜ str() と num() で変換のデフォルトが異なるかが不思議なら、 シンボリック計算機を書くのがどれだけ簡単だったかに注意してください。 この簡単さは適切なデフォルトの選択によるものです。 もう一つ追加の注意: 明示的な再帰によって、num() は sym() より壊れやすいです: $a と $b の型を明示的にチェックする必要があります。 もし $a と $b がたまたま関係のある型の場合、これは問題を引き起こすかも しれません。
(本当に シンボリックな計算機)
なぜ私達が上述の計算機をシンボリックと呼ぶのかを疑問に思う人も いるかもしれません。 その理由は、式の値の実際の計算はその値が 使われる まで延期されます。
実行中に見るために、以下のメソッドを
sub STORE {
my $obj = shift;
$#$obj = 1;
@$obj->[0,1] = ('=', shift);
}
パッケージ symbolic に追加します。
この変更の後、以下のように出来て
my $a = new symbolic 3; my $b = new symbolic 4; my $c = sqrt($a**2 + $b**2);
$c の数値は 5 になります。 しかし、以下の呼出し後、
$a->STORE(12); $b->STORE(5);
$c の数値は 13 になります。 これでモジュール symbolic はまさに シンボリック 計算機を提供します。
フードの中の荒いエッジを隠すために、パッケージ symbolic へ
tie() したインターフェースを提供します
(Metaphor clash と比較してください)。
メソッドを追加します
sub TIESCALAR { my $pack = shift; $pack->new(@_) }
sub FETCH { shift }
sub nop { } # Around a bug
(このバグは "BUGS" に記述されています)。 この新しいインタフェースは以下のようにして使えます
tie $a, 'symbolic', 3; tie $b, 'symbolic', 4; $a->nop; $b->nop; # Around a bug
my $c = sqrt($a**2 + $b**2);
ここで $c の数値は 5 です。
$a = 12; $b = 5 の後、$c の数値は 13 になります。
モジュールのユーザーを分離す津ために、メソッドを追加します
sub vars { my $p = shift; tie($_, $p), $_->nop foreach @_; }
ここで
my ($a, $b); symbolic->vars($a, $b); my $c = sqrt($a**2 + $b**2);
$a = 3; $b = 4; printf "c5 %s=%f\n", $c, $c;
$a = 12; $b = 5; printf "c13 %s=%f\n", $c, $c;
とすると、$c の数値は $a と $b の値の変更に従います。
Ilya Zakharevich <ilya@math.mps.ohio-state.edu>.
Perl を -Do スイッチか同等のものを使って起動すると、 オーバーロードによって診断メッセージを引き起こします。
Perl デバッガの m コマンド (perldebug を参照してください) を
使うことで、どの演算がオーバーロードされているか (そしてどの祖先が
このオーバーロードを引き起こしているか) を推論することができます。
例えば、eq がオーバーロードされていると、メソッド (eq が
デバッガによって表示されます。
メソッド () は fallback キーに対応します
(実際このメソッドの存在は、このパッケージはオーバーロードが有効になっていて、
overload モジュールの Overloaded 関数で使われていることを
示しています)。
このモジュールは以下の警告を出すことがあります:
(W) 奇数の数の引数で overload::constant を呼び出しました。 引数はペアになっている必要があります。
(W) オーバーロードパッケージが知らない定数型をオーバーロードしようとしました。
(W) overload::constant の 2 番目 (4 番目、6 番目, ...) の引数はコード リファレンスである必要があります。 無名サブルーチンか、サブルーチンへのリファレンスです。
オーバーロードに使用されるため、Perl では、ハッシュ %OVERLOAD は、 パッケージごとに特別な意味を持つことになります。 シンボルテーブルはごみのように見える名前で埋められます。
継承の目的のために、全てのオーバーロードされたパッケージは
(未定義かもしれない) fallback が存在するかのように振る舞います。
これは、あるパッケージがオーバーロードしていないけれども、
2 つのオーバーロードしたパッケージを継承しているという場合に、
興味深い効果を作り出します。
オーバーロードと tie() の関係は壊れています。 オーバーロードは tie() された値の 以前の クラスによって 引き起こされるかどうかが決まります。
これは、オーバーロードの存在のチェックが早すぎて、tie() したアクセスを 試みる前に行われるために起こります。 もし tie() された値の FETCH() されたクラスが変更していないなら、簡単な 回避法は tie() した直後に値にアクセスすることで、この呼び出しの後 以前の クラスは現在のものと同期します。
必要: 速度に影響を与えることなくこれを修正する方法。
裸の単語はオーバーロードされた文字列定数の対象となりません。
このドキュメントは混乱しています。あちこちに 誤解しやすい文章があります。完全な書き直しが必要です。