tech

[English] [Japanese]

06. match

LuneScript supports algebraic data types and pattern matching.

Algebraic data type (alge type)

enum types can group int, real, or str values to limit their range.

Algebraic data types, on the other hand, are a more general version of enum types, and can group all types, not just int, real, and str.

Algebraic data types are declared with the alge keyword.

For example:

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

This example declares an alge type Test .

Test has a range of values from Val1 to Val5. Also, Val1 to Val5 each have the following parameters.

value parameter
Val1 none
Val2 int
Val3 str
Val4 Hoge
Val5 int, str

There are no restrictions on parameter types. In this example, Val4 has a Hoge class type as a parameter.

There is no limit on the number of parameters. In this example, Val5 has int and str as parameters.

There are two ways to specify parameters, one is to specify the type only, and the other is to specify the name and type. In this example, Val2 to Val4 specify only the type, and Val5 specifies the parameter name and type. The parameter name only has the effect of clarifying the meaning of the parameter.

If you want to use this alge type value, write:

// @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" );

The meaning of each is as follows.

  • val1 is Test.Val1
  • val2 is Test.Val2 with 1 as parameter
  • val3 is Test.Val3 with "abc" as a parameter
  • val4 is Test.Val4 with new Hoge( 100 ) as a parameter
  • val5 is 10, Test.Val5 with "xyz" as parameter

match

The enum type can be used like the original value like this:

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

In this example, the value of TestEnum type + 100 is used in function func(), which indicates that TestEnum type can also be used as an int number.

Values of type alge, on the other hand, require special handling. That process is match.

Here is an example 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

In this example, we are doing a match inside the func() function.

Val1 to Val5 are branched by case. Also, Val2 to Val5 declare variables that receive parameters.

For example, func( Test.Val2( 1 ) ) passes Test.Val2( 1 ) to func(). Now matches case .Val2( x ) in match inside func(). Then x is set to 1 and print( test.$_txt, x ) is executed.

Here test.$_txt is expanded to the string "Test.Val2" which indicates the range Val2.

match can use default , _default and _match like switch .

shorthand notation for alge type

The alge type can also use shorthand notation in the same way as the enum type.

Given the following alge type Test and a function with that Test as an argument,

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

You can omit Test when calling func(), like this:

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

Note that if you omit the alge type defined in an external module, you must import that external module.

comparison of alge type

Simple alge values with no parameters can be compared like this:

// @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

Note that a value with a parameter (Val3 in the above case) will be a different value even if the same parameter is given. By the way, above test == .Val3(1) will be an error. This is because the result of this expression is always "false", so a compile error prevents unintended results at runtime.

Examples of using alge

With alge you can write the JSON structure like this:

// @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" ) } ) );

You can do the same by using casts, inheritance, etc. without using alge. But casting, of course, and inheritance also have drawbacks.

alge isn't a panacea either, but I think it's a better option than using casts and inheritance in some situations.

Next time I will explain the interface.