この記事はPerl Advent Calendar25日目の記事です。
Perlは動的型付き言語として一般的に知られています。実は、そのデータ型の定義とその使い方は一般的な他の動的型付き言語とは考え方が全く異なります。
そして、残念ながらそのデータ型について正しく理解していらっしゃる方はそこまで多くいらっしゃらないと思います。
今回は、そんなPerlにおけるデータ型と、それを用いてデータを扱う上で非常に重要になってくる概念であるコンテキストについて簡単に解説します。
なお、自分は一般的な型システムの概念そのものを正しく理解できている自信はないので、そのあたりの説明に関しておかしな記述があればその点へのツッコミは特に歓迎します。
また、Type::TinyなどPerlの上に作られた型の仕組みについては今回は触れません。
一般的なデータ型
一般的なデータ型の定義としては、たとえばRubyでは以下のようなものが挙げられます。
puts 123.class.to_s # Integer
puts((1.23).class.to_s) # Float
puts "123".class.to_s # String
puts true.class.to_s # TrueClass
puts [].class.to_s # Array
puts [1,2,3].class.to_s # Array
puts({a:"b"}.class.to_s) # Hash
また、Java ScriptにおけるそれはRubyと比べると少し特殊に見えるかと思いますが、同様に、以下のようになります。
> typeof 123
'number'
> typeof "123"
'string'
> typeof true
'boolean'
> typeof {}
'object'
> {} instanceof Array
false
> typeof []
'object'
> [] instanceof Array
true
また、静的型付き言語であるGoにおいてはたとえば以下のような定義があります。
// complex128 is the set of all complex numbers with float64 real and
// imaginary parts.
type complex128 complex128
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
type int int
このように、データ型は一般的にはそのデータの内容そのものの種類を区別するための概念です。
先の例のように、いくつかのデータの種類に対しては型があらかじめ定義されており、その種類のデータについて何の事前定義を自分で書かずともそれを利用することができるようになっているのが一般的です。
たとえば、Rubyの +
演算子はその両辺の型に応じてそのデータを区別することでRubyの仕様に対して正しく振る舞うことができます。
irb(main):001:0> 1 + 2
=> 3
irb(main):002:0> "a" + "b"
=> "ab"
irb(main):003:0> true + true
NoMethodError (undefined method `+' for true:TrueClass)
irb(main):004:0> "a" + 2
TypeError (no implicit conversion of Integer into String)
正確には、Rubyにおいてあらゆる値の型はクラスで表現され、またその値はそのクラスのインスタンスであり、演算子はそのクラスにメソッドとして定義されているので、それぞれの型に対して適切な振る舞いができるようそのメソッドのレシーバーと引数でそれを区別することができます。
また、その型に対する動作が定義されていない演算子を実行した場合にはNoMethodError
がraise
されます。
データ型の実装は様々ですが、一般的にはデータ型は様々な場面で文字列・整数・浮動小数点数・真偽値・配列といった具体的なデータの種類を区別できる概念になっており、それに応じて振る舞いを変えたり場合によってはその利用方法を制約できることがわかります。
Perlにおけるデータ型
ところが、Perlにおいてデータ型の定義はたったの3種類だけです。refs. https://perldoc.jp/docs/perl/5.34.0/perldata.pod
- Scalar (
$calar
) - Array (
@rray
) - Hash (
%ash
)
本当にこれだけなのか?と思われるかもしれませんが、本当にこれだけです。
Perlにおけるデータ型は一般的なデータ型とは異なり、そのデータの種類の詳細に対して関心を持ちません。
それが単一のデータであるのか、配列構造であるのか、ハッシュマップ構造であるのかという点だけを型で区別しています。
配列やハッシュマップは0個以上のスカラー型のデータを持つ型であり、Perlの型として最も詳細なものはスカラー型です。
数値でも、文字列でも、それはただの単一のデータであり、Perlのスカラー型はそれ以上の区別をしません。
Perlは型で数値/文字列などを区別しないにも関わらず、Perlはこれまでに紹介した各種プログラミング言語と同様に文字列や数値などを扱うことが可能です。
以下はそれぞれスカラー型の数値同士の加算と、文字列同士の結合です。(Perlにおいてはそれらの処理の演算子は同じ +
を利用せず、それぞれ別のものになっています。これには明確な理由があるのですがそれについては後述します。)
use feature qw/say/; # Rubyのputs相当であるところの関数sayを使えるようにする
say 1 + 2; # => 3
say "a" . "b"; # => ab
このように、少し不思議な構文ですが、スカラー型という、単一のデータであることしかデータ型で定義されていないにも関わらず数値や文字列などのデータを区別して適切に扱うことができています。
しかし、その前提となるデータ型についての考え方がそもそも異なるので、一般的なデータ型の認識でPerlを扱ってしまうと、何がどうなっているのか?キモい!とハマることうけあいです。
Perlが好きな自分としてはこれが本当に残念でならない。
個人的には、これはPerlの最も面白い部分であると思っていますが、同時に最も理解されづらい部分だとも思っています。
実際には一般的な言語とくらべて型の考え方とデータのコントロールの仕方が違うだけで、
他の動的型付き言語と同様に、コードで型をほとんど完全にコントロールすることができるのです。
それについて今回は解説していきます。
文脈的多態言語
なぜスカラー型だけで先程のような処理ができるのか、それを理解するためにはPerl独特の文脈(コンテキスト)という概念を理解する必要がありますがが、それについて公式ドキュメントperldata
より一部の説明を引用します。
Perlはこのようなデータの扱い方について文脈的多態言語(contextually polymorphic language)という言葉を用いて表現しています。
実際に、Perlの公式ドキュメントのperldataには以下のような記載があります。
Perl is a contextually polymorphic language whose scalars can be strings, numbers, or references (which includes objects).
Perl はスカラが文字列、数値、リファレンス (オブジェクトを含みます)を 保持することのできる文脈的多態言語 (contextually polymorphic language) です。
詳細の説明はぜひ原文を読んで頂ければと思いますが、原文は歴史的経緯も含めた説明であり難解で、また他のドキュメントも併せて読まなければ残念ながらその意味するところがよくわからないかと思います。
そこで、文脈的多態言語とはなにかを自分なりに説明してみます。
文脈的多態言語においては文字列や数値などの詳細なデータ表現を単一のスカラー型にまとめて扱います。
そして、そのスカラー型を評価した文脈(コンテキスト)によってそのデータの表現を変えます。
それにによって、同一のデータをその表現によらず1つの値として扱います。文脈によって多態な表現ができるようになっています。
重要なところは、このようにPerlの型は他の一般的なプログラミング言語の型の考え方とは異なる考え方を持って設計されているという点です。
他の言語における型の理解をそのまま当てはめることは難しく、その型を理解するためには純粋にPerlの型の概念を学ぶ必要があるということを理解して頂ければと思います。
コンテキスト
さて、文脈的多態言語においてはそのスカラー型を評価した文脈(コンテキスト)によってそのデータの表現を変えるということを説明しました。
では、それはどのようなことなのでしょうか?また、そのコンテキストとは一体何なのでしょうか?
Perlにおいてはスカラー型(単一の値)をどのように解釈したいのかを示す都度都度のコンテキスト毎に詳細なデータの種類(数値なのか文字列なのか)が確定します。
そして、そのコンテキストは、演算子や値の受け取り型など、その値の扱い方を形作る構文や演算子などの使い方によって生み出されます。
たとえば、1 + 2
は数値を加算する+
演算であり、その両辺の値は数値コンテキストというコンテキストで解釈されます。
数値コンテキストにおいては、スカラー値の1
と2
はそれぞれ数値としての表現が利用されます。この場合は3
になります。
別の例として、1 . 2
は.
文字列を結合する.
演算であり、その両辺の値は文字列コンテキストというコンテキストで解釈されます。
文字列コンテキストにおいては、スカラー値の1
と2
はそれぞれ文字列としての表現が利用されます。この場合は'12'
になります。
数値の1
と2
は数値リテラルで記述されていますが、それぞれの文字列表現を得てそれを結合した格好です。
ほかにも、my $num = 1;
としていたものを say "num: $num"
とした場合は $num
を文字列内に展開することになるため、文字列コンテキストで評価されたり、など、それぞれの構文や演算子などに応じて一意に表現が定まるようになっています。
このように、スカラー型はある単一の値に対して多相的な表現を持ち、コンテキストによって一意の表現に定まるようになっています。
良く言えば、コンテキストはPerlがいい感じに空気を読んでくれる上でのルールとも言えるかもしれません。
コンテキストの詳細について学びたい場合は以下のスライドがとても分かりやすいのでオススメです:
では、なぜこのような仕組みになっているのでしょうか?
わざわざ標準的な型とは異なるアプローチを取った理由はなんなのでしょうか?
これは持論ですが、Perlの主な用途がテキスト処理であったことが関係していると思います。
テキスト処理
Perlという名前はPractical Extraction and Report Language
のそれぞれの頭文字を取って命名されているとおり、複雑なテキスト処理でも簡単に実現できるように作られた言語です。
そして、その名前が示すように、ログファイルやCSVなどから特定のデータを抽出し、それらをフォーマットして出力することがその主な仕事でした。今日ではテキスト処理はもちろんのこと、システム管理のためのあらゆる処理にPerlは利用されています。
テキスト処理という性質上、その入力は基本的には文字列です。
そのなかでは、たとえば抽出したデータを数値的に扱いたい場面があることもあれば、文字列としてそれを扱いたい場面もあるでしょう。
たとえば、入力となったTSV(TAB Separeted Values)の一番右側の数値の合計を算出し、出力するPerlコードは丁寧に書くと以下のようになります。
use strict;
use warnings;
use feature qw/say/;
my $total = 0;
while (defined(my $line = <ARGV>)) {
chomp $line;
my @columns = split /\t/, $line;
$total += $columns[-1];
}
say $total;
実は、これくらい丁寧に書くならあまり他の言語との違いはありません。
たとえば、Perlの一部を参考にして作られたRubyであれば以下のように書けるでしょう。
total = 0
while line = gets
columns = line.chomp.split("\t")
total += columns.last.to_i
end
puts total
このように、ほとんど同じようなコードをほとんど同じように書くことができました。
(なお、この例はRubyの良いところが出ていて、シジルなどの記号がないぶん、単語を片言で読んでいくような感覚で、言語に対する理解が浅くとも何をするコードなのかがなんとなく理解できるかと思います。)
ただ、このコードは to_i
がなければうまく動きません。
これを除くと以下のようなエラーが発生します。
`+': String can't be coerced into Integer (TypeError)
total
はInteger型であり、それに対して入力はString型であるため、+
演算子がうまく使えないためです。
これは正しい挙動です。
正しい挙動ではありますが、逆に言えばその型を正しく理解し意識的に型変換をしなければ正しいコードにはなりません。
その一方で、Perlはテキスト処理に特化した言語です。
awkやsedで型を意識する必要がないように、Perlでも同様に型を意識せずに書けるようにしたかったのだと思います。
実際、数学的に素直な記号の使い方をすれば数値コンテキストで解釈されてうまく加算ができるような仕組みになっています。
また、自然言語で考える際に、文中の数字を無意識に取り出して考えることになると思いますが、人間はその際にわざわざ型変換など考えません。
なぜなら、それを扱う文脈によってそれをどのように扱えば良いのかを人は理解しているからです。
それと同様のことをコンピューターにやらせるうえでの表現とその実現方法と捉えると、Perlのコンテキストの仕組みはなかなかおもしろいとは思えませんか?
……というのを以下のスライドでも以前自分は主張していました:
ところで、すこし脱線しますが、Perlの作者であるLarry Wallによると、実はPerlはPathologically Eclectic Rubbish Lister
の略でもあります。
これはよく「病的折衷主義のがらくた出力機」と訳されますが、「がらくた」のようなコードをうまく動かすこともできます。
これは、同じことを実現するワンライナーです。
perl -F'/\t/' -anE '$total+=$F[-1]}{say $total'
なぜこのようなコードが動くのかは手前味噌ですが自分の以前の記事を参考にどうぞ:
まとめ
Perlの型は一般的な型の考え方とは異なり、スカラー型・配列型・ハッシュ型の3種類だけしかなく、さらにその具体的な表現はコンテキストによって定まるようになっています。
構文や演算子がデータや変数に対してどのように使われるのかによってコンテキストが形作られ、それによって一般的に言われる「型」のようなものをコントロールするのがPerlの世界観であり、その仕組みがあるおかげでテキスト処理において型のことをあまり考えずとも直感的にコードを書くことができることを示しました。
Perlのコンテキストを面白がって扱えると、Perlの世界は広がって見えてくると思います。
よかったら、これを機にPerlをもっと面白がって触れてみてもらえると嬉しいです。