Lua宣言型プログラミングの基本

Luaは、強力で高速、簡単、拡張可能で、埋め込み可能なスクリプトプログラミング言語です。 Luaはビジネスアプリケーションロジックの作成に役立ちます。

多くの場合、アプリケーションロジックの一部は宣言スタイルで簡単に記述されます。 宣言型プログラミングスタイルは、まず何が何かであり、どのように作成されるかではないことを記述するという点で、多くの命令型のより馴染みのあるスタイルとは異なります。 宣言的なスタイルでコードを記述すると、多くの場合、不要な実装の詳細を隠すことができます。

Luaはマルチパラダイムプログラミング言語です。 Luaの長所の1つは、宣言型スタイルを適切にサポートしていることです。 この記事では、Lua言語が提供する基本的な宣言ツールについて簡単に説明します。


素朴な例として、必須のスタイルのテキストメッセージとボタンを含むダイアログボックスを作成するコードを見てみましょう。

function build_message_box ( gui_builder ) <br/>
local my_dialog = gui_builder:dialog ( ) <br/>
my_dialog:set_title ( "Message Box" ) <br/>
<br/>
local my_label = gui_builder:label ( ) <br/>
my_label:set_font_size ( 20 ) <br/>
my_label:set_text ( "Hello, world!" ) <br/>
my_dialog:add ( my_label ) <br/>
<br/>
local my_button = gui_builder:button ( ) <br/>
my_button:set_title ( "OK" ) <br/>
my_dialog:add ( my_button ) <br/>
<br/>
return my_dialog<br/>
end

宣言スタイルでは、このコードは次のようになります。

build_message_box = gui:dialog "Message Box" <br/>
{ <br/>
gui:label "Hello, world!" { font_size = 20 } ; <br/>
gui:button "OK" { } ; <br/>
}

はるかに明確です。 しかし、それを機能させる方法は?

基本


問題が何であるかを理解するには、Luaの言語の機能のいくつかについて知る必要があります。 この記事を理解するために最も重要なことについて表面的に話します。 詳細については、以下のリンクを参照してください。

動的型付け


Luaは動的な類型化を持つ言語であることを覚えておくことが重要です。 これは、言語の型が変数ではなくその値に関連付けられていることを意味します。 同じ変数が異なるタイプの値をとることができます。

a = "the meaning of life" --> , <br/>
a = 42 -->

テーブル


テーブルは、Luaでデータを作成する主な手段です。 テーブルは、レコードと配列、辞書、セットとオブジェクトの両方です。

Luaでのプログラミングでは、このデータ型をよく知ることが非常に重要です。 理解のために最も重要な詳細のみを簡単に説明します。

テーブルは、「テーブルコンストラクター」、つまり中括弧のペアを使用して作成されます。

空のテーブルtを作成します。

t = { }

テーブルtに、キー1による文字列「one」とキー「one」による番号1を書き込みます。

t [ 1 ] = "one" <br/>
t [ "one" ] = 1

テーブルの内容は、作成中に指定できます。

t = { [ 1 ] = "one" , [ "one" ] = 1 }

Luaのテーブルには、すべてのタイプのキーと値を含めることができます(nilを除く)。 ただし、ほとんどの場合、正の整数(配列)または文字列(レコード/辞書)がキーとして使用されます。 この言語は、これらのタイプのキーを操作するための特別なツールを提供します。 構文のみに焦点を当てます。

まず 、テーブルを作成するときに、連続する要素の正の整数キーを省略できます。 この場合、要素は、テーブルコンストラクターで指定されたのと同じ順序でキーを受け取ります。 最初の暗黙的なキーは常に1です。 明示的に指定されたキーは、暗黙的に発行されると無視されます。

次の2つの記録形式は同等です。

t = { [ 1 ] = "one" , [ 2 ] = "two" , [ 3 ] = "three" } <br/>
t = { "one" , "two" , "three" }

第二に 、文字列リテラルをキーとして使用する場合、リテラルがlouid identifiersに課せられ制限を満たす場合、引用符と角括弧は省略できます。

テーブルを作成する場合、次の2つの記述形式は同等です。

t = { [ "one" ] = 1 } <br/>
t = { one = 1 }

同様に、書き込み時のインデックス作成について...

t [ "one" ] = 1 <br/>
t.one = 1

...そして読みながら:

print ( t [ "one" ] ) <br/>
print ( t.one )

機能


Luaの関数はファーストクラスの値です。 これは、たとえば文字列など、すべての場合に関数を使用できることを意味します。変数に割り当て、キーまたは値としてテーブルに格納し、引数または別の関数に戻り値として渡します。

Luaの関数は、コード内の任意の場所で動的に作成できます。 さらに、引数とグローバル変数が関数内で使用できるだけでなく、外部スコープからのローカル変数も使用できます。 実際、Luaの関数はクロージャーです。

function make_multiplier ( coeff ) <br/>
return function ( value ) <br/>
return value * coeff<br/>
end <br/>
end <br/>
<br/>
local x5 = make_multiplier ( 5 ) <br/>
print ( x5 ( 10 ) ) --> 50

Luaの「関数の宣言」は実際には構文糖であり、「関数」型の値の作成を隠して変数に割り当てることを覚えておくことが重要です。

関数を作成する次の2つの方法は同等です。 新しい関数が作成され、グローバル変数mulに割り当てられます。

砂糖入り:

function mul ( lhs, rhs ) return lhs * rhs end

シュガーフリー:

mul = function ( lhs, rhs ) return lhs * rhs end

括弧なしの関数呼び出し


Luaでは、この引数が文字列リテラルまたはテーブルコンストラクターである場合、単一の引数で関数呼び出すときに括弧を入れる必要はありません。 これは、宣言スタイルでコードを書くときに非常に便利です。

文字列リテラル:

my_name_is = function ( name ) <br/>
print ( "Use the force," , name ) <br/>
end <br/>
<br/>
my_name_is "Luke" --> Use the force, Luke

シュガーフリー:

my_name_is ( "Luke" )

テーブルコンストラクター:

shopping_list = function ( items ) <br/>
print ( "Shopping list:" ) <br/>
for name, qty in pairs ( items ) do <br/>
print ( "*" , qty, "x" , name ) <br/>
end <br/>
end <br/>
<br/>
shopping_list<br/>
{ <br/>
milk = 2 ; <br/>
bread = 1 ; <br/>
apples = 10 ; <br/>
} <br/>
<br/>
--> Shopping list: <br/>
--> * 2 x milk <br/>
--> * 1 x bread <br/>
--> * 10 x apples

シュガーフリー:

shopping_list ( <br/>
{ <br/>
milk = 2 ; <br/>
bread = 1 ; <br/>
apples = 10 ; <br/>
} <br/>
)

コールチェーン


前述したように、Luaの関数は別の関数(またはそれ自体)を返すことができます。 返された関数はすぐに呼び出すことができます:

function chain_print ( ... ) <br/>
print ( ... ) <br/>
return chain_print<br/>
end <br/>
<br/>
chain_print ( 1 ) ( "alpha" ) ( 2 ) ( "beta" ) ( 3 ) ( "gamma" ) <br/>
--> 1 <br/>
--> alpha <br/>
--> 2 <br/>
--> beta <br/>
--> 3 <br/>
--> gamma

上記の例では、文字列リテラルを囲む括弧を省略できます。

chain_print ( 1 ) "alpha" ( 2 ) "beta" ( 3 ) "gamma"

わかりやすくするために、「トリック」のない同等のコードを示します。

do <br/>
local tmp1 = chain_print ( 1 ) <br/>
local tmp2 = tmp1 ( "alpha" ) <br/>
local tmp3 = tmp2 ( 2 ) <br/>
local tmp4 = tmp3 ( "beta" ) <br/>
local tmp5 = tmp4 ( 3 ) <br/>
tmp5 ( "gamma" ) <br/>
end

方法


Luaのオブジェクト-ほとんどの場合、テーブルを使用して実装されます。

メソッドは通常、文字列識別子キーでテーブルにインデックスを付けることで取得した関数値を隠します。

Luaは、メソッドを宣言して呼び出すための特別な構文糖-コロンを提供します。 コロンは、メソッドへの最初の引数-self、オブジェクト自体を隠します。

次の3つの記録形式は同等です。 グローバル変数myobjが作成され、その中に単一のメソッドfooを使用してオブジェクトテーブルが書き込まれます。

コロン付き:

myobj = { a_ = 5 } <br/>
<br/>
function myobj:foo ( b ) <br/>
print ( self.a_ + b ) <br/>
end <br/>
<br/>
myobj:foo ( 37 ) --> 42

コロンなし:

myobj = { a_ = 5 } <br/>
<br/>
function myobj.foo ( self, b ) <br/>
print ( self.a_ + b ) <br/>
end <br/>
<br/>
myobj.foo ( myobj, 37 ) --> 42

完全にシュガーフリー:

myobj = { [ "a_" ] = 5 } <br/>
<br/>
myobj [ "foo" ] = function ( self, b ) <br/>
print ( self [ "a_" ] + b ) <br/>
end <br/>
<br/>
myobj [ "foo" ] ( myobj, 37 ) --> 42

注:ご覧のとおり、コロンを使用せずにメソッドを呼び出すと、myobjが2回言及されます。 次の2つの例は、get_myobj()が副作用で実行された場合、明らかに同等ではありません

コロン付き:

get_myobj ( ) :foo ( 37 )

コロンなし:

get_myobj ( ) .foo ( get_myobj ( ) , 37 )

コードをコロンオプションと同等にするには、一時変数が必要です。

do <br/>
local tmp = get_myobj ( ) <br/>
tmp.foo ( tmp, 37 ) <br/>
end

コロンを介してメソッドを呼び出すときに、メソッドが明示的な引数(文字列リテラルまたはテーブルコンストラクター)のみを受け取る場合は、括弧を省略することもできます。

foo:bar "" <br/>
foo:baz { }

実装


これで、宣言型コードが機能するために必要なほぼすべてのことがわかりました。 どのように見えるか思い出させてください:

build_message_box = gui:dialog "Message Box" <br/>
{ <br/>
gui:label "Hello, world!" { font_size = 20 } ; <br/>
gui:button "OK" { } ; <br/>
}

そこには何が書かれていますか?


宣言的な「トリック」なしで同等の実装を提供します。

do <br/>
local tmp_1 = gui : label ( "Hello, world!" ) <br/>
local label = tmp_1 ( { font_size = 20 } ) <br/>
<br/>
local tmp_2 = gui : button ( "OK" ) <br/>
local button = tmp_2 ( { } ) <br/>
<br/>
local tmp_3 = gui : dialog ( "Message Box" ) <br/>
build_message_box = tmp_3 ( { label, button } ) <br/>
end

GUIオブジェクトインターフェイス


ご覧のとおり、すべての作業は、guiオブジェクト(build_message_box()関数の「コンストラクター」)によって行われます。 インターフェイスのアウトラインが表示されます。

それらを擬似コードで説明します。

 gui:ラベル(タイトル:文字列)
   =>関数(パラメーター:テーブル):[gui_element]

 gui:ボタン(テキスト:文字列)
   =>関数(パラメーター:テーブル):[gui_element]
   
 gui:ダイアログ(タイトル:文字列) 
   =>関数(element_list:テーブル):関数

宣言的方法


GUIオブジェクトインターフェイスは、テンプレートを明確に示しています。これは、引数の一部を取り、残りの引数を取り、最終結果を返す関数を返すメソッドです。

簡単にするために、既存のgui_builder APIの上に宣言型モデルを構築していることを前提としています。これは、記事の冒頭にある必須の例に記載されています。 サンプルコードを思い出させてください。

function build_message_box ( gui_builder ) <br/>
local my_dialog = gui_builder:dialog ( ) <br/>
my_dialog:set_title ( "Message Box" ) <br/>
<br/>
local my_label = gui_builder:label ( ) <br/>
my_label:set_font_size ( 20 ) <br/>
my_label:set_text ( "Hello, world!" ) <br/>
my_dialog:add ( my_label ) <br/>
<br/>
local my_button = gui_builder:button ( ) <br/>
my_button:set_title ( "OK" ) <br/>
my_dialog:add ( my_button ) <br/>
<br/>
return my_dialog<br/>
end

gui:dialog()メソッドがどのようになるか想像してみましょう:

function gui:dialog ( title ) <br/>
return function ( element_list ) <br/>
<br/>
-- build_message_box(): <br/>
return function ( gui_builder ) <br/>
local my_dialog = gui_builder:dialog ( ) <br/>
my_dialog:set_title ( title ) <br/>
<br/>
for i = 1 , #element_list do <br/>
my_dialog:add ( <br/>
element_list [ i ] ( gui_builder ) <br/>
) <br/>
end <br/>
<br/>
return my_dialog <br/>
end <br/>
<br/>
end <br/>
end

[gui_element]の状況は解消されました。 これは、対応するダイアログ要素を作成するコンストラクター関数です。

build_message_box()関数は、ダイアログを作成し、子要素のコンストラクター関数を呼び出してから、これらの要素をダイアログに追加します。 ダイアログ要素のコンストラクター関数は、構造がbuild_message_box()と非常によく似ています。 それらを生成するGUIオブジェクトメソッドも同様です。

これは、少なくとも次の一般化を示唆しています。

function declarative_method ( method ) <br/>
return function ( self, name ) <br/>
return function ( data ) <br/>
return method ( self, name, data ) <br/>
end <br/>
end <br/>
end

gui:dialog()はより明確に書くことができます:

gui.dialog = declarative_method ( function ( self, title, element_list ) <br/>
return function ( gui_builder ) <br/>
local my_dialog = gui_builder:dialog ( ) <br/>
my_dialog:set_title ( title ) <br/>
<br/>
for i = 1 , #element_list do <br/>
my_dialog:add ( <br/>
element_list [ i ] ( gui_builder ) <br/>
) <br/>
end <br/>
<br/>
return my_dialog <br/>
end <br/>
end )

gui:label()およびgui:button()メソッドの実装が明らかになりました:

gui.label = declarative_method ( function ( self, text, parameters ) <br/>
return function ( gui_builder ) <br/>
local my_label = gui_builder:label ( ) <br/>
<br/>
my_label:set_text ( text ) <br/>
if parameters.font_size then <br/>
my_label:set_font_size ( parameters.font_size ) <br/>
end <br/>
<br/>
return my_label<br/>
end <br/>
end ) <br/>
<br/>
gui.button = declarative_method ( function ( self, title, parameters ) <br/>
return function ( gui_builder ) <br/>
local my_button = gui_builder:button ( ) <br/>
<br/>
my_button:set_title ( title ) <br/>
-- , . <br/>
<br/>
return my_button<br/>
end <br/>
end )

何を得たの?


素朴な命令型の例の読みやすさを改善する問題は、正常に解決されました。

私たちの仕事の結果、実際には、「おもちゃ」のユーザーインターフェイス(DSL)を記述するための独自のサブジェクト指向の宣言型言語であるLuahの助けを借りて実現しました。

Luaの機能により、実装は安価で非常に柔軟で強力であることが判明しました。

実生活では、すべてがもちろん複雑です。 解決する問題に応じて、私たちのメカニズムは非常に深刻な改善を必要とするかもしれません。

たとえば、ユーザーがマイクロ言語で記述する場合、実行可能コードをsandboxに配置する必要があります。 また、エラーメッセージのわかりやすさに真剣に取り組む必要があります。

説明されているメカニズムは万能薬ではなく、他のメカニズムと同様に賢明に適用する必要があります。 しかし、それにもかかわらず、この最も単純な形式であっても、宣言型コードはプログラムの可読性を大幅に向上させ、プログラマーの生活を楽にします。

完全に機能する例はここにあります

追加の読書

Source: https://habr.com/ru/post/J77413/


All Articles