Minecraftとタートルと僕

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

【補足】論理演算子を用いたLuaの慣用表現

はじめに

f:id:hevohevo:20140128224533g:plain

今回はCCから離れて、プログラミング言語Luaについてお話しましょう。

お題はこちら。以前の記事で説明を省いてしまったので、その説明回となります。

function defaultPrint(x)
  local x = x or "world"
  print("Hello ",x)
end

x = x or "world"という表記により、この関数defaultPrint()の引数の初期値、つまりデフォルト値を指定することができます。

この関数を実行すると次のようになります。

lua> defaultPrint("Bob")
Hello Bob

lua> defaultPrint(" ")
Hello  

lua> defaultPrint()
Hello world

引数xに何か値を指定したらその値を使って関数を実行するけれど、何も値を指定しなかったらデフォルト値を使って関数を実行します。

つまり、

x = x or (xの初期値) 

は、次のIF文と同じです。

if (not x) then
  x = (xの初期値)
end

この書き方はLuaの慣用表現としてよく使われるので覚えておきましょう。

今回は、なぜ、この書き方でこのような挙動となるのかを説明します。

解説

この挙動を説明するには、Luaの以下のルール(仕様)を理解しておかなくてはなりません。

ルール1

  • Luaでは論理演算における真偽値(Boolean)を次のように定めている。
    • 「false」「nil」だけが偽。それ以外はすべて真。
    • つまり、数値「0」、文字列"a"、半角スペース" "、長さ0の文字列""などは全部「真」です。

ルール2

  • 定義されていない変数、まだ何も代入されていない変数を参照するとnilが返る*1
# 変数xがこれまで一度も出てきていない場合
# print(nil)を実行したことになるので、空行が表示される。
lua> print(x)
(空行)

ルール3

  • たとえば、関数の引数が3つ必要であるにも関わらず、2つしか指定しなかったら、最後の1つの引数には何も代入されない。
  • そのため参照するとnilが返る(ルール2を適用)。
  • つまり、「指定しなかった引数にはnilが入っている」
function test(x,y,z)
  print("x: ", x)
  print("y: ", y)
  print("z: ", z)
end


test(1,2)
-- 実行結果
-- x: 1
-- y: 2
-- z: 

上記プログラムを実行すると、引数xには1、引数yには2が入るが、残るzはnilとして処理される。

ルール4

  • 代入記号「=」は、その右辺を全部処理してから左辺の変数に代入する。
# 右辺の1+2+3+4を全部計算してから左辺のxに代入
lua> x = 1+2+3+4
10

ルール5

  • 論理演算子「or」は、まずその左側の式を確認し真ならその値を返す。偽ならその右側の式を確認しその値を返す。
  • つまり、左から順に真偽値を見て判断するのだけど、左を見た時点で真ならばもう右側は見ない。
  • 何が真・偽なのかは、ルール1を確認のこと
# orの左が真ならそのままその値を返すよ。という例
lua> 1 or 2
1
lua> true or 11
true
lua> 0 or true
0

# orの左が偽なら右側の値をそのまま返すよ。という例
lua> false or 1
1
lua> false or true
true
lua> nil or 3
3
lua> false or "hello"
"hello"
lua> false or false
false

以上のルールを前提として、説明

function defaultPrint(x)
  local x = x or "world"
  print("Hello ",x)
end

実行例1) defaultPrint("Nancy")

defaultPrint("Nancy")
↓ 関数内のローカル変数xには、文字列"Nancy"が入っている。
local x = x or "world"  【ルール4適用により、「=」の右辺から処理します】
↓ xは"Nancy"
local x = "Nancy" or "world"
↓ ルール5適用により、orの左側が真なのでその値を返す
local x = "Nancy"  
↓ つまり変数xに"Nancy"を代入
Hello Nancy

実行例2) defaultPrint()

defaultPrint()
↓ 関数内のローカル変数xの値はなし(nil)。 
local x = x or "world"  【ルール4適用により、右辺から処理】
↓ ルール3適用して、xはnil
local x = nil or "world"  
↓ ルール5適用により、orの左側を飛ばして右側の値を返す
local x = "world"  
↓ 変数xに"world"を代入して実行
Hello world

この慣用表現のまた別の使用例

次のような使い方をよく見ますね。

-- プログラム実行時に引数が指定されているならその値(文字列なので数値化している)を代入
-- 引数なしならば初期値10を代入。

local args = {...}

local depth = tonumber(args[1]) or 10

-- 以降、いろんな処理

さらに応用。その他の慣用表現(三項演算子の実現)

上記で紹介した慣用表現をさらに発展させたものが次の表現です。

条件によって異なる初期値を設定する。

x = 条件式 and 値1 or 値2

これは、以下と(ほぼ)同等です。

if (条件式) then
  x = 値1
else
  x = 値2
end

つまり、条件式が真のときに値1をxに代入、偽のときに値2を代入します。非常によく使う条件式ですね。

よく使う条件式ならば、できるだけ簡潔に書きたくなります。

この条件式を簡単に1行で表現する演算子を「三項演算子」と呼びます。様々なプログラム言語であらかじめ用意されていることが多いのですが*2、Luaには三項演算子が用意されていないために、論理演算子「and/or」を使って擬似的に実現しているのです。

ただしこの擬似三項演算子には、値1が偽であってはならないという使用上の制限があります。ご注意ください*3

具体的な使用例としては、以下のとおり。

-- 入力した文字列が100より大きいかどうかを判断して、その結果を表示。
-- なお、入力した文字列を数値化できないとき、たとえば"abc"などではエラーとなります。
local size = 100

(sizeについて、いろいろな処理)

local strPrint = (size > 100) and  "100より大きい" or "100以下" 
print(strPrint)

上記を1行で書くこともできますが、さすがにこれは見づらいかな?

print( (size > 100) and  "100より大きい" or "100以下" )

 擬似三項演算子の解説

まず最初に論理演算子「and」の振る舞いについて。

ルール6

  • 「and」はまずその左側を確認し、偽ならその値を返す。真ならその右側の値を返す。
# 左が偽ならその値を返す
lua> false and 1
false
lua> false and true
false

# 左が真なら右の値を返す
lua> 1 and 2
2
lua> true and true
true
lua> true and false
false
lua> "a" and "b"
"b"

このルールも踏まえたうえで、sizeの値によって以下のような挙動になります。

実行例1)size=200のとき

local strPrint = (size > 100) and  "100より大きい" or "100以下" 
↓ sizeは200です
local strPrint = (200 > 100) and  "100より大きい" or "100以下" 
↓ 条件式(不等号による大小比較)を評価するとtrue
local strPrint = true and  "100より大きい" or "100以下" 
↓ ルール6よりandの右側を返す
local strPrint = "100より大きい" or "100以下"
↓ ルール5よりorの左側を返す
local strPrint = "100より大きい"
↓ よって
print("100より大きい")  

実行例2)size=1のとき

local strPrint = (size > 100) and "100より大きい" or "100以下" 
↓ sizeは1です
local strPrint = (1 > 100) and "100より大きい" or "100以下" 
↓ 条件式(不等号による大小比較)を評価するとfalse
local strPrint = false and "100より大きい" or "100以下" 
↓ andの左が偽なのでルール6によりfalseを返す
local strPrint = false or "100以下"
↓ orの左が偽なのでルール5により"100以下"を返す
local strPrint = "100以下"
↓ よって
print("100以下")  

おわりに

Luaの論理演算子を使った慣用表現を紹介し、Luaの仕様とともにその挙動を解説しました。

このように演算子を使うことで複雑な処理を簡潔に書くことができます。

しかし複雑な論理式は、やりすぎると逆にプログラムが見づらくなります。ほどほどにしておくのが良いでしょう。

やりすぎ(?)の例

local args={...}
local num = tonumber(args[1])
local limit = turtle.getFuelLimit()
for i=1,16 do
  turtle.select(i)
  while (num and num > 0 and num < limit and num or limit) > turtle.getFuelLevel() and turtle.refuel(1) do
    print(turtle.getFuelLevel())
  end
end

何をやっているかわかります?

*1:このルールは、変数宣言が必須なプログラミング言語に慣れ親しんでいる人にとってはモヤモヤするかも?

*2:たとえばRubyなど多くの言語では「x = (条件式) ? 値1 : 値2」。

*3:この制限のため、僕はLuaの擬似三項演算子が好きではありません:P

広告を非表示にする