Minecraftとタートルと僕

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

FS API を使いこなそう(8): 【補足】関数を定義する関数を定義する(応用編)

前回のお話

前回は関数型プログラミングの基本ということで、Luaの関数がファーストクラスオブジェクトであることを説明しました。

・・・えーつまり。Luaの「関数型」は「数値型」や「文字列型」と同レベルの基本的な型であり、

数値を変数に代入したり、数値を関数の引数として与えたり、関数の返値として数値が返ってくるのと同様に、

関数を変数に代入したり、関数を関数の引数として与えたり、関数の辺地として関数が返すようにできるのです。

そのおかげで、長い関数名を短い関数名に変えたり、後から柔軟に挙動を変える「動的な関数」を作れたりするわけです。

便利ですよね。

これらを使いこなすことができれば、あなたも立派な関数型プログラマ!!!

・・・・・・ごめん、うそです。これらはまだ基本に過ぎないのですよ。

似たような関数をたくさん定義するという苦難

APIを作るときに悩まされるのが、似たような関数をたくさん定義しなければならないことです。

プログラマは冗長なプログラムを嫌います。

「同じような処理、同じような記述が長々と続くプログラムはキモイ!」と生理的なレベルで嫌悪を抱くようになればあなたも立派なプログラマですね:P

そのような冗長な処理、冗長な記述は、共通部分をまとめてサブ関数にしましょう。

構造化プログラミングですよ。 構造化!構造化!

さて今回のお題、ZAP!ZAP!する冗長なプログラムは、前回紹介した「logging API」です。

forward()、back()、up()、down()、turnRight()、turnLeft()の6つの関数を定義しているのですが、ほとんど同じ記述となっています。

自分の書いたプログラムながら、うんざり。

関数型プログラミングの手法を利用して、構造化しましょう。

まず、共通部分を見つけ出す。

まずはじっくりと見比べてみましょう。

function forward()
  local status, error_msg = turtle.forward()
  if status then -- succeeded
    write("turtle.forward()")
  else -- failed
    write("-- turtle.forward()")
  end
  return status, error_msg
end
 
function back()
  local status, error_msg = turtle.back()
  if status then -- succeeded
    write("turtle.back()")
  else -- failed
    write("-- turtle.back()")
  end
  return status, error_msg
end

違うのは以下の3箇所のようです。

  • 各関数の2行目: 関数!
    • local status, error_msg = turtle.forward()
    • local status, error_msg = turtle.back()
  • 各関数の4行目: 文字列!
    • write("turtle.forward()")
    • write("turtle.back()")
  • 各関数の6行目: 文字列!
    • write("-- turtle.forward()")
    • write("-- turtle.forward()")

ということは、3つの引数を持つサブ関数を作ればうまく行きそう。

function subDef(func, succeeded_msg, failed_msg)
  local status, error_msg = func()
  if status then -- succeeded
    write(succeeded_msg)
  else -- failed
    write(failed_msg)
  end
  return status, error_msg
end

forward()
  subDef(turtle.forward, "turtle.forward()", "-- turtle.forward()")
end

back()
  subDef(turtle.back, "turtle.back()", "-- turtle.back()")
end

前回のプログラムと比べるとかなりすっきりとしましたね。

ポイントは、サブ関数subDef()の第1引数が関数であることです。

関数それ自体を引数として与えるために、turtle.forwardと末尾のかっこ()は必要ないのですからね。一応念のため。

このようなサブ関数を使った構造化はバグを減らす効果がありますし、何より書くのが楽になります。

残った4つの関数についても書き換えたプログラム全体はと以下のようになります。

-- ###########################
-- config
local LOGFILE = "mylog"
 
-- ###########################
-- functions
function renameLogfile(name)
  LOGFILE = name
end
 
function write(message)
  local fh = fs.open(LOGFILE, "a")
  fh.writeLine(message)
  fh.close()
end

local function subDef(func, succeeded_msg, failed_msg)
  local status, error_msg = func()
  if status then -- succeeded
    write(succeeded_msg)
  else -- failed
    write(failed_msg)
  end
  return status, error_msg
end

function forward()
  subDef(turtle.forward, "turtle.forward()", "-- turtle.forward()")
end
 
function back()
  subDef(turtle.back, "turtle.back()", "-- turtle.back()")
end
 
function up()
  subDef(turtle.up, "turtle.up()", "-- turtle.up()")
end
 
function down()
  subDef(turtle.down, "turtle.down()", "-- turtle.down()")
end
 
function turnRight()
  subDef(turtle.turnRight, "turtle.turnRight()", "-- turtle.turnRight()")
end
 
function turnLeft()
   subDef(turtle.turnLeft, "turtle.turnLeft()", "-- turtle.turnLeft()")
end

なお、変数や関数定義の頭にlocalとつけたのは、それらをローカル変数、ローカル関数にするためです。

APIを作るときには、変数・関数のローカル/グローバルを意識しましょう。

APIファイルの中で(localをつけていない)グローバルな変数・関数が、このAPIが提供する変数・関数になります。

逆に、APIファイルの中でローカル宣言した変数・関数は他のプログラムから(原則として)利用することはできません。

つまり、このAPIを利用する側のプログラムからは、変数LOGFILEやsubDef()関数を直接触れないようにしているわけです。

つまりデータ隠蔽! カプセル化! カプセル化!

前回学んだことを応用してさらに簡略化

前回、「無名関数」はファーストクラスオブジェクトなので好きな変数に代入することができると学びました。

そのため、上記の関数定義は以下のように書き換えることが可能です。

forward = function() 
  subDef(turtle.forward, "turtle.forward()", "-- turtle.forward()")
end
 
back = function()
  subDef(turtle.back, "turtle.back()", "-- turtle.back()")
end
 
up = function up()
  subDef(turtle.up, "turtle.up()", "-- turtle.up()")
end
 
down = function()
  subDef(turtle.down, "turtle.down()", "-- turtle.down()")
end
 
turnRight = function()
  subDef(turtle.turnRight, "turtle.turnRight()", "-- turtle.turnRight()")
end
 
turnLeft = function()
   subDef(turtle.turnLeft, "turtle.turnLeft()", "-- turtle.turnLeft()")
end

function() ~ endという制御文で、無名関数を作成することができます。

forward = function() ~ endとはすなわち、forward = (無名関数)のように変数forwardに新たに作った無名関数を代入するという処理になります。

つまり、以下の2つは全く同じ意味です。

function hoge()
  ・・・
end
hoge = function()
  ・・・
end

実はぶっちゃけて言いましょう。

Luaでは、前者の記述がプログラム中にあったら、後者の記述に置き換えてから実行されます。

つまり。後者の変数に無名関数を代入するという処理の方がLua的には正しいのです!

ええー!なんだってー!?(AA略

まだ終わりではないのですよ

かなりすっきりとしましたが、実はもっと簡略化できます。え?もうこれ以上は無理って?

いえ、共通部分はまだあるのですよ。見比べて探してみましょう。

forward = function()
  subDef(turtle.forward, "turtle.forward()", "-- turtle.forward()")
end
 
back = function()
  subDef(turtle.back, "turtle.back()", "-- turtle.back()")
end
  • 1行目と3行目:無名関数作成文
    • function() ~ end

ね? 

それでは、この記事のタイトルである「関数を定義する関数を定義」してみましょう。

function createLoggedFunc(func, succeeded_log, failed_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

forward = createLoggedFunc(turtle.forward, "turtle.forward()", "-- turtle.forward()")
back = createLoggedFunc(turtle.back, "turtle.back()", "-- turtle.back()")

ポイントは2行目のreturnです。

関数createLoggedFunc()は、以下のような構造をしており、実行すると無名関数を返してくれるのです。

function createLoggedFunc()
  return (function() ~ end)
end

次回のお話

以下が、この方法で作ったプログラム完成版です。

http://pastebin.com/CUyLbuZ3

このプログラムの詳細な解説はまた次回。お楽しみに。