(注:このブログはもう更新していません)この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

[クラウド帳票エンジンDocurain]

むかし自作したプログラミング言語の仕様書


昔、Velocityを激しく改造したプログラミング言語(もちろん処理系も)を作ったことがあるのですが、
PCを整理していたら仕様書が出てきたので載せてみます。


いや、深い意味はないんです。
近頃は多忙のため全くブログを更新できておらず、苦し紛れにやってるだけ。。。

コレを作った当時、

一番苦労していたのが『分かりやすいコンパイルエラー情報の表示』。
分かりやすいコンパイルエラー情報を表示するためには、相当な分量の情報をずっと持ちまわっていかなければなりません。
また、コンパイルエラーを起こしている箇所を特定して、そこだけ抜き出すのも結構大変でした。


きちんとしたコンパイラを作れる人はスゴイ、と身を持って知ることになった作品です。




全文貼りつけたら途中で切れちゃったので、

途中(10章)までです。ではどうぞ。

Title: スクリプト言語vel リファレンスマニュアル

version : $Id$

1 目次

2 velとは

そもそもは、Apache Velocity三項演算子を追加するところから
開発がスタートしたスクリプト言語です。基本的な文法は、一部を除いてほぼ互換性があります。

しかしvelは、Velocityと異なり テンプレートエンジンではなく、あくまでもスクリプト言語 です。
従って、それぞれ目指すところが全く違います。

2.1 謝辞

スクリプト言語velは、Apache Velocityのソースを元に作成されました。
この偉大な製品を作り出した作者の方々には、心からの尊敬の意を表します。

2.2 謝罪

まず最初に、

  • シンプルなテンプレートエンジンであるVelocityを、こんな風に改造してしまってすいません。

また、現状のvelコンパイラ実装には、いろいろなくだらない制限があります。

  • 例えば、式のグルーピングが「基本的には」できません。ごめんなさい。
  • 例えば、式の中にコメントを入れることはできません。ごめんなさい。
  • 例えば、コンパイルエラーのメッセージはとても不親切です。ごめんなさい。

2.3 前提条件

このテキストの読者には、以下の知識があることを前提としています。

3 Hello, World

それでは、早速お約束のプログラム(と言えるかどうか微妙ですが)を作成してみます。

<prog.1>

Hello, World!

このプログラム<prog.1>の出力は以下の通りです。

Hello, World!

どうでしょうか。
まぁ、このままでは味気ないので、HTMLで書いてみます。

<prog.2>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja-JP">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
Hello, World!
</body>
</html>

<prog.2>の出力は以下の通りです。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja-JP">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
Hello, World!
</body>
</html>

出力は、<prog.2>で記述した内容そのまんまです。(実際コピペしただけです)

以上のことから、普通のHTML(velの制御構文が入っていない、プレーンなHTML)を出力する分には、
そのまま書けばよさそうだということがわかると思います。

まとめると、

  • ただのプレーンなテキストでも、正当なvelのプログラムとして認識される。
  • 出力は、テキストの内容そのまんまである。PHPとかと同じ。

4 Hello, World (その2)

前章では、ただ単に固定の文字列を出力してみただけでした。

次は、せっかくだから自分のユーザIDを出力するプログラムに改良してみます。

<prog.1>

Hello, $_USER.id!

そして、実行します。以下のように出力されるはずです。

Hello, kechizen!

やっとプログラムらしくなってきました。

さて、さっそくですが上記<prog.1>の

$_USER.id

はこんな意味です。

『「USERという名前のオブジェクト」の中から「idという名前の値」を取り出して表示せよ』

velでは、 $_USER のように $ から始まる半角英数文字列は、 参照 と呼びます。
スクリプト言語ではお馴染の記法です。

そして、 .id は、の部分は プロパティ と呼びます。
これは、オブジェクトの中から値を取りだすという意味となります。

なお、 $_USER 自体は ビルトインオブジェクト です。
ビルトインオブジェクトについては後の章で解説します。

4.1 参照の例

有効な参照とは、

  1. $ から始まり、
  2. 次に[半角英字、またはアンダースコア]があり、
  3. その後に0個以上の[半角英数、またはアンダースコア]が続くもの

となります。大抵のプログラミング言語の場合とほぼ同じです。

以下は、有効な参照の例です。

$a

$sn42

$HOGE

$foo_bar

そして、こちらは無効な参照の例です。(パースエラーになります)

$        ## "$" だけ

$42sn ## 数字から始まっている

$foo&bar ## "&"は無効な文字

$ hoge ## "$" と "hoge" の間にスペースがある

なお、有効な参照を正規表現で表すと

\$[A-z_][A-z0-9_]*

となります。

4.2 無効な参照

さて、 $USER.id と記述すると自分のユーザIDを表示することがわかりました。
それでは、次に自分の年齢を表示してみます。

$USER.age

これを実行すると、以下のように表示されます。

$USER.age

あれ、プログラムと全く同一の出力ですね。ちゃんと実行されていないのでしょうか。
それでは、これではどうでしょうか。

$my_age

これの実行結果も、やはり同じです。

$my_age

…とまぁ、そもそも出力されるはずがないですね。29とかが表示されたりはしません。
なぜならユーザ登録する際に、自分の年齢がわかるような情報は、なにも教えていないですから。

ここでは、存在しない参照、またはプロパティを指定するとどうなるかを例示するために
わざと「自分の年齢」などという存在しない値を使いました。

以上のように、velでは存在しない参照、またはプロパティを指定すると、
記述した内容がそのまま表示される仕様です。

まとめると、以下の通りです。

  1. $USER.id と書くと、USERという参照のidというプロパティを表示する。
  2. オブジェクトの値を表示するだけなら、他のスクリプト言語とほぼ同じ記法でOK。
  3. 参照またはプロパティが存在しない場合、そのまま出力される。

5 参照の設定

ほとんどのプログラミング言語には、値の代入のための構文が備わっています。
当然のことながらvelにも、代入の構文(代入文)が存在します。

<prog.1>

#set($foo = 1)

この文は、 "foo" という名前の参照に、整数値 1 を代入する という意味になります。

したがって、以下のようなプログラム

#set($one   = 1)
#set($two = 2)
#set($three = 3)

$one $two $thee

の実行結果は、以下の通りとなります。

1 2 3

さて、大抵のプログラミング言語では、代入文は

<prog.2>

foo = 1

などと書けるのですが、velではこれはダメです。
いちいち #set などと書いてやる必要があります。面倒くさいですね。
でもこれには理由があります。

前の章で出てきた Hellow World の例のように、velはプログラム中のプレーンなテキストはそのまま出力します。

よって、ただ単に<prog.2>のように書いても、velはプログラム(この場合は代入文)としては認識しない仕様になっています。
(逆に、 foo = 1 のようなテキストが代入文とみなされてしまうと、
例えばHTMLの属性が全て代入文として認識されてしまい、まともに表示できなくなります)

そこで、<prog.1>のように特別な構文(この場合は #set )を用いることで、ここはプログラムとして解釈してね、
とvelコンパイラに教えてやる必要があるのです。

この「特別な構文」のことを、velでは ディレクティブ と呼びます。
velには、setディレクティブ( #set のことです)以外にもたくさんのディレクティブが存在しますが、
それはこれからの章で徐々に解説していきます。

6 参照(その2)

6.1 フォーマル記法

参照に波括弧を付けることで、参照の区切りを明示的に指定することができます。

#set($foo = 'foo')
#set($foobar = 'FOOBAR')

$foobar ## 'FOOBAR'
${foo}bar ## 'foobar'

プロパティにも使えます。

${_USER.id}  ## 'foo'

6.2 参照のエスケープ

参照のエスケープには、以下の3通りの方法があります。

  1. $$

    #set($foo = 1)

    $foo ## "1"
    $$foo ## "$foo"
    $${foo} ## "${foo}"

    $$$foo ## "$$foo"
    $$$$foo ## "$$$foo"

  2. {$}

    #set($foo = 1)

    $foo ## "1"
    {$}foo ## "$foo"
    {$}{foo} ## "${foo}"

    {$$}foo ## "{$}foo"
    {$$$}foo ## "{$$}foo"

  3. \$
    (Velocityとの互換性のために存在しているのみであり非推奨。
    将来のバージョンでは使えなくなる可能性があります)

    #set($foo = 1)

    $foo ## "1"
    \$foo ## "$foo"
    \${foo} ## "${foo}"

    なお、

    ${foo == 1}

    のような文字列をエスケープする場合は、 \$ は使えません。すなわち

    $${foo == 1}

    {$$}{foo == 1}

    のように書く必要があります。

6.3 クワイエット記法

通常、値が null である参照を表示すると、その部分のコードがそのまま出力されてしまいます。
これを避けるためには、 クワイエット記法 を使用します。

クワイエット記法を使うには、参照の開始を示すシンボル $ の代りに、 $! を使います。

$foo   ## fooは定義されていないので、"$foo"
$!foo ## クワイエット記法を使っているので、なにも出力されない


$foo.bar ## "$foo.bar"
$!foo.bar ## なにも出力されない


$!{foo} ## フォーマル記法だとこうなる

6.4 チェックド記法

$ の代りに $? を使うことで、
ある参照の値が null でないことを強制的に確認できます。

$?foo        ## $fooの値がnullだと、ここで例外が発生する
$?foo.bar() ## 同上


## $fooがnullでないときに、
## $hogeはnullでもよいが、$mogeがnullだとbazが呼ばれる前に例外が発生する
$?foo.baz($hoge, $?moge)

7 コメントについて

velのコメントには、 行コメント と ブロックコメント の2つがあります。

  • 行コメントは ## から開始します。
  • ブロックコメントは、 #* から開始し、 *# で終了します。
    • ブロックコメントの中に、行コメントをネストさせることができます。
    • ブロックコメントの中に、ブロックコメントをネストさせることは できません

以下に、有効なコメントの例を示します。

##行コメント

#*
ブロックコメント
*#

#*
このように、
##ブロックコメントの中に行コメント
をネストできます。
*#

8 実装上の制限と、その回避方法

ここでは、現状のvelコンパイラが不完全なために存在する制限について解説します。

8.1 コメントの制限

ステートメント内にコメントを入れることはできません。

例えば、以下のようなコードはコンパイルエラーになります。

#set(

##$foo = 1 ← コメントがsetディレクティブの中に入っている

$foo = 2
)

この場合、面倒ですが以下のようにしないとダメです。

## #set($foo = 1)  ← setディレクティブごとコメントアウトする

#set($foo = 2)

8.2 参照直後の記号

参照の直後に記号が付く場合、フォーマル記法が必要になることがあります。

例えば、次のコードはコンパイルエラーが発生してしまいます。

#set($foo = 'FOO')

"$foo" ## ここでコンパイルエラー

このエラーは、フォーマル記法を使うことで解消します。

#set($foo = 'FOO')

"${foo}"

また、次のようなコードもエラーになります。

#set($foo, $bar = 'FOO', 'BAR')

#set($foobar = "$foo:$bar") ## ここでコンパイルエラー

$foobar

このような場合も、フォーマル記法を使えばエラーは解消されます。

#set($foo, $bar = 'FOO', 'BAR')

#set($foobar = "${foo}:${bar}")

$foobar

9 velのデータ型

velにはたくさんのデータ型があります。それらを1つずつ解説していきます。

9.1 整数型

整数を表現する型です。
velでは、32bit符号付き整数、または64bit符号付整数を必要に応じて自動的に使いわけます。

最大長64bit、符号付きなので、表現できる値の範囲は

  • -9223372036854775808 (最小値)
  • 9223372036854775807 (最大値)

です。

9.2 小数型

小数を表現する型です。
精度の範囲に上限はありません。

浮動小数点とは異なり、整数どうしの演算(除算以外)をするかぎり精度が失われることはありません。

velでは、 .1 のように、最初のゼロを省略することはできません。きちんと 0.1 と書く必要があります。

#set($very_long_integer = 10000000000000000000000000000000000000000000000000000000000000000) ## 10 ^ 64
#set($very_very_long_integer = $very_long_integer * $very_long_integer)

$very_very_long_integer ## 10 ^ 128


#set($f = 0.1)
#set($very_small_decimal = 0.00000000000000000000000000000000000000000000000000000000000001)
#set($very_large_decimal = 10000000000000000000000000000000000000000000000000000000000000.0)

指数表記を使用することができます。

#set($f1  = 1.234e5)  ## 1.234 * 10 ^ 5

#set($f2 = 1.234e+5) ## このようにも書ける

小数表記を使用することで、整数値を強制的に小数であると認識させることもできます。

#set($d  = 1d)  ## 整数型でなく、小数型の値として扱う

9.3 文字列型

文字列を表現する型です。
velでは、シングルクォートで囲まれた文字シーケンスを文字列と見なします。

  • 改行文字(エスケープ文字ではなく、ホントの改行文字)を含むことができます。
  • 以下のエスケープ文字が有効です。
    1. \n - 改行
    2. \t - タブ
    3. \b - バックスペース
    4. \r - 復帰
    5. \f - 改ページ
    6. \\ - エスケープ文字
    7. \" - ダブルクォート
    8. \' - シングルクォート
    9. \` - バッククォート
  • ユニコードエスケープが使用可能です。

コード例

#set($str0  = 'hoge')

#set($str1 = '') ## 空文字列

#set($str2 = 'hoge
moge') ## "hoge" と "moge" の間に改行がある

#set($str3 = 'hoge\nmoge') ## エスケープ文字で改行を入れる


## シングルクォートの中にダブルクォートをエスケープなしで入れられる
#set($str4 = ' He said, "Stop!" ')


## ユニコードエスケープ
#set($str5 = '\u3042\u3044\u3046\u3048\u304a') ## 「あいうえお」が格納される

さて、他のプログラミング言語では、文字列はダブルクォート文字列で表すことが多いです。

じゃあ、velではダブルクォート文字列はないのか、と言うと、一応ありますが…

しかし ダブルクォート文字列の中に $ (参照の開始文字)、または # (ディレクティブの開始文字)が出現する 場合に、
シングルクォート文字列の場合と動作が大きく異なります。

それじゃどう違うのか、というと、それを理解するにはもう少しvelの知識が必要になります。
よって、ここではとりあえず「velでは、文字列はシングルクォートを使う」ということにしておきます。

ダブルクォート文字列の詳細については、後の章(文字列展開の章)できちんと解説します。

9.4 bool型

正、または偽の2値を表現する値です。

#set($this_is_true  = true)

#set($this_is_false = false)

9.5 null値

ヌル値です。なにも値が設定されていないことを表現します。
Javascript系の言語と異なり、velには undefined はありません。

#set($this_is_null = null)

9.6 リスト

参照の、順番付シーケンスを表現します。

<prog.1>

#set($one_two_three = [1, 2, 3])

#set($foo_bar_baz = ['foo', 'bar', 'baz'])

## 参照をリストに入れることもできる。
##この場合、一つ上の "$nested" と全く同じ要素を持つリストになる。
#set($refs = [ $one_two_three, $foo_bar_baz ])

リストの各要素にアクセスするには、 [] 演算子を使います。
インデックスは、0から開始します。

なお、velでは 不正なインデックスを指定した場合は常に null となります。

#set($list = [1, 2, 3])

$list[0] ## "1"
$list[1] ## "2"
$list[2] ## "3"

$list[3] ## インデックスオーバー。nullとなる。

どうでしょうか。
記法については、普通のプログラミング言語で言うところの「配列」と同じようなものです。

入れ子表記も可能です。入れ子の深さに上限はありません。

#set($nested = [ [1, 2, 3], ['foo', 'bar', 'baz'] ])  ## リストの入れ子

$nested[0][0] ## "1"
$nested[0][1] ## "2"
$nested[0][2] ## "3"
$nested[1][0] ## "foo"
$nested[1][1] ## "bar"
$nested[1][2] ## "baz"

9.6.1 範囲指定リスト

ある一定の範囲を表す整数値のリストを生成するために、
以下の4つの特別な記法が用意されています。

  • ..
  • <..
  • ..<
  • <..<

例えば、上の<prog.1>の $one_two_three は、以下のように書き換えることが可能です。

#set($one_two_three = [1..3])


#set($one = 1)
#set($three = 3)

#set($one_two_trhee_by_ref = [$one..$three]) ## 範囲指定には、参照を使うこともできる

#set($one_two_trhee_by_num_and_ref = [1..$three]) ## これもOK

また、範囲指定には負値を指定できます。

#set($minus1_to_plus1 = [-1..1])  ## [-1, 0, 1]となる

更に、 [$x..$y] としたときに、 $x <= $y を満す必要はありません。
逆にした場合は降順のリストとなります。

#set($plus1_to_minus1 = [1..-1])  ## [1, 0, -1]となる

なお、その他の範囲指定リスト記法はそれぞれ以下の動作をします。

  • ..< は、 範囲終了値が含まれない という点以外は、 .. と同じです。
  • <.. は、 範囲開始値が含まれない という点以外は、 .. と同じです。
  • <..< は、 範囲開始および終了値が含まれない という点以外は、 .. と同じです。

以下に例を示します。

#set($list00 = [0..<5])
#set($list01 = [0..<-5])

#set($list10 = [0<..5])
#set($list11 = [0<..-5])

#set($list20 = [0<..<5])
#set($list21 = [0<..<-5])


$list00 ## "[0, 1, 2, 3, 4]"
$list01 ## "[0, -1, -2, -3, -4]"

$list10 ## "[1, 2, 3, 4, 5]"
$list11 ## "[-1, -2, -3, -4, -5]"

$list20 ## "[1, 2, 3, 4]"
$list21 ## "[-1, -2, -3, -4]"

9.6.1.1 範囲開始値と範囲終了値が同じ場合

この場合、空のリストを生成します。
以下に例を示します。

#set($list0 = [0..0])
#set($list1 = [0<..0])
#set($list2 = [0..<0])
#set($list3 = [0<..<0])


$list0 ## ""
$list1 ## "
"
$list2 ## ""
$list3 ## "
"

9.6.2 リストへの値の設定

普通の言語で言うところの配列への値の設定と同じです。
ただ、不正なインデックスへの値の設定はエラーとなりません。
単に無視します。

#set($hoge = [0, 0, 0])

#set($hoge[0] = 1) ## インデックス0の値として1を設定
#set($hoge[1] = 10) ## インデックス1の値として10を設定
#set($hoge[2] = 100) ## インデックス2の値として100を設定

#set($hoge[3] = 1000) ## インデックスオーバー。無視される


$hoge ## "[1, 10, 100]"と表示される

9.7 マップ

キーとそれに関連付けられた値をペアとして、複数のデータをプロパティとして保持するデータ型です。
一般的にはハッシュとか連想配列と呼ばれているものです。

velでは、あえて「マップ」と呼ぶことにします(その理由は後でわかります)。

キー、値ともに使用できる型に制限はありません。どんな値でもキー/値として使用できます。

もちろん、マップの値としてマップを指定することで、マップの入れ子とすることもできます。
まぁ、リストの時と要領は同じです。

格納された値へのアクセスには、以下の手段があります。

  1. プロパティ形式
  2. 演算子
#set($foo_bar_baz = { 'foo':1, 'bar':2, 'baz':3 })

$foo_bar_baz.foo ## "1"
$foo_bar_baz.bar ## "2"
$foo_bar_baz.baz ## "3"

$foo_bar_baz['foo'] ## "1"
$foo_bar_baz['bar'] ## "2"
$foo_bar_baz['baz'] ## "3"

演算子を使う場合、キーとして指定する値は参照でもOKです。

#set($foo_bar_baz = { 'foo':1, 'bar':2, 'baz':3 })
#set($key_of_1 = 'foo')
#set($key_of_2 = 'bar')
#set($key_of_3 = 'baz')

$foo_bar_baz[$key_of_1] ## "1"
$foo_bar_baz[$key_of_2] ## "2"
$foo_bar_baz[$key_of_3] ## "3"

また、キーとして指定する値が文字列リテラルで、正規表現

[A-z][A-z0-9_]*

にマッチする場合、クォートせずに、以下のように記述することもできます。

#set($foo_bar_baz = { 'foo':1, 'bar':2, 'baz':3 })

$foo_bar_baz[foo] ## "1"
$foo_bar_baz[bar] ## "2"
$foo_bar_baz[baz] ## "3"

以下の例は、いずれも正しくありません。全てコンパイルエラーになります。

#set($one_two_three = { '1one':1, 'the two':2, 'three!':3 })

#foo_bar_baz[1one] ## コンパイルエラー (数字から始まっているからダメ)
#foo_bar_baz[the two] ## コンパイルエラー (スペースを含むからダメ)
#foo_bar_baz[three!] ## コンパイルエラー ("!"を含むからダメ)

入れ子のマップから値を取り出す場合などに、上記のアクセス方法を組み合せて使うことも可能です。
ただし、使う場面はほとんどないと思いますが…

##三重の入れ子マップ
#set($nested = {
'inner1':{
'inner2':{
'foo':1
}
}
})

##以下は、いずれも"1"が表示される

##普通のアクセス方法
$nested.inner1.inner2.foo
$nested[inner1][inner2][foo]


##組み合せられたアクセス方法 (ただし、あまり意味はない)
$nested[inner1].inner2['foo']
$nested.inner1[inner2].foo

9.7.1 マップへの値の動的な設定

マップへの値の設定は、なにも宣言時に限られるわけではありません。
以下に、マップへの値の設定方法例を示します。
要は、アクセスする記法をそのままsetディレクティブの中で使うだけです。

#set($hoge = {}) ## 空のマップはこう書く

#set($hoge.foo = 1) ## キー"foo"の値として1を設定
#set($hoge['bar'] = 2) ## キー"bar"の値として2を設定
#set($hoge[baz] = 3) ## キー"baz"の値として3を設定

$hoge ## "{foo:1, bar:2, baz:3}"と表示される

入れ子のマップに対しても値を設定できます。

#set($hoge = {
'foo':{},
'bar':{}
})

#set($hoge.foo.a = 1)
#set($hoge.foo.b = 2)
#set($hoge.bar.a = 3)
#set($hoge.bar.b = 4)

$hoge ## "{foo:{a:1, b:2}, bar:{a:3, b:4}}"

なお、存在しないマップに対して値を設定しようとしても無視されます。

#set($hoge = {'foo':{}})

#set($hoge.foo.a = 1)
#set($hoge.foo.b = 2)
#set($hoge.bar.a = 3)
#set($hoge.bar.b = 4)

$hoge

このコードの実行結果は以下の通りです。

#set($hoge.bar.a = 3)  ← これらのコードは無視されたので、そのまま出力されてしまう
#set($hoge.bar.b = 4) ←

{foo:{a:1, b:2}}

9.7.2 マップの宣言記法について

さて、実は、文字列型キーのクォート省略は、 [] 演算子だけでなくマップの宣言時にも使えます。

#set($foo_bar_baz = { foo:1, bar:2, baz:3 })

こっちの方が、クォート文字がないので読むのも書くのもラクです。

本ドキュメントでは、以下、最もタイプ量の少ない記法を使うことにします。
すなわち、

  • プロパティの取得、設定は $foo.bar 形式
  • マップの宣言は、 {foo:'bar'} 形式

です。

10 演算子

velで使える演算子のセットは、大抵のプログラミング言語と同等のものですが、
vel独自のものもいくつか存在します。

10.1 演算子の一覧

算術演算子には、以下のものがあります。
優先順位の高いものから順に挙げています。

なお、例えば(1)の ! , not のように演算子が2つ登場している場合、
これは not ! エイリアスだという意味です。
つまりどちらを使っても同じ意味になります。

  1. ! , not (not)
  2. - , negate (nagate)
  3. * , mul (multiply)
  4. / , div (divide)
  5. % , mod (modulus)
  6. + , add (add)
  7. - , sub (subtract)
  8. < , lt (less than)
  9. <= , le (less equal)
  10. > , gt (greater than)
  11. >= , ge (greater equal)
  12. == , eq (equal)
  13. != , ne (not equal)
  14. same (same pointer)
  15. isnt (type is not)
  16. is (type is)
  17. =~ , matches (regular expression match)
  18. has (contains value)
  19. & (operational and)
  20. | (operational or)
  21. && , and (logical and)
  22. || , or (logical or)
  23. ^^ , xor (logical xor)
  24. =>> , applies (function application)
  25. ; , default (default value)
  26. asserts (inline assertion)
  27. [cond] then [if cond true] else [if cond false] (ternary operation)

10.2 全ての演算子に共通の性質

  1. いくつかの二項演算子は、特定の値の組み合わせであることが要求されます。
    正しい値の組を指定しなかった場合、ほとんどの場合は false または null を返します。
  2. 特に記述がない限り、二項演算子の左辺に null を指定した場合、計算結果は以下の通りとなります。
    • 参照を返す演算の場合は null

      つまり、 null に対してどんな演算を行なっても null になります。

    • booleanを返す演算の場合は false

      つまり、 null との論理演算の結果は常に false になります。

  3. booleanを要求する演算子は、以下の値のみを true であると見なします。
    • boolean値 true
    • 文字列 t (ケース非依存)
    • 文字列 true (ケース非依存)
    • 数値 0 以外の任意の数値
    • 文字列 数値文字列で、 '0' 以外のもの

10.3 一般的な演算子

次の演算子は、大抵のプログラミング言語には存在している演算子です。
これらは、velでも同様の意味を持っています。

  • ! : 否定 (単項演算子)
  • - : 符号反転 (単項演算子)
  • * : 乗算
  • / : 除算
  • % : 剰余算
  • + : 加算
  • - : 減算
  • < : 縲恂「満
  • <= : 縲怦ネ下
  • > : 縲怩謔闡蛯ォい
  • >= : 縲怦ネ上
  • == : 等しい
  • != : 等しくない

ただし、特定の値の組合せの場合には、特別な演算結果を生成することがあります。
以下、特定の組合せについて説明していきます。

10.3.1 演算子 * , / , + , - の特例

左辺が数値、右辺が文字列の場合、
右辺を数値に変換してから、通常の算術演算を行ないます。
(この性質を使うことで、文字列から数値への型変換が簡単にできます)

#set($num = 1 + '2')

$num ## "3"を表示

#set($num = 1 + 'abc')

$num ## "abc"は数値に変換不可能。よって "1 + 0" と同じであると見なされるため "1" を表示


#set($foo = '42')
#set($bar = 0 + $foo) ##"$foo"を数値に変換。すなわち"$bar"は整数値"42"

10.3.2 演算子 * の特例

  1. 左辺が文字列、右辺が整数値の場合

左辺の文字列を、右辺の数値回数分繰り返した文字列を生成します。

#set($foo3 = 'foo' * 3)

$foo3 ## "foofoofoo"

  1. 左辺がリスト、右辺が整数値の場合

左辺のリストを、右辺の数値回数分連結した新しいリストを生成します。

#set($list3 = [1, 2, 3] * 3)

$list3 ## "[1, 2, 3, 1, 2, 3, 1, 2, 3]"

10.3.3 演算子 % の特例

  1. 左辺が文字列、右辺がリストの場合

左辺をテンプレート、右辺を値としてprintfフォーマットした結果の文字列を生成します。(pythonと同様)

#set($printf_sample = '%d is a number, and %s is a string.' % [100, 'foo'])

$printf_sample ## "100 is a number, and foo is a string."

10.3.4 演算子 + の特例

  1. 左辺が文字列、右辺は任意の型

左辺と、 右辺の文字列表現 を連結した文字列を生成します。

  1. 左辺がリスト、右辺もリスト

二つのリストを連結した、新しいリストを生成します。

#set($two_lists = [1, 2] + [3, 4])

$two_lists ## "[1, 2, 3, 4]"

10.3.5 演算子 - の特例

  1. 左辺が文字列、右辺は任意の型

左辺の文字列に、 右辺の文字列表現 が含まれる場合、そのうち最も先頭に近いものを削除した文字列を生成します。

#set($barbaz1 = 'foobarbazfoobarbaz' - 'foo')

$barbaz1 ## 左辺から、"foo"を一つ削除した文字列。すなわち "barbazfoobarbaz"


#set($barbaz2 = 'foobarbazfoobarbaz' - 'foo' - 'foo')

$barbaz2 ## 左辺から、"foo"を2つ削除した文字列。すなわち "barbazbarbaz"


#set($barbaz3 = 'foobarbazfoobarbaz' - 'foo' - 'foo' - 'foo')

$barbaz3 ## "foo"は2つしかないので、3回以上引き算しても変化なし。すなわち "barbazbarbaz"

  1. 左辺がリスト、右辺もリスト

左辺のリストの要素から、右辺のリストの要素を先頭から順に削除した新しいリストを生成します。

#set($list = [1, 1, 1, 2, 2, 3, 2, 1] - [1, 1, 2])

$list ## "1"を2つ、"2"を1つ削除したので、残りは"[1, 2, 3, 2, 1]"

10.3.6 演算子 < , <= , > , >= の特例

以下のように、ある値に対する複数個の比較式を、一つにまとめて記述することが可能です。

#set($two = 2)

##大抵のプログラミング言語の場合、
##ここは "0 <= $two && $two < 3" などと書く必要がある
#set($less_than_3 = (0 <= $two < 3))

$less_than_3 ## "true"

10.4 演算子 =>>

この演算子は、二項演算子です。リスト、マップまたはイテレータを返します。

この演算子は、

という組み合せでなくてはなりません。

この演算子を理解するには、もう少しvelについての知識が必要になるため、
「関数についての高度なトピック」の章で詳しく解説することとし、ここでは解説しません。

10.5 演算子 ;

この演算子は、二項演算子です。参照を返します。
左辺、右辺ともに任意の型の値を指定できます。

この演算子は、

  • 左辺が null ない ならば左辺の値自身
  • 左辺が null の場合は、右辺の値

を返します。

この演算子は、三項演算子の特別なバージョンと考えることができます。
デフォルト値を設定したい場合などに使います。

## $augendと$addendを足し算する。
## でも、$fooがnullの場合、計算結果がおかしくなってしまうので、
## $fooがnullの場合は0をデフォルト値として設定する。
#set($augend = $foo ; 0)

#set($addend = 1)


#set($add_result = $augend + $addend)

10.6 演算子 asserts

この演算子は、二項演算子です。参照を返します。
左辺、右辺ともに任意の型の値を指定できます。

この演算子は、以下の動作を行ないます。

  • 右辺が true である場合は左辺の値を返します
  • 右辺が false である場合はエラーとなり、スクリプトの実行が直ちに終了します

この演算子は、インラインのアサーション構文を提供します。

例えば次のように記述することで、 $bar の値が 0 以上であることを確認した上で
$foo に値を設定できます。

#set($foo = ($bar asserts (0 <= $bar)) )

10.6.1 演算子 same

この演算子は、二項演算子です。boolean値を返します。

  • 2つの参照の参照先が同じオブジェクトを指しているなら true
  • そうでないなら false

つまり、ポインタの比較です。

#set($list0 = [0, 1, 2])
#set($list1 = $list0)
#set($list2 = [0, 1, 2])

#set($list0_same_list1 = ($list0 same $list1))
#set($list0_same_list2 = ($list0 same $list2))

$list0_same_list1 ## "true"
$list0_same_list2 ## "false"

10.7 演算子 is , isnt

演算子 is は、二項演算子です。boolean値を返します。

10.7.1 左辺は任意の型、右辺に文字列型の値

  • 左辺の値の型が、右辺の文字列で指定された型に一致するなら true

コード例

#set($foo = 'foo')
#set($bar = 42)

#set($foo_is_string = ($foo is 'string') )
#set($foo_is_number = ($foo is 'number') )
#set($bar_is_string = ($bar is 'string') )
#set($bar_is_number = ($bar is 'number') )

$foo_is_string ## "true"
$foo_is_number ## "false"

$bar_is_string ## "false"
$bar_is_number ## "true"

10.7.2 左辺は任意の型、右辺は null

  • 左辺の値が null なら true

コード例

#set($foo = null)
#set($bar = 42)

#set($foo_is_null = ($foo is null) )
#set($bar_is_null = ($bar is null) )
#set($baz_is_null = ($baz is null) ) ##定義されていない参照は、常にnullであることに注意

$foo_is_null ## "true"
$bar_is_null ## "false"
$baz_is_null ## "true"

10.7.3 左辺は特定の型、右辺は 'empty'

  • 以下のケースに該当する場合に true
    1. 左辺が空文字列
    2. 左辺が空リスト
    3. 左辺が空マップ

10.7.4 その他、右辺に指定可能な文字列

  • 本ドキュメントで、既に登場済みの型
    • 'string' → 文字列型
    • 'number' → 整数型
    • 'decimal' → 小数型
    • 'list' → リスト型
    • 'map' → マップ型
    • 'boolean' → ブーリアン
  • 未登場の型
    • 'bytes' → バイト配列型
    • 'date' → 日付型
    • 'function' → 関数型
    • 'template' → テンプレート型
    • 'functable' → 関数テーブル型
    • 'builtin_obj' → ビルトインオブジェクト型
    • 'object' → オブジェクト型

10.7.5 isnt について

演算子 isnt (isn'tの意味) は、 is の否定です。すなわち、
not ($x is 'yyy') という式と、 $x isnt 'yyy' は等価です。

#set($foo = 'foo')

#set($not_number1 = not ($foo is 'number'))
#set($not_number2 = $foo isnt 'number')

$not_number1 ## "true"
$not_number2 ## "true"

10.8 演算子 =~

この演算子は、二項演算子です。boolean値を返します。
左辺、右辺ともに文字列型の値を指定します。

正規表現マッチングを行ない、その結果をbooleanで返します。
この時、左辺はマッチング対象文字列、右辺は正規表現パターンです(Rubyのように左右逆転はできません)。

使用している正規表現エンジンは、JDK標準添付のものです。
従って、扱える正規表現JDKがサポートしているものになります。

#set($foo = 'abc123cdf')

#set($match_result = ($foo =~ '^[a-z]+[0-9]+[a-z]+$'))

$match_result ## "true"

また、指定する正規表現パターンには、オプションを指定できます。
オプションを指定する場合、正規表現パターンは

/[regex pattern]/[options]

のように記述します。
options に指定できるオプションは、JDK正規表現における 埋め込みフラグ として指定できるものです。

埋め込みフラグとして使用できるものは、以下の通りです。
詳しくは、JDKドキュメントを参照してください。

  • m : メタキャラクタ ^ $ が、入力文字列の先頭と末尾でなく、行頭と行末にマッチするようになります。
  • i : 大文字小文字の別を無視します。
  • s : メタキャラクタ . (ドット)が、改行文字にもマッチするようになります。
  • d : UNIX改行文字(LF)のみを改行文字として認識するようになります。
  • u : オプション i を指定した際に、ユニコード文字も対象に含めるようにします。
  • x : パターン内の空白文字を無視し、コメントを入れることを許可します。

以下に、オプション指定したマッチングの例を示します。

#set($foo = 'abc123CDF')

#set($match_result1 = ($foo =~ '^[A-z0-9]+$'))
#set($match_result2 = ($foo =~ '(?i)^[a-z]+[0-9]+[a-z]+$')) ## '(?i)' はJDK正規表現でサポートされている埋め込みオプション

$match_result1 ## "true"
$match_result2 ## "true"

なお、正規表現perl風に記述することもできます。

#set($foo = 'abc123CDF')

#set($match_result1 = ($foo =~ '/^[A-z0-9]+$/'))
#set($match_result2 = ($foo =~ 'm/^[a-z]+[0-9]+[a-z]+$/i')) ## 'm'を付けてもよい

$match_result1 ## "true"
$match_result2 ## "true"

ただし、正規表現リテラルはサポートされていません。
すなわち、

#if( $foo =~ /^[A-z0-9]+$/ )

のように記述することはできません。

また、perl風の記法でサポートしているのはマッチングだけです。
置換( s/.../.../ , tr/.../.../ )は使えません。

10.9 演算子 has

この演算子は、二項演算子です。boolean値を返します。

左辺は、

  • 文字列
  • リスト
  • マップ

のどれかです。右辺は、任意の型を指定できます。

10.9.1 左辺が文字列の特例

  • 左辺の文字列中に、 右辺の文字列表現が含まれている なら true
  • それ以外なら false

#set($foobar = 'foobar')

#set($has_foo = ($foobar has 'foo') )
#set($has_bar = ($foobar has 'bar') )
#set($has_baz = ($foobar has 'baz') )

$has_foo ## "true"
$has_bar ## "true"
$has_baz ## "false"

10.9.2 左辺がリストの特例

  • 左辺のリストが、右辺の値に等しい要素を1つ以上含んでいるなら true
  • それ以外なら false

#set($one_two = [1, 2])

#set($has_one = ($one_two has 1) )
#set($has_two = ($one_two has 2) )
#set($has_three = ($one_two has 3) )

$has_one ## "true"
$has_two ## "true"
$has_three ## "false"

10.9.3 左辺がマップの特例

  • 左辺のマップが、右辺で示されたキーを保持しているなら true
  • それ以外なら false

#set($foobar = {foo:1, bar:2})

#set($has_foo = ($foobar has 'foo') )
#set($has_bar = ($foobar has 'bar') )
#set($has_baz = ($foobar has 'baz') )

$has_foo ## "true"
$has_bar ## "true"
$has_baz ## "false"

10.10 演算子 &

この演算子は、二項演算子です。
いくつかの値の組み合わせがあります。その組み合わせにより、計算結果の型が異なります。

10.10.1 左辺、右辺ともに数値

左辺、右辺の両方を64bit符号付き整数値に変換した上で、bit積を計算します。

#set($bit_and = 7 & 14)

$bit_and ## "6"

10.10.2 左辺、右辺ともにboolean

左辺と右辺の論理積を計算します。

10.10.3 左辺、右辺ともにリスト

左辺、右辺ともにリストの場合、
右辺と左辺のリストのどちらにも含まれている要素だけを含む、新しいリスト を返します。

つまり、左辺と右辺のリストを集合とみて、その集合の積を取得するということです。
また、以下の性質があります。

  • リスト内で重複している要素が存在する場合、最初の要素だけが対象となり、残りは無視します。
  • 計算結果のリストにおける要素の順序は、 右辺のリストにおける順序 を適用します。

#set($fib_even = [1, 2, 3, 5, 8, 13] & [12, 10, 8, 6, 4, 2, 0])

$fib_even ## [8, 2]


#set($empty = [0..10] & )

$empty ## ""

10.10.4 左辺、右辺ともにマップ

左辺、右辺ともにマップの場合、以下の計算結果を生成します。

  1. まず、 左辺と右辺のマップのどちらにも含まれているキー を抽出し、
  2. 抽出されたキーの値を、 右辺のマップから取り出して 新しいマップを作成する

つまり、左辺と右辺、それぞれのマップが保持するキー集合の積を取得するということです。

#set($map = {foo:1, bar:2, baz3:, qux:4} & {foo:'FOO', baz:'BAZ', hoge:'HOGE', moge:'MOGE'})

$map ## "{foo:'FOO', baz:'BAZ'}"

10.10.5 左辺がマップ、右辺がリスト

以下の計算結果を生成します。

  1. まず、 左辺のキー集合と右辺のリストのどちらにも含まれている値 を抽出し、
  2. 抽出された値をキーとみなし、左辺のマップから値を取り出して新しいマップを生成する

つまり、左辺のマップに対し、右辺のリストで(キー集合とみなして)フィルタを掛けるということです。

10.10.6 左辺がリスト、右辺がマップ

左辺と右辺を入れ替えた上で、再度 & 演算子を適用します。

10.10.7 上記以外のケース

例外をスローします。

10.11 演算子 |

この演算子は、二項演算子です。
演算子 & と同様に、いくつかの値の組み合わせが存在します。

10.11.1 左辺、右辺ともに数値

左辺、右辺の両方を64bit長整数値に変換した上で、bit和を計算します。

#set($bit_or = 7 & 14)

$bit_or ## "15"

10.11.2 左辺、右辺ともにboolean

左辺と右辺の論理和を計算します。

10.11.3 左辺、右辺ともにリスト

左辺、右辺ともにリストの場合、
右辺と左辺のリストのどちらかに含まれている要素を含む、新しいリスト を返します。

つまり、左辺と右辺のリストを集合とみて、その集合の和を取得するということです。
また、以下の性質があります。

  • リスト内で重複している要素が存在する場合、最初の要素だけが対象となり、残りは無視します。
  • 計算結果のリストにおける要素の順序は、以下の法則に従います。
    1. 左辺に含まれている要素は、右辺に含まれている要素より先に出現する
    2. 左辺に含まれている要素ならば、左辺のリストの順序
    3. 左辺には含まれておらず、右辺のみに含まれている要素ならば、右辺のリストの順序

#set($or_list = [2, 4, 6, 8, 10, 12] | [3, 6, 9, 12, 15])

$or_list ## "[2, 4, 6, 8, 10, 12, 3, 9, 15]"

10.11.4 左辺、右辺ともにマップ

+ 演算子を適用した場合と同一の結果(マップの足し算)を生成します。

10.11.5 左辺がマップ、右辺がリスト

右辺のリストの要素のうち、左辺のマップのキーとなっていないものが、
キーとして追加(値は null )された新しいマップを返します。

まぁ、あまり役に立たないので使うことはないでしょう。

#set($filtered = {foo:1, baz:3} | ['foo', 'bar', 'baz'])

$filtered ## "{foo:1, bar:null, baz:3}"

10.11.6 左辺がリスト、右辺がマップ

左辺と右辺を入れ替えた上で、演算子 | を再度適用します。

これも、あまり役に立ちません。

10.11.7 上記以外のケース

例外をスローします。

10.12 演算子 && , || , ^^

通常の論理演算子と同一です。よって解説は省略します。

10.13 演算子 [cond] then [if cond true] else [if cond false]

これは三項演算子です。

普通の言語では、 [cond] ? [if cond true] : [if cond false] と書くヤツです。

動作的には普通の三項演算子と同一なので、解説は省略します。