M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Java言語ではFunc<T>とFunc<T,U>は区別できない

この1年くらいの間Javaを離れてC#ばかりをコーディングしていました。戻ってきて愕然としたのは例えばFunc<T>Func<T,U>という型を仮定した時、Java言語においてはそれらが区別のつかない型、型名の競合を引き起こす型として捉えられてしまうということです。

これは、次の2点を考えればなんのことはない、当然の事態ではあります:

  • Java言語のジェネリクスのベースにイレージャのメカニズムが存在すること、つまりコンパイル過程でリフレクションのための型のメタ情報はともかくとして型そのものからは型パラメータが消去されてしまうこと。
  • C#言語におけるような型パラメータに即した型名の自動修飾──Func<T>Func`1となり、Func<T,U>Func`2となる──の仕組みはJava言語には存在しないこと。

1点目のイレージャ型のジェネリクスを採用したことの背景にある事情を考えれば、2点目もこのようにならざるをえないわけです。既存APIを維持しつつ拡張しようとすると、イレージャを採用する一方で型名の自動修飾のメカニズム(もしくはルール)は不採用とする必要があるのです。

Java SE 8の関数型インターフェースについて勉強する人はFunctionBiFunctionSupplierなどなどのいくつものインターフェース名を記憶させられることになります。種々の型名で区別されるインターフェースが存在することは、それらのインターフェースの目的・役割を示す点でも有意義なものではありますが、実情としては型名の競合を防ぐためにはこうするしかなかったということです。
言い方を変えると「ソースコード上、型パラメータで示される情報は同時に型名でも示さないといけない」という二重修飾の拘束がはたらいているのです。

「だからなんだ、C#だって裏側では型名の自動修飾で競合を回避したりしているではないか」と意見もありそうですが、それにしてもそうした内情をソースコードのレベルで露わにしてしまい、標準APIの「概念的重み」(≒学習コスト)も増大させてしまうのはやはり望ましいことではないでしょう。イレージャ型ジェネリクスには、型パラメータの実パラメータが異なる型を引数とするオーバーロード・メソッドが定義できないという、これも地味ですが何かしらのAPI設計をしていると結構痛い(結果として引数の型名やメソッド名にバリエーションをつくる必要が生じる)問題もあります。

いずれも「言っても仕方のないこと」ではあるのですが、C#からの帰還後にはどうにも気になってしょうがないことでもあるのです。。

Unclazz.Commons.Jsonライブラリv1.2.1リリース

昨年11月に公開したJSONパーサー(かつシリアライザ) Unclazz.Commons.Jsonライブラリ のバージョン1.2.1をリリースしました。
このリリースでは以下の2つのバグフィックスを行っています:


余談 - サロゲートペア問題

ところで、目的はあくまでバグフィックスだったのですが、とくに2点目のUnicodeエスケープシーケンスのサポート追加はいくつかの点で勉強になりました:

  • UTF-16ではUnicodeの拡張領域に含まれる文字を表現するためサロゲートペアという4バイトを使用する(それ以外は2バイト)
  • JavaC#のようなやや旧世代の言語では文字列はUTF-16で表現されている
  • これらの言語における文字型(char)はその定義からしてサロゲートペアを扱うにはキャパシティが足りない(Javaでは概念として「文字」と「コードポイント」を分けており後者はサロゲートペアを含む)
  • これらの言語における文字列型(String)リテラルで拡張領域に含まれる文字をUnicodeエスケープシーケンスで表現する場合\uXXXX\uXXXX(XXXXは左ゼロ詰め4桁固定16進数値)という記法をとる
  • これは「あたかも2文字であるように記述する」とも読めるが実際上その長さ(JavaではString#lengthC#ではString#Length)をとると2文字分に該当する
  • 「(Python3の)文字列はUnicode文字のシーケンスだ」というとき含意されているのはこうしたUTF-16に依拠する実装をとる前世代に対する優位性である

参考にした記事:


2016年11月の記事:

m12i.hatenablog.com

数週間かけてC#でパーサーコンビネーターつくってみた

先日ふとFastParseというScala言語で開発されているパーサーコンビネーター・ライブラリーについての記事を見つけ、それを読んでいるうちにC#言語で似たようなものを作ってみたくなりました。

そして、実際に作ってみたのがこちら

README.mdからの転載となりますが、例えば次のコードは浮動小数点数のパーサーの実装例です:

sealed class NumberParser : Parser<double>
{
    readonly Parser sign;
    readonly Parser digits;
    readonly Parser integral;
    readonly Parser fractional;
    readonly Parser exponent;
    readonly Parser<double> number;

    public NumberParser()
    {
        sign = CharIn("+-").OrNot();
        digits = CharsWhileIn("0123456789", min: 0);
        integral = Char('0') | (CharBetween('1', '9') & digits);
        fractional = Char('.') & digits;
        exponent = CharIn("eE") & (sign) & (digits);
        number = ((sign.OrNot() & integral & fractional.OrNot() 
                  & exponent.OrNot()).Capture()).Map(double.Parse);
    }

    protected override ParseResult<double> DoParse(Reader input)
    {
        return number.Parse(input);
    }
}

こうして作成したパーサーは次のようにして使用します:

var p = new NumberParser();

var r1 = p.Parse("-123.456");
var s1 = r.Successful; // returns true.
var c1 = r.Capture; // returns Optional(-123.456), and c1.Value returns -123.456.

var r2 = p.Parse("hello");
var s2 = r.Successful; // returns false.
var c2 = r.Capture; // throws InvalidOperationException.

FastParseのドキュメンテーションにあるサンプルを真似たものです。

比較してみてもらうと一目瞭然ですが、演算子オーバーロードの許容範囲の違いによる影響は大きいです。内部DSLを作り上げる場合、演算子オーバーロードでどこまでの演算子を(あるいは標準にはない演算子を)サポートできるかは、結果に大きく出てきます。逆に言えばDSLづくりでもしない限りそんなキャパシティはあるだけ無駄ということかもしれませんけど。。細かいところではその他にもいろいろ制約となった事項はあるのですが、くたびれてしまってここに書き出す気力もありません。

つくりながら今更ですが「パーサーコンビネーターにおけるパーサーは値のキャプチャを行わないケースが多いこと」「(パース対象の構文にもよりそうだが)パースの途中経過もしくは最終成果物として抽象構文木のような構造は必要不可欠ではないこと」「パーサーを組み立てる過程で『左再帰』の除去の工夫が必要になること」などなどの事項を学ぶことができました。