公開技術情報

[English] [Japanese]

06. match 編

LuneScript は、代数的データ型とパターンマッチをサポートします。

代数的データ型 (alge型)

enum 型は、 int, real, str のいずれかの値をグルーピングして、値域を制限できます。

一方、代数的データ型は enum 型をより一般的にしたもので、 int, real, str に限らず全ての型をグルーピングすることが出来ます。

代数的データ型は alge キーワードで宣言します。

次に例を示します。

// @lnsFront: ok
class Hoge {
   pri let val:int {pub};
}
alge Test {
   Val1,
   Val2( int ),
   Val3( str ),
   Val4( Hoge ),
   Val5( num:int, txt:str ),
}

この例では、 alge 型 Test を宣言しています。

Test は、 Val1 〜 Val5 までの値域を持ちます。 また、 Val1 〜 Val5 はそれぞれ次のパラメータを持ちます。

パラメータ
Val1 なし
Val2 int
Val3 str
Val4 Hoge
Val5 int, str

パラメータの型に制限はありません。 この例の場合、 Val4 は Hoge クラス型をパラメータに持ちます。

パラメータの個数も制限はありません。 この例の場合、 Val5 は int と str をパラメータに持ちます。

パラメータには、型だけ指定する方法と、名前と型を指定する方法があります。 この例の場合、 Val2 から Val4 は型だけ指定し、 Val5 はパラメータ名と型を指定しています。 パラメータ名は、そのパラメータの意味を明瞭にする効果があるだけです。

この alge 型の値を使用する場合、次のように書きます。

// @lnsFront: skip
let val1 = Test.Val1;
let val2 = Test.Val2( 1 );
let val3 = Test.Val3( "abc" );
let val4 = Test.Val4( new Hoge( 100 ) );
let val5 = Test.Val5( 10, "xyz" );

それぞれの意味は次になります。

  • val1 は Test.Val1
  • val2 は 1 をパラメータに持つ Test.Val2
  • val3 は "abc" をパラメータに持つ Test.Val3
  • val4 は new Hoge( 100 ) をパラメータに持つ Test.Val4
  • val5 は 10, "xyz" をパラメータに持つ Test.Val5

match

enum 型は、次のように元の値と同じように使用できます。

// @lnsFront: ok
enum TestEnum {
  val0,
  val1,
  val2,
}
fn func( val:TestEnum ): int {
   return val + 100;
}
let val = 1;
func( unwrap TestEnum._from( val ) );

この例では、 関数 func() 内で TestEnum 型の値 + 100 をしていますが、 これは TestEnum 型が int の数値としても利用できることを示しています。

一方で alge 型の値は、特別な処理が必要です。 その処理が match です。

次に match の例を示します。

// @lnsFront: ok
class Hoge {
   pri let val:int {pub};
}

alge Test {
   Val1,
   Val2( int ),
   Val3( str ),
   Val4( Hoge ),
   Val5( int, str ),
}

fn func( test:Test ) {
   match test {
      case .Val1 {
         print( test.$_txt );
      }
      case .Val2( x ) {
         print( test.$_txt, x );
      }
      case .Val3( x ) {
         print( test.$_txt, x );
      }
      case .Val4( x ) {
         print( test.$_txt, x.$val );
      }
      case .Val5( x, y ) {
         print( test.$_txt, x, y );
      }
   }
}

func( Test.Val1 ); // Test.Val1
func( Test.Val2( 1 ) ); // Test.Val2 1
func( Test.Val3( "abc" ) ); // Test.Val3 abc
func( Test.Val4( new Hoge( 100 ) ) ); // Test.Val4  100
func( Test.Val5( 10, "xyz" ) ); // Test.Val5 10 xyz

この例では、 func() 関数内で match を実行しています。

case によって Val1 〜 Val5 を分岐しています。 また Val2 〜 Val5 は、それぞれパラメータを受け取る変数を宣言しています。

例えば func( Test.Val2( 1 ) ) は、 Test.Val2( 1 )func() に渡しています。 ここで func() 内の match の case .Val2( x ) にマッチします。 そして、 x には 1 がセットされ、 print( test.$_txt, x ) が実行されます。

ここで test.$_txt は、 値域の Val2 を示す "Test.Val2" の文字列が展開されます。

match は、 switch と同様に default と _default、そして _match を利用できます。

alge 型の省略表記

alge 型も enum 型と同じように省略表記を利用できます。

次のような alge 型の Test と、その Test を引数に持つ関数があった場合、

// @lnsFront: ok
alge Test {
   Val1,
   Val2,
   Val3( int ),
}
fn func( test:Test ) {
   print( test );
}

func() をコールする際、次のように Test を省略可能です。

// @lnsFront: skip
func( .Val1 );
func( .Val2 );
func( .Val3( 10 ) );

なお、 外部モジュールで定義されている alge 型を省略指定する場合、 その外部モジュールを import している必要があります。

alge型 の比較

パラメータを持たない単純な alge 型の値は、次のように比較することが出来ます。

// @lnsFront: error
alge Test {
   Val1,
   Val2,
   Val3( int ),
}
fn func( test:Test ) {
   if test == .Val1 {
      print( "Val1" );
   }
   elseif test == .Val2 {
      print( "Val2" );
   }
   elseif test == .Val3(1) {  // error
      print( "Val3" );
   }
   else {
      print( "no" );
   }
}
func( .Val1 ); // Val1
func( .Val2 ); // Val2

なお、パラメータを持つ値 (上記の場合 Val3) は、 同じパラメータを与えても異なる値になるため注意が必要です。 ちなみに、上記の test == .Val3(1) はエラーとなります。 なぜならば、この式の結果は必ず「false」となるため、 コンパイルエラーとすることで、実行時に意図しない結果になることを防ぎます。

alge の使用例

alge を使うと JSON 構造を次のように書くことが出来ます。

// @lnsFront: ok
alge JsonVal {
   JNull,
   JBool(bool),
   JInt(int),
   JReal(real),
   JStr(str),
   JArray(List<JsonVal>),
   JObj(Map<str,JsonVal>),
}
fn dumpJson( stream:oStream, jval:JsonVal ) {
   match jval {
      case .JNull {
         stream.write( "null" );
      }
      case .JBool( val ) {
         stream.write( "%s" (val) );
      }     
      case .JInt( val ) {
         stream.write( "%d" (val ) );
      }        
      case .JReal( val ) {
         stream.write( "%g" (val ) );
      }        
      case .JStr( val ) {
         stream.write( '"%s"' (val ) );
      }        
      case .JArray( list ) {
         stream.write( "[" );
         foreach val, index in list {
            if index > 1 {
               stream.write( "," );
            }
            dumpJson( stream, val );
         }
         stream.write( "]" );
      }        
      case .JObj( map ) {
         stream.write( "{" );
         let mut cont = false;
         foreach val, key in map {
            if cont {
               stream.write( ',' );
            }
            else {
               cont = true;
            }
            stream.write( '"%s":' (key) );
            dumpJson( stream, val );
         }
         stream.write( "}" );
      }
   }
}
dumpJson( io.stdout,
          JsonVal.JObj( { "foo": JsonVal.JInt( 1 ),
                          "bar": JsonVal.JStr( "abc" ) } ) );

alge を使用せずにキャストや継承などを利用すれば同じことは出来ます。 しかし、キャストはもちろん、継承にも欠点があります。

alge も万能ではありませんが、 幾つかの場面ではキャストや継承を使用するよりも、 より良い選択肢になると思います。

次回はインタフェースを説明します。