前回のお話
- 関数型プログラミング(基本編):
前回は関数型プログラミングの基本ということで、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
次回のお話
以下が、この方法で作ったプログラム完成版です。
このプログラムの詳細な解説はまた次回。お楽しみに。