Luaの始め方

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

NGINXの基本を理解した後、Luaについてさらに学びます。LuaはOpenRestyで使用されるプログラミング言語であり、その基本的な構文を習得することが必要です。

Luaは小さくて洗練されたスクリプト言語で、ブラジルの大学の研究室で生まれました。その名前はポルトガル語で「美しい月」を意味します。NGINXはロシアで生まれ、Luaはブラジルで、OpenRestyは作者の国である中国で生まれました。興味深いことに、これら3つの同等に賢いオープンソース技術は、欧米ではなくBRICS諸国から生まれました。

Luaは、シンプルで軽量な組み込み用の接着剤言語として設計されており、大きくて大胆な路線を取っていません。日常の仕事で直接Luaコードを書くことはないかもしれませんが、Luaは広く使われています。多くのオンラインゲーム、例えばWorld of Warcraftは、Luaを使ってプラグインを書いています。キーバリュー型データベースのRedisは、Luaを内蔵してロジックを制御しています。

一方で、Luaのライブラリは比較的シンプルですが、C言語のライブラリを簡単に呼び出すことができ、多くの成熟したC言語のコードを利用できます。例えば、OpenRestyでは、NGINXやOpenSSLのC言語関数を頻繁に呼び出す必要がありますが、これはLuaとLuaJITがCライブラリに簡単にアクセスできるためです。

ここでは、Luaのデータ型と構文を簡単に紹介し、後でOpenRestyをよりスムーズに学べるようにします。

環境とhello world

標準のLua 5.1環境を特別にインストールする必要はありません。なぜなら、OpenRestyは標準のLuaをサポートしておらず、LuaJITのみをサポートしているからです。ここで紹介するLuaの構文もLuaJITと互換性があり、最新のLua 5.3に基づいていないことに注意してください。

OpenRestyのインストールディレクトリにLuaJITのディレクトリと実行ファイルがあります。私はMac環境でbrewを使ってOpenRestyをインストールしたので、以下のパスはあなたの環境とは異なるかもしれません。

$ ll /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit
     lrwxr-xr-x  1 ming  admin    18B  4  2 14:54 /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit -> luajit-2.1.0-beta3

システムの実行ファイルディレクトリでも見つけることができます。

 $ which luajit
 /usr/local/bin/luajit

LuaJITのバージョンを確認します。

$ luajit -v
 LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/

この情報を確認した後、新しい1.luaファイルを作成し、LuaJITを使ってhello worldコードを実行できます。

$ cat 1.lua
print("hello world")
$ luajit 1.lua
 hello world

もちろん、restyを使って直接実行することもできます。最終的にはLuaJITで実行されることを知っておいてください。

$ resty -e 'print("hello world")'
 hello world

これらの方法でhello worldを実行できます。私はrestyの方法を好みます。なぜなら、後で多くのOpenRestyコードもrestyで実行されるからです。

データ型

Luaには多くのデータ型はありません。type関数を使って値の型を返すことができます。例えば以下の通りです。

$ resty -e 'print(type("hello world"))
 print(type(print))
 print(type(true))
 print(type(360.0))
 print(type({}))
 print(type(nil))
 '

以下の情報が出力されます。

 string
 function
 boolean
 number
 table
 nil

これらはLuaの基本的なデータ型です。簡単に紹介します。

文字列

Luaでは、文字列は不変の値です。文字列を変更したい場合は、新しい文字列を作成する必要があります。このアプローチには利点と欠点があります。利点は、同じ文字列が何度も現れてもメモリ内には1つのコピーしかないことです。しかし、欠点も明らかです。文字列を変更したり結合したりする場合、多くの不要な文字列が生成されます。

この欠点を説明するために例を挙げます。Luaでは、文字列の結合に2つのドットマークを使用します。以下のコードは、1から10までの数字を文字列として結合します。

$ resty -e 'local s  = ""
 for i = 1, 10 do
     s = s .. tostring(i)
 end
 print(s)'

ここでは10回ループし、最後の結果だけが必要です。その間の9つの新しい文字列は不要です。これらは余分なスペースを占有するだけでなく、不要なCPU操作も消費します。

もちろん、後でパフォーマンス最適化のセクションでこの問題に対する解決策を紹介します。

また、Luaでは、文字列を表現するために3つの方法があります。シングルクォート、ダブルクォート、そして長い括弧([[]])です。最初の2つは比較的理解しやすく、他の言語でも一般的に使用されています。では、長い括弧は何に使われるのでしょうか?

具体的な例を見てみましょう。

$ resty -e 'print([[string has \n and \r]])'
 string has \n and \r

長い括弧内の文字列はエスケープされていないことがわかります。

もう1つの質問があるかもしれません。上記の文字列に長い括弧が含まれている場合はどうなるのでしょうか?答えは簡単です。長い括弧の間に1つ以上の=記号を追加します。

$ resty -e 'print([=[ string has a [[]]. ]=])'
  string has a [[]].

ブール値

これはシンプルで、truefalseです。Luaでは、nilfalseのみが偽であり、それ以外はすべて真です。これには0や空の文字列も含まれます。以下のコードでこれを確認できます。

$ resty -e 'local a = 0
 if a then
   print("true")
 end
 a = ""
 if a then
   print("true")
 end'

このような判断は、多くの一般的な開発言語と一致しないため、このような問題でエラーを避けるために、比較の対象を明示的に書くことができます。例えば以下の通りです。

$ resty -e 'local a = 0
 if a == false then
   print("true")
 end
 '

数値

Luaの数値型は倍精度浮動小数点数として実装されています。特筆すべきは、LuaJITはデュアルナンバーモードをサポートしており、LuaJITは整数を整数として、浮動小数点数を倍精度浮動小数点数として保存します。これはコンテキストに依存します。

さらに、LuaJITは大きな整数に対してlong-long integersをサポートしています。例えば以下の例です。

$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL

関数

関数はLuaでは第一級市民です。関数を変数に保存したり、別の関数への引数や戻り値として使用したりできます。

例えば、以下の2つの関数宣言は完全に同等です。

function foo()
 end

foo = function ()
 end

テーブル

テーブルはLuaで唯一のデータ構造であり、非常に重要です。そのため、後で特別なセクションを設けて詳しく説明します。まずは簡単な例を見てみましょう。

$ resty -e 'local color = {first = "red"}
print(color["first"])'
 red

ヌル値

Luaでは、ヌル値はnilです。変数を定義しても値を代入しない場合、そのデフォルト値はnilです。

$ resty -e 'local a
 print(type(a))'
 nil

OpenRestyシステムに入ると、多くのヌル値、例えばngx.nullなどを見つけるでしょう。これについては後で詳しく話します。

Luaのデータ型については、まずはこれだけ紹介します。後で記事の中でさらに学ぶべき重要なポイントを紹介します。実践と使用を通じて学ぶことが、新しい知識を吸収する最も便利な方法です。

一般的な標準ライブラリ

しばしば、言語を学ぶことは実際にはその標準ライブラリを学ぶことです。

Luaは比較的小さく、組み込みの標準ライブラリは多くありません。また、OpenResty環境では、Luaの標準ライブラリは非常に優先度が低いです。同じ機能の場合、OpenRestyのAPIを最初に使用し、次にLuaJITのライブラリ関数、そして通常のLua関数を使用することをお勧めします。

OpenRestyのAPI > LuaJITのライブラリ関数 > 標準Luaの関数という優先順位は、使いやすさとパフォーマンスの観点から繰り返し言及されます。

しかし、それでも実際のプロジェクトではいくつかのLuaライブラリを使用することが避けられません。ここでは、より一般的に使用される標準ライブラリをいくつか選んで紹介します。さらに詳しく知りたい場合は、公式のLuaドキュメントを参照してください。

文字列ライブラリ

文字列操作は私たちが頻繁に使用し、落とし穴が最も多い部分です。

1つの簡単なルールは、正規表現が関わる場合は、必ずOpenRestyが提供するngx.re.*を使用して解決し、Luaのstring.*処理を使用しないことです。これは、Luaの正規表現は独特で、PCRE仕様に準拠していないためです。そして、ほとんどのエンジニアはそれを扱うことができないと信じています。

最もよく使用される文字列ライブラリ関数の1つはstring.byte(s [, i [, j ]])で、文字s[i], s[i + 1], s[i + 2], ------, s[j]に対応するASCIIコードを返します。iのデフォルト値は1で、最初のバイトです。jのデフォルト値はiです。

サンプルコードを見てみましょう。

$ resty -e 'print(string.byte("abc", 1, 3))
 print(string.byte("abc", 3)) -- 第3引数が欠落している場合、第3引数は第2引数と同じで3になります
 print(string.byte("abc"))    -- 第2引数と第3引数が欠落している場合、両方ともデフォルトで1になります
 '

その出力は以下の通りです。

 979899
 99
 97

テーブルライブラリ

OpenRestyのコンテキストでは、Luaに付属するほとんどのテーブルライブラリを使用することはお勧めしません。table.concattable.sortなどのいくつかの関数を除いて、それらの詳細はLuaJITの章に任せます。

ここではtable.concatについて簡単に触れます。table.concatは一般的に文字列結合のシナリオで使用されます。以下の例のように、多くの不要な文字列の生成を避けることができます。

$ resty -e 'local a = {"A", "b", "C"}
 print(table.concat(a))'

数学ライブラリ

Luaの数学ライブラリは、標準的な数学関数のセットで構成されています。数学ライブラリの導入は、Luaプログラミング言語を豊かにし、プログラムを簡単に書くことができるようにします。

OpenRestyプロジェクトでは、Luaを使って数学演算を行うことはほとんどありません。しかし、ランダム数に関連する2つの関数、math.random()math.randomseed()はよく使用されます。例えば以下のコードは、指定された範囲内で2つのランダム数を生成できます。

$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'

ダミー変数

これらの共有標準ライブラリを理解した後、新しい概念であるダミー変数について学びましょう。

ある関数が複数の値を返すシナリオを想像してください。そのうちいくつかは必要ありません。では、これらの値を受け取るにはどうすればよいでしょうか?

あなたがどう感じるかはわかりませんが、少なくとも私にとっては、これらの未使用の変数に意味のある名前を付けようとするのは苦痛です。

幸い、Luaにはこれに対する完璧な解決策があります。ダミー変数の概念を提供し、慣例的にアンダースコアで名前を付け、不要な値を破棄し、プレースホルダーとして機能させます。

標準ライブラリ関数string.findを例として、ダミー変数の使用法を見てみましょう。この標準ライブラリ関数は、開始と終了の添字を表す2つの値を返します。

開始添字のみを取得したい場合は、以下のようにstring.findの戻り値を受け取る変数を宣言するだけで簡単です。

$ resty -e 'local start = string.find("hello", "he")
 print(start)'
 1

しかし、終了添字のみを取得したい場合は、ダミー変数を使用する必要があります。

$ resty -e 'local  _, end_pos = string.find("hello", "he")
 print(end_pos)'
 2

ダミー変数は戻り値だけでなく、ループでもよく使用されます。例えば以下の例です。

$ resty -e 'for _, v in ipairs({4,5,6}) do
     print(v)
 end'
 4
 5
 6

そして、無視する戻り値が複数ある場合、同じダミー変数を再利用できます。ここでは例を挙げません。このようなサンプルコードを自分で書いてみてください。コメント欄にコードを投稿して、私と共有・交換してください。

まとめ

今日は、標準Luaのデータ構造と構文を簡単に見てきました。このシンプルでコンパクトな言語について、最初の印象を得たことでしょう。次のレッスンでは、LuaとLuaJITの関係について説明します。LuaJITはOpenRestyの主要な部分であり、深く掘り下げる価値があります。

最後に、もう1つ考えさせられる質問を残しておきます。

この投稿で学んだコードを覚えていますか?数学ライブラリについて話したときに、指定された範囲内で2つのランダム数を生成するコードです。

$ resty -e 'math.randomseed (os.time())
print(math.random())
 print(math.random(100))'

しかし、このコードは現在のタイムスタンプをシードとして使用していることに気づいたかもしれません。このアプローチには問題があるのでしょうか?そして、良いシードをどのように生成すべきでしょうか?私たちが開発するランダム数は、ランダムではなく、セキュリティ上のリスクがあることがよくあります。

ぜひあなたの意見を共有してください。また、この投稿を同僚や友人と共有して、一緒にコミュニケーションと改善を進めましょう。