0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Perl で JSX を実装してみる

Last updated at Posted at 2024-12-25

Merry Perl Birthday! 12月18日は Perl の誕生日! :cake: 1

JSX とは

JSX は React などで使われる、プログラムの中に HTML のようなものを導入する構文です2。もともとは React と一緒に使うために生まれたのですが、今では React から独立して使うこともできます。

JavaScript を読んでいたら突然 HTML3が出てきてドキッとするのですが、慣れると意外に読みやすいという構文です。

const greeting = <h1 className='greeting'>Hello</h1>;

これは一体何かというと、これが React で書かれた場合、実際には見えないところで React.createElement 関数の呼び出しに変換されます。

import { createElement } from 'react'
const greeting = createElement(
    'h1',
    { className: 'greeting' },
    'Hello'
);

上記のような、JSX を JavaScript/TypeScript に変換する処理は、通常は何らかのトランスパイラによって事前に行われます4

React では 関数コンポーネント を定義することによって、JSX として新たなコンポーネント=要素を定義することができます。

import { createElement } from 'react';

function Greeting({ name }) {
    return <h1 className='greeting'>{name}</h1>;
}

const greeting = <Greeting name='Hello' />;

関数コンポーネントも、結果的に React.createElement の構文へ帰着(トランスパイル)されます。

また、関数コンポーネントは「子要素」を持つこともでき、多数の関数コンポーネントを重層的に配置されるよう定義することでコンポーネントの再利用性を高めることができます。

ここが重要なところなのですが、 コンポーネントの再利用性を高めようと多数の小さな関数コンポーネントを作成する場合、いちいち createElement を書くことが辛くなることは想像に難くありません5。createElement を書かなくて良い、これが JSX の大きな側面と言ってもよいでしょう。

React.createElemennt が内部的にどういった解釈をされるか、世間で仮想DOMと呼ばれるものの関係等、込み入った解説は割愛します。

さてさて、この JSX、Perl でも導入できそうではないですか?

Perl で JSX を実装できるか考察してみる

Perl で JSX ……といっても、どうするとよいでしょうか。例えばこんな感じ?

# 注:まだまだ想像上のコードなので動かない
use v5.40; # signature の feature を含んでいることに注意

sub Greeting(%prop) {
    my $name = $prop{name};
    return <h1 className='greeting'>$name</h1>;
}

const greeting = <Greeting name='Hello' />;

んんん???これはどうなんだろう?

ここで少し立ち返って、JSX という文法が JavaScript(または TypeScript) に後付で定義できた理由を考えてみましょう。大方、以下のような状況があったからでしょう。

  • JSX の 「開始タグ」の最初の1文字目である < が登場するところは、演算子ではなく値が置かれることを期待された場所であり、JavaScript で値が期待される場所に < から始まるなにかのトークンが書かれることはなかった
    • return の直後、代入演算子 = のすぐ右側などは、値が置かれることを期待された場所です
    • 結果的に createElement を通した何らかのオブジェクトが評価値としてそこに置かれます
    • もし演算子が置かれることを期待された場所であれば < は小なり比較演算子となるでしょう
  • JavaScript で、リテラルやコメントではない JavaScript の構文が書かれる場所において < は小なり比較演算子の意味しかない
    • ジェネリクス <Foo> は、JavaScript にはない TypeScript の文法です
      • TypeScript においても、ジェネリクスが置かれる場所は、JavaScript の文法の外側の TypeScript の文法にあたるところであり、純粋に JavaScript 的評価をするとエラーとなる場所です
    • 正規表現にて < は名前付きキャプチャ等で使われる特殊な記法ですが、これは正規表現リテラルの中での話であり、JavaScript の文法上の評価ではありません

上記のような状況があったため、JavaScript に JSX の文法を入れても他の文法との衝突が無く、曖昧さ無しに後付けできたと言えるでしょう。

そう考えると、Perl の < には(小なり比較)演算子以外の値評価に関わる文法があります。そう、readline として知られる、ファイルハンドル $fh から1行読み出す構文

my $line = <$fh>;

です。

すでに値評価の構文に < が使われていました。JSX 導入は諦める他ないのでしょうか。

Perl の <ファイルハンドル> を考察してみる

Perl の <ファイルハンドル> は以下のような構文でした。

  • <> でファイルハンドルを挟むことで、それを値評価するとファイルハンドルから次の1行を読むことができる
    • これは readline(ファイルハンドル) の別構文です
  • ファイルハンドルは、Perl 5 から導入されたスカラー変数のシジル $ を伴った $fh といったものと、Perl 4 以前からあった裸のワード FH がある
    • 現在では $fh が推奨されています
    • 裸のワードではレキシカル変数にできませんが、スカラー変数の $fh はレキシカル変数にできます
    • 現代でも、ごく一部の特殊なファイルハンドルは裸のファイルハンドルを使います

裸のファイルハンドルが使われるごく特殊なファイルハンドルとは、標準入出力に関する STDIN STDOUT STDERR 、あとよく使われるものは DATA あたり(残りは後述)。

少し考えてみると、 <STDIN><STDOUT><STDERR><DATA> 以外、例えば <FOO> と書かれていたら、それはファイルハンドル FOO の readline ではなく、JSX だと思えばいいだけなんじゃ!?

上記で $fh 形式のファイルハンドルが推奨されていると言った話をしましたが、通常の JSX のコンポーネント名に $ は使われなければ6、STDIN 等の特殊な裸のワードだけ特殊扱いにすれば良さそうです。

そもそも2024年は Perl 5.40 の世。私も最初のサンプルコードに use v5.40 と書きました。

そう、Perl 5.34 から、裸のワードのファイルハンドルを禁止にする bareword_filehandles ができたではありませんか!bareword_filehandles は、以下の7個の裸のファイルハンドル以外の裸のファイルハンドルを使用禁止にします。

  • STDIN : 標準入力
  • STDOUT : 標準出力
  • STDERR : 標準エラー出力
  • DATA : 同じファイルの __DATA__ トークンが書かれた行の次の行以降
  • ARGV : コマンドライン引数 @ARGV にファイルパスが書かれているとして開いたファイルハンドル
  • ARGVOUT : コマンドラインに -i (in-place) オプションを指定した場合の出力ファイルのファイルハンドル
  • _ : stat 組み込み関数で検査した直前のファイルの情報を格納したファイルハンドル

Perl 公式で、上記7個の裸のファイルハンドル以外は使うなと言っておられる。というわけで、この7個以外が < の直後に続いても、JSX であるとみなすことにするでよいでしょう。

Perl でどう < に JSX 開始の意味をもたせるか

JavaScript と Perl の文法を考察することで、Perl にもある程度妥当な形で JSX を導入できそうだということがわかりました。では実際の導入手法はどうするとよいでしょうか。考えてみましょう

外部トランスパイルツール → 真っ当だが面倒だし Perl っぽくない

React が JSX を導入する際に、外部トランスパイルツールを使う方法をそのまま真似る方法です。

一番確実にできるとは思うのですが、ちょっと Perl っぽくないなと感じてしまいます7。実装するとなると、参考にできる先行実装も無さそうな割に、考えることが多くなりそうなのも悩ましいポイント。

use overload → できそうに見えて、やっぱりできない

演算子オーバーロードができる Perl コアプラグマの overload が使えるかなと思いました。

例えば比較演算子 <> はオーバーロード可能演算子なので

use overload
    "<" => \&tag_start,
    ">" => \&tag_end;

とすることで、うまく置き換えられるのでは?と思わせておいて、これはうまくいきません。上でも少し出てきた通り、比較演算子は二項演算子であり、値と値の間に置かれる(値ではない)演算子だからです。つまり return の直後にある < は、すぐ左の return が値ではなく制御構文の一つなので、そこに置かれる < は二項演算子にはなりえません。オーバーロードをしたとして、二項演算子としての定義を変更することはできません。

ダメかーと思いつつつ overload の perldoc に再度目をとしてみると、readline の <> もオーバーロード可能だと書いてありました!やった!

use overload
    "<>" => \&create_tag;

これで行けたか…と思ったのもつかの間、これもうまくいきません。

これは以下のように簡単なサンプルでわかります。

use overload "<>" => &create_tag;

my $greeting = <div>Hello</div>;
#              ^^^^^create_tag("div")の評価値
#                   ^^^^^通常の文字列リテラルどころか裸のワード?
#                        ^^^^^^create_tag("/div")の評価値

私も "<>" をオーバーロードしたことが今までに無く、深い挙動はわかっていないのですが、たとえ上記のように3つの値が展開できたとして、3つの値が何の演算子の類も無く隣接しているだけになります。これだけでも文法エラーですが、「タグ」で囲まれた内容 `Hello` に至ってはただの裸の文字列となってしまい、 `use strict` 下では当然エラーとなってしまいます。"<>" のオーバーロードはあくまでそれ自身の値評価方法の変更にとどまるため、なんの演算子の類も隣接する「内容」「子要素」の処理に介入することは多分できないでしょう。

思いついたときは、overload があるのでコアだけで手軽にできる!と思ったんだけどなぁ。3分くらい考えてダメだとわかりました。

あくまで「演算子オーバーロード」なわけで、演算子としての使用を逸脱することはできないというオチでした。

キーワードプラグインによる実装 → 私には難しすぎる、何でもありになってしまう不安

Perl 5.14 の差分にて プラグ可能なキーワード(Pluggable keywords) の仕組みが登場。これを利用すると、Perl の文法の範囲内のメタプログラミングにとどまらず、新しい文法キーワードを導入することができる機能です。界隈では Keyword Plug-in、キーワードプラグイン等とも呼ばれているようです。

この機能を用いて実装されているのが、Function::Parameters や、Paul Evans 氏によって取り組まれている Syntax::Keyword:: シリーズ。昨今では Perl 5 コアの開発の実験的位置付けにもなっているようです8

演算子とかに限らずブロックといった広範な文法を書き換えられるのであれば JSX もイケるのでは!?と思ったのですが、実装には XS(Perl の C 言語拡張)を覚える必要があり、そもそも本当にキーワードプラグインが JSX 実装に使えるのか、それの調査すら私には難しそうでした9

言ってしまうと低レイヤー言語でインタープリタの処理に深く介入する方法論ですが、JavaScript であれば Node.JS それ自体に手をいれるのか…と考えると、大掛かりだな…という気がします10。あと C 言語などの低レイヤー言語でインタープリタに直接働きかけるのは、インタープリタのメジャーバージョンアップ等の環境変化に総じて弱いのも及び腰になってしまう11

ソースフィルタを用いる → 手軽かつ Node.js でのアプローチに近い

Perl 5 には ソースフィルタ という仕組みが備わっており、C のマクロのような、実際にソースコードを Perl の文法として評価する前にソースコードを文字列処理する仕組みが存在します。これを用いることで、 Perl の文法から外れた内容を書いたソースコードにソースフィルタを適用することで、適切に Perl の文法の範囲内に収めることができます。

Perl のソースフィルタについて理解するには、Perl に付属している perldoc perlfilter が参考になります。Node.js の JSX も「Node.js の実際の実行前に別の枠組みで変換される」ため、アプローチとしても似ているなと感じます12

「毎回ソースコードを変換するのは重い」という懸念も、実際はそれほど心配ではないでしょう。インタプリタ言語はそもそも遅いです。また、JSX のようなツールは「コマンドラインツールとして頻繁に実行されるよりも、一度サーバプログラムとして実行(コンパイル)されたあとは、デーモンプロセスとして長く稼働し続ける」ものであり、その時間は初回実行にのみ積まれるものでしょう。

ここまで考察して、とりあえずソースフィルタを使ってみるのが良さそうだと感じました。早速やってみましょう。

ソースフィルタで Perl に JSX を実装する

前段の考察が長くなったので、さっさと実装してみましょう。

適当なフォルダを作って書き始めてみます。

use v5.40;
use lib qw(./lib);
use utf8;
binmode STDOUT, ":utf8";
use FilterJSX;

my $abc = 123;
my $def = <div>Hello</div>;

このあたりからスタートしましょう。 lib/FilterJSX.pm を書いてこれを動かしてみましょう。

私も今回初めてソースフィルタに触れたか、遠い昔にソースフィルタを触ったっきりで何もかも忘れているかのどちらかなのですが、Damian Conway 氏が用意してくれた Filter::Simple を使うことで文字通りシンプルに実装することができました。

まず練習からやってみます。底が2の対数を求める log2 をサブルーチンではなくソースフィルタで導入してみましょう。

use v5.40;
package FilterExam {
    use Filter::Simple;
    FILTER {
        s/\blog2(?=\()/(1\/log(2))*log/g;
    }
}
1;

FILTER ブロックの中に、置換正規表現を書けばOKなのは楽ですね!

なお正規表現の説明をすると、単語「log2」があって、その直後に開き丸括弧がある場合、その log2 を (1/log(2))*log で置換するというものです。直後にある開き丸括弧はそのままに、 1/log(2))*log が置かれることで、対数の底の変換公式となるわけです。つまり log2(8)(1/log(2))*log(8) となり、数式で書くと

\log_2 8 = \frac{\log_e 8}{\log_e 2}

であり、この値は $8=2^3$ より 3 です。

$ cat exam.pl
use v5.40;
use lib qw(./lib);
use FilterExam;

say "Hello";
say "log2(8) = ", log2(8);
say "log2(3) = ", log2(3);
say "log(3)/log(2) = ", log(3)/log(2);
$ perl exam.pl
(1/log(2))*log(8) = 3
(1/log(2))*log(3) = 1.58496250072116
log(3)/log(2) = 1.58496250072116

この例からも分かる通り、そのままだと文字列リテラルの中も置換してしまいます。このあたりは、C のマクロと違って大味ですね(C のマクロはリテラルの中を置換しない)。

この場合、FILTER_ONLY を使うとよいです。

use v5.40;
package FilterExam {
    use Filter::Simple;
    # FILTER {
    #     s/\blog2(?=\()/(1\/log(2))*log/g;
    # }
    FILTER_ONLY
        code => sub {
            s/\blog2(?=\()/(1\/log(2))*log/g;
        }
    ;
}
1;
$ perl exam.pl
Hello
log2(8) = 3
log2(3) = 1.58496250072116
log(3)/log(2) = 1.58496250072116

なんか大丈夫そうですが、上の log2 導入コードはほころびがあります。たとえば log2( の間に空白を入れる場合。Perl の文法上はサブルーチンのキーワードと引数丸括弧開きの間に空白は許されるのですが、上記の正規表現では許されないため log2\s*(?=\() などと書く必要があるでしょう。その他にも見逃している箇所はあるかもしれません。

今回の例では問題になりませんでしたが、FILTER や FILTER_ONLY で渡されるソースコードは1行毎に都度渡されるわけではなく、丸ごと渡されることにも注意。

これはたとえば、 %Q から %E までを文字列としてみなす独自構文を入れる場合、先程のコードに

use v5.40;
package FilterExam {
    use Filter::Simple;
    FILTER_ONLY
        code => sub {
            s/\blog2(?=\()/(1\/log(2))*log/g;
            s/%Q(.*)%E/"$1"/g; ### これは悪い例
        }
    ;
}
1;

と追記しても

my $str = %QHello,
World,
Ogata.
%E;

say "str => $str";

という場合に対応できないということです。

これは素直に正規表現のメタ文字 ./s 修飾子をつけて改行にもマッチするようにすれば良いです13

use v5.40;
package FilterExam {
    use Filter::Simple;
    FILTER_ONLY
        code => sub {
            s/\blog2(?=\()/(1\/log(2))*log/g;
            s/%Q(.*)%E/"$1"/gs;
        }
    ;
}
1;

だいたいソースフィルタがつかめたところでいよいよ本題。

とりあえず、書いた JSX の要素を、その文字列リテラルとして取得する場合、以下のような感じかなと書いてみました。

use v5.40;
package FilterJSX {
    use Filter::Simple;
    FILTER_ONLY
        code => sub {
            s{<(\w+)([^>]*)/>}{<$1$2></$1>}g;
            s{<(?!STDIN|STDOUT|STDERR)([\w-]+)([^>]*)>(.*?)</\1>}["$&"]g;
        }
    ;
}

1;

書かれる箇所はコードが解釈される場所なので FILTER_ONLY code としました。

まず、処理を一元化したかったので、閉じタグ省略の <Foo /><Foo></Foo> にしておきます。これが最初の正規表現。

その次に、正規表現の否定の先読みを使って、STDIN STDOUT STDERR ではないキーワードが要素名となった場合に、その閉じタグまでをマッチさせて、ひとまず "$&" としてマッチ文字列全体を文字列リテラルとするようにしています。

単純な例では問題ありません。

(続きます)

  1. 18日に投稿できず、後日しれっと投稿していることはお見逃しください。あと豆知識として Perl1 とファイナルファンタジー1 は生年月日(リリース日と発売日)が同じです。詳細は Perlファイナルファンタジーの Wikipedia 記事を参照ください。

  2. なお、10年ほど前に DeNA が開発した Alt-JS 実装の方ではありません。

  3. より正確に言うと well-formed な XML。

  4. JSX をトランスパイルできる有名なトランスパイラとして、Babel、esbuild、tsc (TypeScript) があります。最近では Vite や Biome を使う事例も増えてきています。

  5. できる限り書かなくて意図が伝えられるのであれば、書かないことが正義です。今では多くの人が過去の遺物としてネガティブな論調を投げかける jQuery もいちいち document.getElementById や document.querySelectorAll といったことを書かなくていいライブラリです。大きな違いは、思想の上で React や JSX は宣言的であり、jQuery は命令的であるというところ。jQuery にネガティブな論調が投げかけられる主要な要因の一つは、現在のパラダイムが宣言的インターフェースに傾いているためですが、書かないことが正義の歴史は連綿と続いています。私は jQuery やさらに過去の JavaScript が苦しい時代を支えたライブラリに敬意を持ち続けています。

  6. JavaScript の文法に置いて $ は半角英文字と同等の扱いができる文字です。つまり変数名や関数名として $ を英文字同様使っても構わないということ。その点において、本家 JSX コンポーネント名に $ が使われることは、もしかしたらあるかもしれません。私はそこまで深い JSX の知識はないのですが、XML の仕様では $ 記号は要素名としては使えないので、JSX でも使われることはないのではと考えています。

  7. この違和感をうまく説明しづらいのですが、一つの理由として、自分が(少なくとも広く使われている)Perl プログラムファイルをトランスパイルするツールを聞いたことがないため。

  8. 第79回 最近Perlに追加された実験的機能 try文、defer文、class文(1) | gihyo.jp などが詳しいです。

  9. 今まで Perl XS の解説を読んでも、自分に使いこなせる気が全くしませんでした。あと、頑張って対応したとして、忘れず身につけ続けられる自信がない。Perl XS を日々使い続けられるような場所に身を置けば少しは変わるかもしれませんが、そういう職場や案件はあるのだろうか。むしろ、C 以外の Rust とかで XS が書けるようになる日を心待ちにするほうが良かったり!?勉強しない言い訳は色々できますが、才能や時間は有限であるというのもまた事実。

  10. Node.js での React や JSX はそういうアプローチを取らずトランスパイラをおいているのは結果的に良いアプローチだなと思えます。Node と同じ作者のライアン・ダール氏による Deno が JSX にネイティブ対応している のは、ある意味「Node に手を入れたら Deno になった」みたいな神の手のような感じがして興味深いです。

  11. 文法上は後方互換性を重視する Perl ですが、XS 使用モジュールが新しい Perl バージョンで動かないということはしばしば見かけます。また Node.js でも高速な演算処理が必要な場合に C 言語等で処理を書く NAN(Native Abstractions for Node.js) といった機構が存在するようですが、事情は似たようなもののようです。プロジェクトトップページには「Thanks to the crazy changes in V8 (and some in Node core), keeping native addons compiling happily across versions, particularly 0.10 to 0.12 to 4.0, is a minor nightmare.」(要約すると、V8 や Node.js のコアの一部のクレイジーな変更に影響されて、バージョン間でコンパイル可能を維持するは小さな悪夢だとのこと)と書かれているほど。NAN とは違ったアプローチでコンパイル言語のマシンネイティブバイナリを Node.js プログラムに取り込む node-gyp なども同様。今回は Perl の話であり、Node.js の NAN よりは Perl XS のほうが後方互換性に配慮しているのかなという気もしました。

  12. ソースフィルタが Perl の文法としても use Filter::Util::Call などとして実現できる理由は、Perl のプログラム実行が、コンパイル時フェーズと実行時フェーズに分かれているからでしょう。これは他の P 言語(PHP、Python、Ruby)には無い特徴だと思われます。ソースコードを評価する上での核心そのものをソースコードに文法として入れることは、いくつかの事例を観測すると困難ですが、Perl はコンパイル時フェーズを利用して対応しています。 use はコンパイル時フェーズに評価される数少ないキーワードの一つです。

  13. 詳細は Perlの正規表現の一行モード(/s)と複数行モード(/m)の覚え方 #正規表現 - Qiita を参照ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?