公開技術情報

[English] [Japanese]

10.2. 多値の戻り値

LuneScript は、多値の関数戻り値に対応しています。

例えば、次のように関数は複数の値を返せます。

// @lnsFront: ok
fn func(): int,int {
   return 1, 2;
}
print( func() );  // 1 2

上記サンプルでは、 func() は 1 と 2 を返します。 この多値の戻り値を、そのまま print() の引数として渡しています。

多値の戻り値は非常に便利ですが、戻り値が多値である言語はまだまだ少ないです。 また、戻り値の 2 番目以降の値は、オプション的な意味合いのケースが多いです。

そのため、2 番目以降の戻り値の存在を忘れがちです。

これによって、不具合が発生することがあります。

多値の戻り値による不具合

次のコードは LuneScript ではなく、 Lua のコードです。

local function func1( txt ) 
   print( string.byte( txt:gsub( "b", "B" ) ) )
end
local function func2( txt ) 
   print( string.byte( (txt:gsub( "b", "B" )) ) )
end
func1( "abcb" ) -- 66
func2( "abcb" ) -- 97

この func1() と func2() は、同じ引数 "abcb" を与えているのに異なる結果を出力します。 何故そうなるか分かるでしょうか?

ちなみに txt:gsub( "b", "B" ) は、 文字列 txt 内の "b" を "B" に変換した文字列を返す Lua の標準ライブラリで、 string.byte() は、 指定の文字列に含まれる値を返す標準ライブラリです。

それでは問の答です。

func1() は string.byte に txt:gsub( "b", "B" ) の 戻り値そのままを渡しているのに対し、 func2() は () で括った (txt:gsub( "b", "B" )) を渡しています。

これにより、 func1() は string.byte()txt:gsub( "b", "B" ) の多値の値をそのまま渡しているのに対し、 func2() は string.byte()txt:gsub( "b", "B" ) の多値の先頭の値である文字列だけを渡しています。

string.byte() は、 第2引数が渡された場合、 第2引数が指定するインデックスの文字の値を返します。 一方、第2引数が省略された場合、先頭の文字の値を返します。

そして、 txt:gsub( "b", "B" ) は多値を返す関数です。 具体的には、変換後の文字列 str と何箇所変換したか int を返します。

txt:gsub( "b", "B" ) が変換した文字列を返す関数であることは、 Lua を利用している人であれば認識していることだと思います。 一方で、この関数が多値を返す関数だと認識している人はどの程度いるでしょうか?

また、認識していたとして、それが不具合につながるということに即座に気付く人は どれほどいるでしょうか?

本来 txt:gsub( "b", "B" ) の変換後の文字列だけを引数として使用したいのに、 意図せずに渡された多値の戻り値によって、関数動作が変ってしまうことがあります。

つまり、本来は func2() のように () を使って多値の先頭だけ使わなければならない時に、 func1() のようにそのまま多値を使ってしまうことがあるのです。

ここで冒頭で書いたことを思い出してください。

  • 多値の戻り値は非常に便利ですが、戻り値が多値である言語はまだまだ少ないです。
  • また、戻り値の 2 番目以降の値は、オプション的な意味合いのケースが多いです。
  • そのため、2 番目以降の戻り値の存在を忘れがちです。

多値の戻り値は便利ですが、このようなリスクもあるんです。

LuneScript の場合

LuneScript では、このように問題を避けるため多値の戻り値を扱う際、 明示することにしました。 なお、明示しない場合は warning となります。

ただ、多値を利用する際に明示するのは、それはそれで面倒です。

そこで、明示が必要なケースとして、次のケースに限定します。

多値の戻り値の利用先が

  • 関数の引数で、なおかつその引数が省略可能な場合
  • return に指定する値で、なおかつその値が省略可能な場合

つまり、 let など直接変数に値を代入するような場合は、明示は不要です。 わざわざ値を代入する変数を用意しているということ自体が、 多値の第二引数以降を使用すると明示しているのと同様だと考えたためです。

明示方法

具体的な明示方法は次になります。

// @lnsFront: ok
fn func1(): int,int {
   return 1, 2;
}
fn func2( val1:int, val2:int!) {
   print( val1, val2 );
}

func2( func1()** );

上記 func1()** のように、関数コールの後に ** を付加することで、 その関数コールの多値を使用していることを明示します。

多値戻り値を使用する際の仕様

上記で示す通り、多値を返す関数の実行結果をそのまま別の関数の引数に渡すことができます。

但し、多値を返す関数の戻り値をそのまま多値として扱うには、 関数の後に何も指定していない場合に限ります。

これはどういうことかというと、 以下のように多値の戻り値を返す関数 func1 の実行結果を print で出力する際、 func1 の後に指定していると、 func1 の実行結果は先頭の値だけが処理されます。

// @lnsFront: ok
fn func1(): int,int {
   return 1, 2;
}
print( func1() );      // 1 2
print( func1(), 10 );  // 1 10
print( 0, func1() );  // 0 1 2

上の例の場合、

  • print( func1() ) は、多値の戻り値 1 2 がそのまま処理される。
  • print( func1(), 10 ) は、多値の戻り値の先頭の 10 が処理される。
  • print( 0, func1() ) は、多値の戻り値 1 2 がそのまま処理される。

これは、 List のコンストラクタなどの場合も同様です。

// @lnsFront: ok
fn func1(): int,int {
   return 1, 2;
}
let list1 = [ func1() ]; // [1 2]
let list2 = [ func1(), 10 ];  // [ 1 10]
let list3 = [ 0, func1() ];  // [0 1 2]

なお、次のように多値の関数呼び出しに () を付けた場合も、多値の戻り値の先頭だけを処理します。

print( (func1()) );

これは、 LuneScript の仕様というよりは、 Lua の仕様にもとづく動作です。