tech

[English] [Japanese]

11. nilable

This time I will explain nilable in LuneScript.

What is nilable

LuneScript has a value of nil.

As mentioned earlier, variables of type stem can hold any type except nil .

// @lnsFront: error
let mut val:stem = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];
val = nil; // error

So how can we keep nil ? Use stem! instead of stem .

// @lnsFront: ok
let mut val:stem! = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];
val = nil; // ok

A type that can hold nil is called nilable.

nilable is nilable type not only stem! but all types except some.

For example, int! and str! are nilable types of int and nilable types of str .

By adding ! to the end of the type name, it becomes a nilable type that can hold nil as well as the original type.

// @lnsFront: error
let mut val1:int = 1;
val1 = nil;  // error

let mut val2:int! = 1;
val2 = nil;  // ok

nil is a value that is distinct from all other values and is a useful value to use as an outlier. However, there are many cases where the value of a variable becomes nil at unintended timing, causing problems.

LuneScript achieves nil safety (NULL safety) by dividing types into nilable types that can hold nil and non-nilable types that cannot hold nil .

Restrictions on nilable types

A nilable type has the restriction that it cannot be used as the original type as is.

I think it's hard to understand what I'm talking about with this explanation, so please take a look at the following example.

// @lnsFront: error
let val:int! = 1;
print( val + 1 );  // error

val above is a nilable type of int. And I'm running val + 1 which gives an error.

Because val is int!, not int, so it cannot be treated as int as it is.

The reason why nilable has such restrictions is as follows.

  • nilable is a type that can hold nil
  • i.e. nilable types can be nil
  • To use it as the original type, you must check that it is not nil

A non-nilable type cannot be assigned nil. And the nilable type cannot be used as the original type as it is.

This restriction logically prevents a variable from becoming nil at an unintended timing and causing a bug.

This is the nil-safety (NULL-safety) principle adopted by many languages.

Comparing with nilable types

As mentioned earlier, nilable types are not directly usable as non-nilable.

But you can compare:

// @lnsFront: ok
fn check( val:int! ) {
   if val == 1 {
      print( "ok" );
   }
   else {
      print( "ng" );
   }
}
check( 1 ); // ok
check( 2 ); // ng

In the above example, val is int! and val == 1 is compared with int. It is possible to compare nilable and non-nilable like this.

Converting from a nilable type to a non-nilable type

Conversions from non-nilable types to nilable types are implicit:

// @lnsFront: ok
let val:int! = 1;   // int! <-- int

On the other hand, conversions from nilable types to non-nilable types must be done explicitly.

LuneScript provides the following conversions from nilable types to non-nilable types:

  • unwrap
  • when!
  • if!
  • if! let
  • let!
  • unwrap!

unwrap

unwrap converts an expression of type nilable to a non-nilable type.

For example:

// @lnsFront: ok
let val1:int! = 1;
let val2:int = unwrap val1;

val1 is int! in this example. By unwrapping that val1, we are converting from int! to int.

If the unwrap value is nil as shown below, the program will generate an error at runtime.

// @lnsFront: ok
let val1:int! = nil;
let val2:int = unwrap val1;   // runtime error

The unwrap default prevents this run-time error. unwrap default specifies the value if the value being converted is nil.

Here is an example using default :

// @lnsFront: ok
let val1:int! = nil;
let val2:int = unwrap val1 default 0;

In this example, val1 is nil, so 0 in default is the result of unwrap evaluation.

Only use unwrap without a default when you know for sure that it's not nil.

when!

when! determines whether the specified nilable type variable is nil and branches.

Here is an example of when!

// @lnsFront: ok
fn func( val:int!, val2:int! ): int {
   when! val, val2 {
      return val + val2;
   }
   else {
      return 0;
   }
}
print( func( 1, 2 ) );      // 3
print( func( nil, 2 ) );    // 0
print( func( 1, nil ) );    // 0
print( func( nil, nil ) );  // 0

In this example, when! branches for int! type val and val2.

  • If val and val2 are non-nil, do return val + val2
  • if val or val2 is nil, do return 0

when! executes the first block when all specified variables are non-nil.

Within this block, the following behavior occurs:

  • The specified variable will be of unwrapped non-nilable type.
  • Specified variables are immutable.

If any of the variables specified in when! is nil, execute the else block. else is optional.

Note that when! can only be variables. You cannot write members or expressions.

if!

if! determines whether the given expression is nil and branches.

Here is an example of if!

// @lnsFront: ok
fn func( val:int! ): int! {
   return val;
}
fn sub( val:int! ): int {
   if! func( val ) {
      return _exp + 10;
   }  
   else {
      return 0;
   }
}
print( sub( 1 ) ); // 11
print( sub( nil ) ); // 0

In this example, if! is branched to func().

  • If func() is non-nil, do return _exp + 10;
  • If func() is nil, do return 0

if! executes the first block when the specified expression is non-nil. Inside this block, you can access the result of the expression as _exp. Then _exp is of non-nilable type.

If the expression given to if! is nil, execute the else block. else is optional.

By the way, if the expression specified with if! returns multiple values, only the first return value is considered. Ignore the second and subsequent return values.

Note that if! cannot be nested like this:

Because the _exp of the inner if! is shadowed by the _exp of the outer if!.

// @lnsFront: skip
   if! func( val ) {
      if! func( val ) {
         return _exp + 10;
      }  
      else {
         return 0;
      }  
   }

To prevent this, use the following if! let .

if! let

if! let is a version that allows you to specify a variable name to store the result of the expression tested by if!.

Here is a sample if! let .

// @lnsFront: ok
fn func( val1:int!, val2:int! ): int!, int! {
   return val1, val2;
}
fn sub( val1:int!, val2:int! ): int {
   if! let work1, work2 = func( val1, val2 ) {
      return work1 + work2;
   }  
   else {
      return 0;
   }
}
print( sub( 1, 2 ) ); // 3
print( sub( nil, 2 ) ); // 0
print( sub( 1, nil ) ); // 0
print( sub( nil, nil ) ); // 0

In this example we are running if! let work1, work2 = func( val1, val2 ).

This assigns the result of func() to work1, work2 and executes the first block if all are non-nil. You can access work1, work2 inside this block. work1, work2 are non-nilable types. The scope of variables declared with if! let is the first block.

If either is nilable, execute the else block. else is optional.

let!

let! declares variables with non-nil initial values.

Here is a sample let!

// @lnsFront: ok
fn func( val1:int!, val2:int! ): int!, int! {
   return val1, val2;
}
fn sub( val1:int!, val2:int! ): int {
   let mut work0 = 0;
   let! work1, work2 = func( val1, val2 ) {
      work1 = 0;
      work2 = 0;
   }
   then {
      work0 = 10;
   };     
   return work0 + work1 + work2;
}
print( sub( 1, 2 ) ); // 3
print( sub( nil, 2 ) ); // 0
print( sub( 1, nil ) ); // 0
print( sub( nil, nil ) ); // 0

In this example, we are running let! work1, work2 = func( val1, val2 ).

  • This declares work1 and work2 with initial values of the result of func().
  • If either work1, work2 is nil, execute the first block.
  • If all are non-nil, then execute the block. then is optional.

; is required in the let statement. In the above sample, ; is added as }; at the end of the then block.

The first block has restrictions that must handle one of the following:

  • Set values for all variables declared with let.
  • Exit the scope that declares let.

In the above example, values are set to work1 and work2, but it is OK to exit this function with return.

The behavior is undefined if the above restrictions are not followed.

unwrap!

unwrap! is a let! -like control. The difference is that instead of declaring a variable, you assign it to an existing variable.

Here is an example of unwrap!

// @lnsFront: ok
fn test( arg:int! ) {
  let mut val = 0;

  unwrap! val = arg { print( 0 ); return; } then { val = val + 1; };
  print( val );
}
test( 1 );  // print( 2 );
test( 2 );  // print( 3 );
test( nil );  // print( 0 );

val in the above example is an int type variable. This variable is assigned arg of type int! using unwrap!.

The above unwrap! val = arg { print( 0 ); return; } then { val = val + 1; }; does the following:

  • If arg is nil, execute { print( 0 ); return; }.
  • If arg is non-nil, assigns arg to val. Execute more then blocks.
  • then is optional.

Map type access

If you access an element of Map type data, the result will be nilable.

For example if:

// @lnsFront: ok
let val = { "abc": 1 };
let val2 = val.abc;

val2 will be int! instead of int.

This is because the evaluation result is nil if there is no element of type Map .

By the way, list and array element access is not nilable.

// @lnsFront: ok
let val = [ 1, 2, 3 ];
let val2 = val[ 1 ];

In the example above, val2 will be an int instead of an int!

Note that accessing val[ 4 ] has undefined behavior.

Be very careful when accessing lists and arrays by index.

I thought about making the list and array index access results nilable, but I didn't do it because I felt it was overkill.

summary

LuneScript is nil-safe through the following specifications:

  • nilable and non-nilable
  • unwrap

Next time, I will explain the class.