2023年10月15日日曜日

つれづれ思うこと Perl 編 その9

Mathematica が他のプログラミング言語と異なる点の中で、筆者が面白いと思ったことを紹介します。

x = 3;
y = 4;
ab = {x, y};
ab

と書くと、ab には {3, 4} という具体的な数値が代入されます。後から x や y の値を変えても ab は変わりません。これは、一般的なプログラムと同じです。ところが、

ab = {x, y};
x = 3;
ab

というように、今度は行の順番を変えてみると、一行目で ab には {x, y} という文字式が代入されます。そして、面白いことに、二行目で x や y の文字変数の値を変えると、ab も {3, y} に変わるのです。続けて、

x = 8;
ab

と打つと、なんと ab は {8, y} に変わります。ab の中の x という文字はまだ生きているのです。これは Perl などの一般的なプログラムとは異なる点です。

詳細は Mathematica の「遅延割当」という項目をご覧ください。

これは Mathematica が数値計算だけでなく数式処理もできるためですが、いま大変重宝しています。例えば、ある時は数値計算を、またある時には数式処理をしたいとします。いずれにしても、ab = {x, y} のような公式にあたる箇所を先にプログラミングで組み立てておきます。もし、数式処理をしたければ、これでそのまま数式での答を得ることができます。

一方、数値計算をしたい場合には、ここに具体的な数値(たとえば、x = 8)を代入すれば数値としての答を得ることができます。次に x = 4 の場合を計算したければ、続けて x = 4 を実行するだけです。Mathematica は Jupyter のように使うことができるので、二度も ab = {x, y} を実行する必要はありません。実は、再び ab = {x, y} を実行してしまうと、ab は {8, y} となってしまい、もう x = 4 のケースを計算できなくなってしまいます。

上記のような書き方は数値計算に特化されていないので、実は余計に計算時間がかかってしまうのですが、いろいろな数値を入れて答を比べてみたい時には助かります。

よって、次のような計算もできます。

d = e;
e = f;
f = 8;
d

d を表示させると、8 になっています。
これは、普通のプログラミング言語とは行の順番が真逆です。

ややこしくなりますが、次のような配列を含む演算でも同じことが言えます。

blist = alist; (* 先に変数を結び付けておく *)
alist = {2, 4, 6, 8, 10}; (* そして alist に偶数の配列を代入する *)
alist (* 試しに alist を表示 *)
{2, 4, 6, 8, 10} (* もちろん alist は偶数の配列 *)
blist (* そして blist も表示 *)
{2, 4, 6, 8, 10} (* blist も同じ偶数の配列 *)

alist[[3]] = 100; (* それでは alist の3番目の要素に 100 を代入 *)
alist (* を表示させてみる *)
{2, 4, 100, 8, 10} (* ちゃんと3番目の要素は 100 に変わっている OK *)
blist (* それでは blist は? *)
{2, 4, 100, 8, 10}

先に blist = alist のように公式を組み立てておくと、このような結果になります。

Perl ではこのような行の順番で書くことはできませんので、下記のように上の行から下の行へと水が流れ落ちるように書くことになります。

#!/usr/bin/perl

@alist = (2, 4, 6, 8, 10);
@blist = (1, 3, 5, 7, 9, 11);
@blist = @alist;
$alist[2] = 100;
print "alist = @alist\n"; # 2 4 100 8 10 と表示
print "blist = @blist\n"; # 2, 4, 6, 8, 10 と表示

以上より @blist = @alist という表現では、alist のコピーが blist に複写されているようです。つまり、alist の先頭アドレスが blist に代入されたわけではありません。

しかし、次のように書くと、

#!/usr/bin/perl

@alist = (2, 4, 6, 8, 10);
@blist = (1, 3, 5, 7, 9, 11);
$blist = \@alist;
$alist[2] = 100;
print "alist = @alist\n"; # 2 4 100 8 10 と表示
print "blist = @$blist\n"; # 2 4 100 8 10 と表示
print "blist = @blist\n"; # 1 3 5 7 9 11 と表示

$blist には @alist の先頭アドレス(レファレンス)が入り、それとは別に @blist の配列そのものはちゃんと維持されるようです。

話が急に配列そのもののコピーか、それともアドレス(参照)のコピーかに飛んでしまいましたが、Perl, Python, JavaScript とごちゃまぜに書いていると、いつも分からなくなって困ってしまう点です。どこかに比較対照表のようなものをまとめたブログはないでしょうか?

おそらく Python では次のようになります。

a = [1, 1]
b = a
a[1] = 2
print(b) #[1,2]が表示される

多くの HP には「数値はイミュータブル(変更不可)、一方、リストはミュータブル(変更可能)」などといった説明が載っており、それはそれなりに説得力があるのであるが、Perl と比較しながら理解しようとすると混乱してしまいます。上記の Python は、いわば C 言語でいうところの、配列名はその先頭アドレスを表すという概念と似ています。

Python でリスト(配列)そのものをコピーするには、
b = copy.copy(a) # 浅いコピー
としないといけないようです。二次元以上の配列になってくると、またちょっと複雑。

b = copy.deepcopy(a) # 深いコピー
を使わないといけない。

一方、Perl では
@blist = @alist;
と書くと、配列そのもののコピー(複写)になることに注意しないといけません。

Mathematica でも同じ複写ですが、blist = alist を先に書いてから、後で具体的な数値の配列を alist に入れるという方式をとると、これはアドレス渡しのように振る舞うことになります。

いや~もう疲れた。もう JavaScript まで行きたくない。が、ついでだ!思い切ってやってみよう。もう混乱してきたので、詳細は他の情報源に譲ることにして要点だけ。

JavaScript の配列では参照渡しとなるようです。配列を値渡しに(複写)するには、slice や concat の引数なしを使うようです。

var arr1 = [0, 1, 2, 3, 4];
var arr3 = arr1.slice();

ということは C 言語と似ているということ?

C 言語では、配列名は先頭アドレスを指しますよね。
なので、b = a の後で、a の中身を変えると、b も変わって見える。

C 参照渡し
a[3] = {1, 2, 3};
b = a;

Python 参照渡し
a = [1, 2, 3]
b = a

JavaScript 参照渡し
a = [1, 2, 3];
b = a;

Perl 値渡し
@a = (1, 2, 3);
@b = @a;

ただし、$b = \@a; のように、b をポインタ変数のように使うことも可能。この場合は、それ以降 @$b の形で配列 b を使う。「\」は、C 言語のアドレス演算子「&」と似ている。

Mathematica 1 値渡し
a = {1, 2, 3}
b = a

Mathematica 2 参照渡し
b = a
a = {1, 2, 3}

こうして見てみると、Python は、まあ C と似ているのかも。
Perl や Mathematica がむしろ異端児なのか?

以上は一次元配列での話でしたが、NMR データのように二次元配列の場合、配列のコピーはどのようになるのでしょう?おそらく C 言語では memcpy を使う?

もう限界なので、ここでやめておきます。