Open11

ジェネリクス引数の構文的曖昧性まとめ

ジェネリクスを持つ多くの言語では括弧の種類が足りなかったり、既存の文法との互換性を保つために <> をジェネリクス引数に使っている。この文字は比較演算子やシフト演算子にも使われるため、多くの場合は構文的曖昧性の問題がある。

// ジェネリクス引数
(convert<int, string>(number))

// 比較演算子
(score < MAX_SCORE, score > (MIN_SCORE))

各言語でこの問題をどのように解決しているか調べる。

関連する問題として < > を含むトークン (<<, >> など) をどう分割するかという問題があるが、こちらは本スクラップでは扱わない。

C++

元々C言語のtypedef nameに構文的曖昧性があり、宣言済みのtypedefの有無に応じて構文解析の結果が変わる仕組みがある。

typedef int T;
T (*a); // ポインタ型の変数宣言
f(*a); // 関数呼び出し

C++のtemplateも同様に、 < の直後が型かどうかを構文解析時に判定するのが基本的な戦略だと思われる。 (あとでもう少し調べる)

メリット

  • ()[] がすでに使われてしまっていても問題ない
    • TODO: {} だとなんでダメだったんだっけ?

デメリット

Rust

式文脈でジェネリクス引数を指定するときは、Turbofishの名前で知られる括弧 ::< ... > を使う。 https://techblog.tonsser.com/posts/what-is-rusts-turbofish

iter.collect::<Vec<i32>>();
let pt = Point3::<i32> { x: 0, y: 0, z: 0 };

メリット

  • 構文レベルで曖昧性を解消している
  • C++やJavaなど先行する言語と大きな差分がない
  • 魚だと思うとちょっとかわいい

デメリット

  • いつ :: が必要になるかがわかりにくい
  • 識別子とジェネリクス引数が視覚的に遠くなってしまって若干読みにくい

関連

Java

<> を使う。構文の制約が強い + 場合によってジェネリクス引数を前置することで衝突を回避しているっぽい?

変数宣言

Javaの式文には任意の式を書けるわけではなく、かなり厳しい制限がある。以下のような式は有効ではない。

42 > 42; // syntax error

括弧の中以外で比較演算子を書ける式文はないので、変数宣言中のジェネリクス引数が曖昧になることはないと思われる。

new

new の直後にコンストラクタの型引数を、クラス名の直後にクラスの型引数を書ける。Javaではコンストラクタ呼び出しの () が必須なので、 new から ( までの範囲に < が出たらジェネリクス引数と解釈して曖昧なく解釈できそう。

new <T>Klass<U>();

メソッド呼び出し

メソッド呼び出しではジェネリクス引数を前置する。 https://docs.oracle.com/javase/specs/jls/se16/html/jls-15.html#jls-15.12

obj.<T>method();

.< の並びは他にはないだろうから、これで曖昧になることはなさそう。

this. のない関数形式の呼び出しではジェネリクス引数は書けないっぽい。

<T>function(); // error

キャスト

キャスト式が他の式と曖昧にならないようにキャスト対象の式に制限がある。キャスト式であることが確定してしまえば括弧の中は型として読めばいいので、曖昧性自体はない。

) の次のトークンを読むまではキャストか括弧式か区別できない気がするので、先読みをしているかもしれない。

参考

TypeScript

ジェネリクス引数の構文として読んでみて成功したらジェネリクスとして扱う。失敗したらバックトラックして比較演算と見なす。 https://twitter.com/qnighy/status/1396011584158273538 https://github.com/microsoft/TypeScript/blob/v4.2.4/src/compiler/parser.ts#L5197-L5201

(他にも「JSXとgeneric arrow functionの識別」「arrow functionのreturn typeの曖昧性」などいくつか曖昧なパターンがあるが本稿では扱わない)

これは厳密にはJavaScriptの上位互換になっていない。

const x = 42;
console.log(x < x, false > (false)); // TypeScriptではコンパイルが通らない

TypeScript 4.1以前は.js でもこの問題があった。 https://twitter.com/qnighy/status/1396008740252381186

Scala

ジェネリクス引数に [] を使う。かわりに配列には [] を使わない。

array(i) // indexing

Seq[Int] // generics

Go

2019年頃

2019年頃の構想では () を使う方針が示されていた https://blog.golang.org/why-generics https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md

func ReverseAndPrint(s []int) {
    Reverse(int)(s)
    fmt.Println(s)
}

省略もできるらしい。

func ReverseAndPrint(s []int) {
    Reverse(s)
    fmt.Println(s)
}

2021年3月

2021年3月時点で示されている設計では [] を使う方針っぽい。 https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

Print[int]([]int{1, 2, 3})

indexingとの区別をどう実現するのかはあとで調べる

Haskell (おまけ)

関数のtype argumentを明示する記法はない。 (GHC拡張にはあるらしい)

基本的にはtype argumentは型推論に任せる。明示する必要がある場合は式全体にtype annotationをつけるなど他の方法がとられる。

main :: IO ()
main = putStrLn $ show $ 42 -- showの型引数は推論される
main :: IO ()
main = putStrLn $ show (read "(42, 84)" :: (Integer, Integer))
--                                      ^^^^^^^^^^^^^^^^^^^^^ type annotationからreadとshowの型引数が推論される

Type Application

GHC拡張のType Applicationを使うとtype argumentを明示できる。引数に @ をつけることでtype argumentをvalue argumentから区別するっぽい。

{-# LANGUAGE TypeApplications #-}

main :: IO ()
main = putStrLn $ show @Integer 42
ログインするとコメントできます