Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

FS API を使いこなそう(10): 関数の引数を柔軟に取り扱う

前回のお話

前回紹介したlogging API (ver. 0.2)を使うことで、任意の関数にログ記録機能を簡単につけられるようになりました。

問題点

でも使ってみて気づきませんでした?

対象となる関数が引数が必要なタイプのときに、その引数がうまく処理できていないのですよね。

つまり、

os.loadAPI("logging")

forward = logging.createLoggedFunc(turtle.forward, "-- OK", "--Failed")

forward()

このようにturtle.forward()という引数が必要ない関数ならば問題ないのですが、

次のように引数が必要となる関数だと問題が生じます

os.loadAPI("logging")

select = logging.createLoggedFunc(turtle.select, "-- OK", "--Failed")

select(10)

実際にこのプログラムを動かすと、「turtle.select()に引数が指定されてないんですけど!」(意訳)というエラーが出てきます。select(10)のように実行時に指定しているのにね><

createLoggedFunc()関数を修正しなくてはいけません。

ただし問題となるのは、ログ機能をつけたい関数が、いくつ引数をとるかわからないところです。

たとえばturtle.select()は引数1つだけでしたね。でも場合によっては、たとえ引数が10個あるような関数でもログ機能付きにできなくてはなりません。

さてどうすればよいのでしょうか。

いきなり答え

前回紹介したAPIで、該当部分を抜粋すると次のようになっていました。

function createLoggedFunc(func, succeeded_log, failed_log)
  failed_log = failed_log or succeeded_log
  return function()
    local status, error_msg = func()
    if status then
      write(succeeded_log)
    else
      write(failed_log)
    end
    return status, error_msg
  end
end

この関数定義で、以下のように、2箇所だけ、「...」を入れてください。それだけでOKです。

function createLoggedFunc(func, succeeded_log, failed_log)
  failed_log = failed_log or succeeded_log
  return function(...)
    local status, error_msg = func(...)
    if status then
      write(succeeded_log)
    else
      write(failed_log)
    end
    return status, error_msg
  end
end

なお、この修正を反映させたlogging API (ver. 0.3)を以下においておきます。

Luaの関数は引数の数について寛容なのです

LuaはJavaやC言語などとは異なり、関数の引数の数が違っていてもエラーを返しません。

具体的に例を示しましょう。

# 引数を2つとり、それぞれ変数x,yに代入して、それぞれprintする関数を定義
lua> function argTest(x,y) print(x); print(y); end

# 引数2つで実行すると、当然ながら次のようになる
lua> argTest("abc", "def")
abc
def

# 実行時に、定義したときよりも多く引数を与えると、
# 多い引数は捨てられて、定義された数の引数しか使わない
lua> argTest("abc", "def", "ghi")
abc
def

では逆に、定義したときよりも与える引数の数が少ないときはどうなるでしょうか。

lua> argTest("abc")
abc
(空行)

(2014/06/21修正、false×、nil○)

足りない引数には「nil」が代入されているものとして関数を実行します。

上記の場合で言うならば、引数"abc"しか与えられなかったので、1番目の引数として"abc"、2番目の引数としてnilが与えられたと処理されます。

つまり、実際の実行内容は、print("abc"); print(nil);となります。

だから最後は、空行が表示されるのです。

このような仕様により、Luaは柔軟に関数を定義できるのです。面白いですね。

ただしこの仕様には欠点もあります。それは、柔軟に扱える一方でバグがあったときになかなか気づかないといった問題です。

3つの引数を必要としている関数なのに引数2つで実行。でもLuaはエラーを返さずに普通にその関数を実行します。 そして、Luaはエラーを返さないけれど、その関数の中の処理でnilのせいで画面表示が狂ったり計算がおかしくなったりと後から困惑する。

Luaプログラミングにある程度慣れてくると、あるあるネタだったりします。

可変長引数を取り扱う

ここまでは、関数定義時に決めておいた引数の数と関数実行時の引数の数が食い違っていても、Luaは柔軟に処理してくれますよというお話でした。

それに対して、いくつ引数をとってもかまわないという関数を定義することができます。

function manyArgTest(...)
  local arg1, arg2, arg3 = ...
  print('arg1: ', arg1)
  print('arg2: ', arg1)
  print('arg3: ', arg1)
end

manyArgTest(1,2,3,4,5,6,7,8,9,10)

このプログラムでは、可変長引数をまとめてくれる「...」という式を使っています。

このプログラムでは1から10まで計10個の引数を与えて実行しています。

その結果、manyArgTest()内部では、「...」という式がこれら10個の引数をまとめたものになります。

2行目で、それら10個の引数のうち先頭の3個をarg1,arg2,arg3の3つの変数に代入して、画面表示しています。

でもこのプログラムは不完全です。たとえば引数が10個あるうちの10番目を使いたかったらどうすればよいのでしょう。

arg1からarg10までの変数をあらかじめ用意しておく? でも引数が100個ならarg100までの変数が必要になりますよね*1

そんなアホなプログラムを書くなんてとんでもない! 

以下のように、コンストラクタ式「{}」を使いましょう。

function manyArgTest(...)
  local args = {...}
  print('arg1: ', arg[1])
  print('arg2: ', arg[2])
  print('arg3: ', arg[3])
end

manyArgTest(1,2,3,4,5,6,7,8,9,10)

2行目で「{...}」と記述することにより、計10個の引数が一つの配列(テーブル)としてまとめられます。

その引数配列は変数argsに代入していますので、たとえば10番目の引数を取り出したいならば、args[10]のようにしましょう。100番目ならargs[100]です。

なお、以下のプログラムは引数を無限にとることができる関数の例です。

function add(...)
  local sum = 0
  for i,v in ipairs({...}) do
    sum = sum + v
  end
  return sum
end

add(1,2,3,4)
-- => 10

add(1,2,3,4,5,6,7,8,9,10)
-- => 55

そういえば、このような引数を無限にとることができる関数の例としてprint()もありますよね。

lua> print(1,2,3,4,5)
12345

どこかで見たことないですか? args={...}

ところで、args={...}という記述、これまで結構な頻度で見ていますよね? そう。プログラム実行時の引数を取り扱うために利用していました。

じつはこの「...」という式は、関数実行時の可変長引数をまとめるだけでなく、プログラム実行時の可変長引数をまとめる機能もあります。

args = {...}

-- #配列 という記述で配列の要素数を返します。
if #args == 0 then 
  error('invalid arguments')
end

などというように、プログラム実行時に必ず引数の入力を求めるようなプログラムを書くことができます。

おわりに

以上、長々と説明しましたが以下のようにまとめることできます。

  • Luaの関数は、定義時の引数の数と実行時の引数の数が違っていても問題なく動く
    • 実行時に多めに引数を与えると、余った分の引数は捨てられる。
    • 実行時に少なめに引数を与えると、足りない分の引数には、falseが代入されたと処理される
  • 引数が何個でもよい(可変長引数)関数を定義できる
    • 「...」で可変長引数をまとめることができる
    • 実際に可変長引数を取り扱うときには、args = {...}と複数の引数を一つの配列にまとめた方が取り扱いやすいかも

*1:引数を100個必要とするような関数を定義することなんて実際にはありえませんが!たとえ話ですよ? 一般的に、引数の数が多くなると管理が大変になるので、普通はテーブルを引数として渡します。