Ngôn ngữ lập trình Lua: những hỏi-đáp bên lề

Tài liệu được dịch từ Lua Unofficial FAQ (uFAQ) http://www.luafaq.org/

Lua [http://www.lua.org/] là một ngôn ngữ rất nhỏ gọn. Có thể còn ít bạn đọc biết đến Lua, nhưng nếu có thời gian, bạn nên tìm hiểu xem sao. Lua được dùng rất nhiều cho việc scripting (viết những đoạn chương trình ngắn), đặc biệt là những đoạn plug-in do những người sử dụng viết thêm để mở rộng cho trình ứng dụng lớn có sẵn. Một ví dụ là trò chơi World of Warcraft (WoW), người chơi có thể điều chỉnh độ mạnh yếu của các nhân vật, hay sửa đổi một số tính chất trò chơi bằng các đoạn lệnh Lua, thật thú vị!

Bài hỏi đáp này có thể không phù hợp với bạn đọc mới lập trình, nhưng nếu bạn đã biết qua ngôn ngữ Lua hay đã từng lập trình quen với C hay Python hoặc những ngôn ngữ tương tự, thì đây là bài đọc rất thú vị và hé lộ nhiều đặc điểm hay của ngôn ngữ Lua.

Bài hướng dẫn này được chia làm hai phần: phần 1 ở đây còn phần 2 ở trang này.

uFAQ

1 Ngôn ngữ

1.1 Phải bắt đầu từ đâu?

1.2 Liệu Lua có phù hợp trong vai trò ngôn ngữ thứ nhất?

1.3 Liệu Lua có phù hợp trong vai trò ngôn ngữ thứ hai?

1.4 Có trình biên tập và gỡ lỗi nào tốt không?

1.5 Lua có vẻ dài dòng. Tại sao nó không như C?

1.5.1 Tại sao mảng trong Lua lại được đếm từ số 1?

1.5.2 Tôi có thể dùng một biểu thức kiểu như “x ? y : b” của C không?

1.6 Làm thế nào để bắt những truy cập biến không được định nghĩa trong Lua?

1.7 Lua có hỗ trợ Unicode không?

1.8 Có mẹo gì về tối ưu hóa không?

1.9 Khi nào cần phải lo lắng về bộ nhớ?

1.10 Khác biệt giữa pairs và ipairs là gì?

1.11 Tại sao cần có một toán tử riêng để nối chuỗi?

1.12 Tôi đã thấy có mã lệnh Lua gọi các hàm như strfind; nhưng sao thử không được?

1.13 Tại sao lệnh ‘for k,v in t do’ lại không hoạt động được nữa?

1.14 Làm thế nào để đưa giá trị nil vào trong một mảng?

1.15 Tôi có thể xuất một bảng thế nào?

1.16 Tại sao lệnh print ‘hello’ chạy được, còn print 42 thì không?

1.17 a.f(x) và a:f(x) khác gì nhau?

1.18 Các biến được quy định phạm vi thế nào?

1.18.1 Tại sao các biến không để mặc định là biến địa phương?

1.19 require và dofile khác gì nhau?

1.20 Làm thế nào để nạp module nhị phân một cách minh bạch?

1.21 Môi trường hàm là gì?

1.22 Các hàm và toán tử có trùng tải được không?

1.23 Làm thế nào để khiến hàm có thể nhận số lượng các tham số ít nhiều tùy ý?

1.24 Làm thế nào để hàm trả lại nhiều giá trị?

1.25 Bạn có thể truyền các tham biến được đặt tên vào trong một hàm không?

1.26 Tại sao không có lệnh continue?

1.27 Có cơ chế xử lý biệt lệ không?

1.28 Không có lớp à? Thế sao có thể lập trình được?

1.28.1 Closure là cái gì?

1.29 Có nội suy chuỗi không, kiểu như “$VAR is expanded”?

1.30 Các lời gọi đuôi được tối ưu dùng làm gì được?

1.31 Đóng gói thành một ứng dụng độc lập thế nào?

1.32 Làm thế nào tôi có thể nạp và chạy mã lệnh Lua (vốn nhiều khả năng không đáng tin cậy)?

1.33 Nhúng Lua vào trong một ứng dụng thế nào?

1.34 Soạn tài liệu cho mã lệnh Lua thế nào?

1.35 Cách kiểm tra kiểu đối số cho hàm?

1.36 Làm thế nào để kết thúc thực thi một cách êm thấm?

1.37 module() hoạt động thế nào?

1.37.1 Lời chỉ trích về module()

1.37.2 Mọi thứ sau module()?

1.38 Các bảng yếu là gì?

1.39 Khác biệt giữa những cách kí hiệu chuỗi?

1.40 Các vấn đề về tương thích giữa Windows và Unix?

Phụ trách: Steve Donovan (steve j donovan at gmail com), 2009-2011; phiên bản 2.0.

Giấy phép Creative Commons; Nội dung hỏi đáp này có thể sao chép biên tập lại dưới bất kì hình thức nào, miễn là ghi công trạng của người phụ trách.

1 Ngôn ngữ

1.1 Phải bắt đầu từ đâu?

Hỏi đáp lua.org chính thức có ở đây, vốn là nơi tìm kiếm thông thiết yếu như các bản Lua có sẵn và những vấn đề giấy phép.

Cũng có một hỏi đáp Lua Wiki chưa đầy đủ, vì vậy danh sách hỏi đáp bên lề này nhằm bổ sung những thông tin còn thiếu và trả lời càng nhiều câu hỏi càng tốt một cách hữu ích.

Lua là một ngôn ngữ động, hiện đại với cú pháp tiện dụng:

function sqr(x)
    return x*x
end

local t = {}
for i = 1,10 do
    t[i] = sqr(i)
    if i == 10 then
        print('finished '..i)
    end
end

Dù có dáng vẻ tựa Basic, song Lua lại giống JavaScript hơn; như không có cơ chế lớp hẳn hoi và cách viết tương đương giữa a['x'] với a.x. Chỉ có rất ít kiểu dữ liệu: chuỗi, số, bảng, hàm, luồng (thread) và dữ liệu người dùng (userdata). Sự đơn giản này giúp Lua nhanh và gọn gàng hơn so với những ngôn ngữ khác có công năng tương đương.

Bản Lua User’s Wiki có nhiều ví dụ hữu ích và thảo luận sâu sắc, đây là chỗ thích hợp để bắt đầu học Lua.

Cuốn sách giáo khoa Programming in Lua được viết bởi Roberto Ierusalimschy, vốn là một trong những người lập nên Lua. (Ấn bản thứ nhất của cuốn này có trên mạng.)

Nếu bạn đến với Lua từ một ngôn ngữ lập trình khác, hãy xem danh sách những lỗi thường gặp.

Và (tất nhiên), đọc tài liệu hướng dẫn, dù cho có lẽ đây không phải là điểm khởi đầu thích hợp.

1.2 Có thích hợp làm ngôn ngữ lập trình thứ nhất không?

Lua là một ngôn ngữ gọn gàng với cú pháp tiện dụng, sáng sủa vốn rất dễ đọc. Điều này khiến cho Lua là lựa chọn số một trong vai trò ngôn ngữ văn lệnh nhúng trong những trình ứng dụng lớn hơn, song cũng phù hợp để giới thiệu các khái niệm lập trình.

Các hàm đều là những giá trị hạng nhất và cũng có thể ở dạng ẩn danh, nhờ đó người học có thể thực hiện phong cách lập trình hàm. Có hỗ trợ closure nghiêm chỉnh và đệ quy gọi đuôi.

Lua được chạy dưới chế độ tương tác, nhờ vậy dễ dàng khám phá ngôn ngữ và các thư viện của nó.

Lua Wiki cũng so sánh với các ngôn ngữ khác.

1.3 Có thích hợp làm ngôn ngữ lập trình thứ hai không?

Một trường hợp là bạn đã có những ứng dụng C/C++/Java/C# (v.v.) lớn và bạn muốn cho phép người dùng viết mã lệnh tùy chỉnh những ứng dụng này. Lua sẽ chỉ làm tăng thêm 150-170K, cho bạn một ngôn ngữ mở rộng hiện đại mà hiểu được các nhu cầu riêng (như sandbox) và có thể dễ dàng tích hợp với mã lệnh sẵn có.

Trường hợp khác là bạn có rất nhiều thành phần dùng trong chuỗi công việc và bạn cần tìm một ngôn ngữ gắn kết để dễ dàng buộc chúng lại với nhau thật nhanh mà không phải qua công đoạn biên tập lại đầy phiền toái. (Xem khái niệm xung đột được Ousterhout đề xuất.)

Một trường hợp thực dụng khác nữa kết hợp cả hai trường hợp trên, đó là nhúng Lua để giúp gỡ lỗi. Hoàn toàn có thể cung cấp một trình gỡ lỗi dòng lệnh thông minh (ngay cả cho các ứng dụng giao diện như Java/Swing) chạy bằng Lua, vốn cho phép bạn thao tác bên trong một ứng dụng, truy vấn trạng thái và chạy các mã lệnh kiểm thử nhỏ.

1.4 Có trình biên tập và gỡ lỗi nào tốt?

Xem LuaEditorSupport.

Những trình biên tập và gỡ lỗi nào thông dụng đều được, gồm cả XCode.

SciTE là một trình biên tập phổ biến trong Windows và nền GTK+, ứng dụng này hỗ trợ Lua rất tốt. Phiên bản kèm trong Lua for Windows còn có thể gỡ lỗi được mã lệnh Lua. Ngoài ra, trình này còn được lập trình được bằng trình thông dịch Lua nhúng trong đó.

Có một Lua plugin cho IntelliJ IDEA.

1.5 Lua có vẻ rất dài dòng. Tại sao nó không giống như C?

Độ gọn gàng của C (và môi trường Unix mà ngôn ngữ đó phát triển) bắt nguồn từ hạn chế về kĩ thuật của các máy teleprinter cổ lỗ sĩ. Bây giờ ta có cách điền lệnh bằng phím TAB cùng những trình biên tập thông minh cho phép gõ tắt, thì viết mã lệnh Lua chẳng lâu hơn là bao so với C. Chẳng hạn, trong SciTE ta có thể thêm đoạn lệnh sau vào trong viết tắt:

if = if | then \n \
    \n \
end

Sau đó mỗi khi gõ ‘if’ và ấn ctrl-B máy sẽ tự chèn đoạn lệnh khai triển, được thụt đầu dòng đúng quy cách.

Một điều nữa là ta dành nhiều thời gian để đọc mã lệnh hơn là viết mã lệnh, bởi vậy vấn đề là mã lệnh này đọc có hay không. C rất dễ đọc với ai đã quen đọc C, song Lua dễ đọc hơn đối với người lập trình tay ngang.

Một phiên bản Lua tựa C là Squirrel.

Có một yếu tố giúp Lua được coi là C của những ngôn ngữ kiểu động; đó là Lua nhỏ, nhanh, và chỉ đi theo những công cụ thiết yếu nhất. Đây không chỉ là điểm tương đồng, vì bản hiện thực Lua chỉ dùng những gì có trong thư viện C89.

1.5.1 Tại sao các mảng trong Lua bắt đầu đếm từ số 1?

Thật kì quặc, điều khiến một số người quá khích là việc trong Lua, các mảng có chỉ số đếm từ một.

Theo kí hiệu toán học, chỉ số bắt đầu từ 1, và Lua cũng vậy. Nói cách khác, chỉ số không phải là offset.

Lý do lịch sửa (xem sự phát triển của Lua) đó là Lua phát triển để giải quyết nhu cầu kĩ thuật tại công ty dầu hỏa quốc gia Brazil (Petrobras). Ngôn ngữ được thiết kế cho các kĩ sư vốn không phải lập trình viên chuyên nghiệp, và có khả năng là họ quen dùng FORTRAN hơn.

Bạn có thể khiến cho các mảng đếm từ không, song các hàm thư viện chuẩn (như table.concat) sẽ không nhận ra phần tử số 0. Và # sẽ luôn trả lại kích thước bảng trừ đi một.

t = {[0]=0,10,20,30}  -- constructor mảng hơi lủng củng
for i = 0,#t do print(i,t[i]) end  -- không phải 0,#t-1 !

1.5.2 Liệu tôi có thể dùng một biểu thức kiểu như “x ? y : b” của C?

Biểu thức viết trong Lua là res = x and y or b  vốn hoạt động được vì giá trị của những phép toán này không phải đơn thuần là giá trị boole mà là toán hạng thứ hai. Sẽ có sự lượng giá biểu thức “ngắn mạch”, theo nghĩa biểu thức b sẽ không bị lượng giá khi điều kiện là đúng.

Tuy nhiên lưu ý trường hợp khi ynil hoặc false. Cái and thứ nhất bị thất bại và dù sao ta cũng sẽ chọn lấy b.

Lua cũng hiểu nhiều giá trị:

> print (false and 5 or 10,'hello')
10      hello

Người ta sẽ thường viết một hàm phụ trợ:

function choose(cond,a,b)
    if cond then return a else return b end
end

Cách này định giá cả hai giá trị ngay từ đầu, bởi vậy nó chỉ phù hợp với những giá trị đơn giản, song lại rất an toàn.

1.6 Làm thế nào để can thiệp việc truy cập những biến chưa được định nghĩa trong Lua?

Mặc định là một biến chưa biết sẽ được tra trong bảng toàn cục hoặc bảng modul và giá trị của nó sẽ là nil. Đây là một giá trị hợp lệ cho biến, bởi vậy việc gõ nhầm tên biến sẽ có kết quả rất hay, ngay cả khi ta tránh dùng những biến toàn cục.

Có một số cách tiếp cận để áp đặt sự chặt chẽ đối với biến toàn cục. Cách dễ nhất là kiểm tra một cách động xem một biến toàn cục đã cho được khởi tạo minh bạch hay chưa; nghĩa là liệu nó có được đặt bằng nil ở đâu đó không (xem strict.lua trong thư mục etc của bản phân phối Lua bạn dùng.)

File tests/globals.lua trong bản phân phối lại có cách tiếp cận khác, đó là truyền mã bytecode của trình biên dịch luac qua grep, tìm những truy cập toàn cục; song điều này chỉ giúp ích nếu bạn ‘tránh biến toàn cục như tránh hủi’. Nói riêng, có những hàm toàn cục và bảng toàn cục cung cấp bởi thư viện chuẩn.

lua-checker là một công cụ kiểm tra mã lệnh tựa lint.

LuaInspect cho phép phân tích mã lệnh Lua theo thời gian thực bằng [SciTE]((http://www.scintilla.org/SciTE.html) và một plugin vim; cũng hỗ trợ kết quả đầu ra tĩnh như HTML. Điều này cho phép tô màu mã lệnh theo ngữ nghĩa cho Lua, để các biến địa phương được đánh dấu và có thể truy cập được, còn những biến toàn cục chưa biết được dán nhãn màu đỏ.

1.7 Lua có hỗ trợ Unicode không?

Có và không. Các chuỗi lua Lua có thể chứa kí tự bất kì (chuỗi không kết thúc bởi NUL) vì vậy chuỗi Unicode có thể được truyền quanh mà không gặp vấn đề gì, cũng như dữ liệu nhị phân. Tuy nhiên, những thư viện chuỗi sẵn có đều xét các kí tự 1-byte, nên cần phải có thư viện đặc biệt; xem slnunicode hay ICU4Lua.

1.8 Những mẹo nào về tối ưu hóa?

Câu hỏi đầu tiên là, bạn thực sự gặp vấn đề chưa? Có phải chương trình chưa đủ nhanh? Hãy nhớ ba yêu cầu cơ bản về hệ thống: Đúng đắn, Mạnh mẽ và Hiệu quả, và quy tắc cơ bản trogn kĩ thuật là bạn chỉ có thể chọn hai trong số 3 yêu cầu đó thôi.

Lời nói của Donald Knuth về tối ưu hóa thường được nhắc tới: “Nếu bạn tối ưu hóa mọi thức, bạn sẽ luôn thấy bất hạnh” và “ta nên quên đi những chỗ kém hiệu quả nhỏ, chẳng hạn 97% thời gian: tối ưu hóa sớm là căn nguyên của mọi điều xấu.”

Cứ coi một chương trình là đúng đắn và (hi vọng là) mạnh mẽ. Có một cái giá nhất định trong việc tối ưu hóa chương trình này, cả đối với thời gian lập trình và mức độ dễ đọc mã lệnh. Nếu bạn không biết chỗ nào mã lệnh chậm, thì bạn sẽ tốn thời gian làm cho cả chương trình xấu xí và có thể chỉnh nhanh hơn chút (vì vậy mà ông Knuth nói rằng bạn sẽ thấy bất hạnh.) Cho nên bước thứ nhất là dùng chương trình LuaProfiler để tìm những phần có hiệu quả kém, vốn sẽ là những hàm thường được gọi nhiều và các phép toán nằm sâu trong những vòng lặp bên trọng. Chỉ có đây là những phần mà việc tối ưu hóa sẽ đem lại kết quả, và thường chúng chỉ là phần nhỏ trong chương trình, nên tổng cộng bạn chỉ phải làm phần việc nhỏ, và sẽ thấy hạnh phúc hơn và phạm ít điều xấu xa trong đống mã lệnh xấu.

Có một số mẹo riêng với Lua ở đây. Nếu máy bạn có cài LuaJIT, hãy sử dụng nó.

Trong Lua, sự tương đương với việc viết những vòng lặp trong bằng assembly, đó là đánh dấu đoạn mã lệnh dùng CPU nhiều, viết nó bằng C, và dùng như một module mở rộng. Nếu làm đúng, bạn sẽ có một chương trình Lua với hiệu năng gần như bằng C, nhưng ngắn hơn và dễ bảo trì hơn.

Với LuaJIT bạn có thể không cần dùng C chút nào, đặc biệt là vì foreign-function interface FFI có sẵn khiến cho việc truy cập các hàm và cấu trúc C bên ngoài rất hiệu quả. Điều này cũng có thể cho ta những cách biểu diễn mảng chứa kiểu dữ liệu C với dung lượng hiệu quả hơn, so với việc dùng các bảng.

1.9 Khi nào tôi cần phải lo về bộ nhớ?

Câu trả lời gọn là: chỉ khi nào bạn phải lo thực sự. Thường thì Lua làm điều đúng, nhưng nếu nó dùng quá nhiều bộ nhớ thì bạn phải hiểu đôi điều về cách quản lý bộ nhớ trong Lua.

Người ta có thể lo lắng về việc có quá nhiều đối tượng chuỗi trong bộ nhớ. Nhưng Lua lại ghi chuỗi bên trong, bởi vậy mỗi chuỗi riêng chỉ có đủng một phiên bản duy nhất. Và mã lệnh sau đây dùng ít bộ nhớ hơn là bạn tưởng:

local t = {}
for i = 1,10000 do
    t[i] = {firstname = names[i], address = addr[i]}
end

Nó cũng giúp cho việc so sánh chuỗi bằng nhau thực hiện rất nhanh.

Nối quá nhiều chuỗi có thể sẽ chậm và kém hiệu quả. Sau đây là cách đọc sai lầm nội dung của file vào một chuỗi:

local t = ""
for line in io.lines() do
  t = t .. line .. '\n'
end

Điều này tệ, và cũng tương tự khi thực hiện trong Java hoặc Python; các chuỗi không thể thay đổi được, và việc làm vừa rồi là liên tục tạo ra và hủy bỏ các chuỗi.

Nếu bạn cần phải nối rất nhiều chuỗi, hãy đặt nó vào trong một bảng rồi ghép lại bằng table.concat()

Lua, cũng như đa số ngôn ngữ lập trình hiện đại, có thu hồi bộ nhớ tự động. Bộ nhớ được huy động cho các bảng, v.v. đều được tự động thu lại sau khi dữ liệu không còn được tham chiếu đến nữa. Chẳng hạn ta có t = {1,2,3} rồi sau đó gán t = {10,20,30}; bảng ban đầu sẽ trở nên lẻ loi, và khi bộ thu hồi đến thì bảng đó sẽ bị đưa đi.

Vì vậy điều đầu tiên cần nhớ là bộ nhớ dữ liệu sẽ chỉ được giải phóng nếu bạn chính thức không tham chiếu đến dữ liệu đó. Và thứ hai, thường thì lúc thu hồi là lúc bạn không định trước; song bạn cũng có thể ra lệnh thu hồi ngay bằng collectgarbage('collect'). Một chi tiết ở đây là thường phải gọi hàm trên hai lần, vì có những hàm kết thúc (finalizer) sẽ được xếp lịch thực hiện khi gọi thu hồi lần thức nhất, và hàm kết thúc đó mới chính thức thực hiện ở lần gọi thứ hai.

1.10 pairs và ipairs khác gì nhau?

Các bảng và cấu trúc dữ liệu tổng quát ở Lua có thể được xem như ‘giống mảng’ và cũng ‘giống ánh xạ’. Ý tưởng là bạn có thể dùng chúng một cách hiệu quả với vai trò mảng co giãn được với những chỉ số nguyên, song cũng có thể đánh chỉ số chúng với bất kì giá trị khác như một ánh xạ hash. pairs(t) cung cấp một bộ lặp chung qua các khóa và giá trị trong một bảng, dù là số hay không, mà không theo thứ tự nào; còn ipairs(t) thì duyệt theo phần mảng theo đúng thứ tự.

Các bảng có thể chứa hỗn hợp các phần tử mảng và ánh xạ:

t = {fred='one',[0]=1; 10,20,30,40}

Không cần có dấu chấm phẩy ở trên, song nó có thể được dùng làm một dấu ngăn cách thứ hai (ngoài dấu phẩy) trong lệnh tạo bảng. Chỉ vì 0 là một chỉ số không có là nó phải là chỉ số của mảng! Mà theo định nghĩa, chỉ số mảng phải từ 1 đến #t.

Hai lệnh sau là tương đương với mục đích truy cập các phần tử của mảng:

for i,v in ipairs(t) do f(i,v) end

for i=1,#t do f(i,t[i]) end

Các bảng trong Lua biểu diễn mảng thưa một cách tự nhiên:

t = {[1] = 200, [2] = 300, [6] = 400, [13] = 500}

Trong trường hợp này, #t không đáng tin cậy, bởi toán tử độ dài # chỉ được định nghĩa cho các mảng không thưa (mảng không có vị trí rỗng). Nếu bạn muốn theo dõi kích thước của một mảng thưa, thì cách tốt nhất là giữ một biến đếm và tăng dần giá trị của nó:

t[idx] = val;
t.n = t.n + 1

Sẽ hơi bất tiện nếu chỉ muốn duyệt qua phần không phải mảng của một bảng:

local n = #t
for k,v in pairs(t) do
  if type(k) ~= 'number' or k < 1 or k > n then
    print(k,v)
  end
end

Tại sao không đặt ra một phép lặp cho việc này? Lý do là các mảng thì có phần mảng và phần ánh xạ trong chi tiết về lập trình rồi, chứ không phải thuộc về tính năng nữa. (Cso thể diễn đạt trực tiếp đoạn mã lệnh trên dưới dạng một phép lặp tự tạo.)

1.11 Tại sao cần một toán tử riêng cho phép nối chuỗi?

Việc có riêng toán tử .. sẽ hạn chế khả năng nhầm lẫn.

Phép trùng tải toán tử ‘+’ phục vụ nối chuỗi đã trở thành truyền thống lập trình. Nhưng nối chuỗi không phải là phép cộng, và sẽ có ích nếu ta phân biệt được hai khái niệm này. Trong Lua, các chuỗi có thể được đổi thành số lúc thích hợp (ví dụ 10+’20’) và số có thể đổi thành chuỗi (ví dụ 10 .. 'hello'). Việc có toán tử riêng thì đồng nghĩa với tránh nhầm lẫn, như đã xảy ra điển hình trong JavaScript.

Khi trùng tải toán tử, sự phân biệt này vẫn hữu ích. Chẳng hạn, một lớp List có thể được tạo ra trong đó .. một lần nữa có nghĩa là ghép nối, tức là nối các danh sách khác nhau để tạo nên danh sách dài hơn. Ta cũng có thể định nghĩa phép + với danh sách các số, khi đó phép toán này nghĩa là cộng từng phần tử, theo đó v+u là một danh sách chứa các tổng phần tử của v và u, nghĩa là chúng được coi như những vectors.

1.12 Tôi đã từng thấy mã lệnh Lua có viết các hàm như strfind; nhưng sao tôi thử không được?

Các hàm như strfind chỉ có trong mã lệnh Lua 4.0; bây giờ các hàm này nằm trong bảng, như string.find. Hãy tra mục thư viện chuẩn của tài liệu hướng dẫn.

Trong Lua 5.1, mọi chuỗi đều có một bảng meta chỉ đến bảng string, bởi vậy s:find('HELLO') tương đương với string.find(s,'HELLO').

1.13 Tại sao ‘for k,v in t do’ không còn hoạt động nữa?

Đây là một trong những thay đổi lớn từ phiên bản Lua 5.0 đến 5.1. Mặc dù gõ câu lệnh như vậy thì rất tiện tay, song cách viết không rõ ràng bằng việc nêu chính xác các cặp khóa – giá trị được phát sinh thế nào. Lệnh này tương đương với cách viết pairs(t), và vì vậy cách viết cũ không phù hợp với trường hợp chung khi cần lặp qua các phần tử của mảng theo thứ tự.

Trong câu lệnh for , t phải là một hàm. Ở trường hợp đơn giản nhất, nó phải là một hàm trả lại giá trị mỗi khi được gọi đến, và phải trả lại nil khi vòng lặp kết thúc.

Rất dễ viết các hàm lặp. Sau đây là một hàm lặp đơn giản qua tất cả các phần tử trong mảng, hàm lặp này hoạt động hệt như ipairs:

function iter(t)
  local i = 1
  return function()
     local val = t[i]
     if val == nil then return nil end
     local icurrent = i
     i = i + 1
     return icurrent,val
  end
end

for i,v in iter {10,20,30} do print(i,v) end

1.14 Cách đặt một giá trị nil vào trong một mảng thế nào?

Việc đặt nil trực tiếp vào một mảng sẽ gây ra một ‘lỗ hổng’ khiến cho toán tử độ dài bị nhầm lẫn. Có hai cách khắc phục điều này. Một cách là dùng mảng thưa với kích cỡ được nêu rõ. Khi đó, hàm lặp phải trả về cả chỉ số và giá trị:

local sa = {n=0}

function append(t,val)
   t.n = t.n + 1
   t[t.n] = val
end

function sparse_iter(t)
  local i,n = 1,t.n  -- ở đây phải viết cụ thể là t.n ! Còn #t sẽ không dùng được
  return function()
   local val = t[i]
   i = i + 1
   if i > n then return nil end
   return i,val
  end
end

append(sa,10)
append(sa,nil)
append(sa,20)

for i,v in sparse_iter(sa) do ... end

Cách khác là lưu trữ một giá trị độc nhất, đặc biệt là Sentinel thay cho nil thực thụ; chỉ cần viết Sentinel = {} là được. Các vấn đề này được bàn luận kĩ hơn trong wiki.

1.15 Làm thế nào để sổ nội dung của một bảng?

Đây là một trong những điều cần đến trợ giúp của thư viện “hơi chính thức”. Câu hỏi đầu tiên là, bạn muốn sổ nội dung của bảng ra để gỡ lỗi, hay là viết nội dung những bảng lớn để sau này nạp lại chúng?

Một giải pháp đơn giản cho vấn đề thứ nhất:

function dump(o)
    if type(o) == 'table' then
        local s = '{ '
        for k,v in pairs(o) do
                if type(k) ~= 'number' then k = '"'..k..'"' end
                s = s .. '['..k..'] = ' .. dump(v) .. ','
        end
        return s .. '} '
    else
        return tostring(o)
    endend

Tuy vậy, cách này không hiệu qủa lắm với những bảng lớn vì có những phép nối chuỗi trong đó, và sẽ gây rắc rối nếu bạn có các tham chiếu vòng tròn.

PiL chỉ dẫn cách xử lý các bảng cả có lẫn không tham chiếu vòng tròn.

Những phương án khác cũng có ở đây.

Và nếu bạn muốn hiểu một tập hợp phức tạp gồm các bảng có liên hệ với nhau, thì cách vẽ sơ đồ sẽ là tốt nhất để biểu thị mối liên hệ; bài viết này cho một đoạn mã lệnh nhằm tạo ra hiển thị Graphviz về những mối liên hệ bảng thế này. Còn đây là một kết qủa ví dụ.

1.16 Tại sao print ‘hello’ lại thực hiện được, nhưng print 42 thì không?

Có sự nhầm lẫn này là vì một số ngôn ngữ (như Python 2.x) có câu lệnh print, còn print lại là một hàm trong Lua (hay trong Python 3).

Tuy  nhiên, có hai trường hợp mà bạn có thể bỏ cặp ngoặc đơn đi khi gọi hàm trong Lua function: bạn có thể truyền đúng một gía trị kiểu chuỗi hay kiểu bảng. Bởi vậy gọi một hàm như myfunc{a=1,b=2} là hoàn toàn được. Cách viết tiện dụng này có từ truyền thống của Lua là một ngôn ngữ mô tả dữ liệu.

1.17 a.f(x) và a:f(x) khác nhau thế nào?

a.f đơn giản nghĩa là ‘tra tìm f trong a’. Nếu kết qủa là dạng ‘gọi được’ thì bạn có thể gọi nó. Một cách làm mẫu thông dụng là lưu trữ các hàm trong một bảng; cách này hay được dùng trong những thư viện chuẩn, ví dụ table.insert , v.v.

a:f thực ra bản thân nó thì không có nghĩa cụ thể. a:f(x) là dạng viết tắt của a.f(a,x) – nghĩa là tra tìm hàm trong bối cảnh của đối tượng a, rồi gọi nó bằng việc truyền đối tượng a như tham biến đầu.

Cho trước một đối tượng a, nhiều khi người lập trình mắc lỗi gọi phương thức bằng dấu chấm. Đành rằng có phương thức như vậy, nhưng tham biến self lại không được đặt, và phương thức sẽ bị lỗi khi chạy, đưa ra thông báo rằng tham số thứ nhất không phải là đối tượng mà phương thức trông đợi.

1.18 Các biến được phân định tầm ảnh hưởng thế nào?

Theo mặc định, các biến có tầm ảnh hưởng toàn bộ (hay toàn cục), và chúng chỉ có ảnh hưởng cụ bộ nếu là các đối số của hàm hay được khai báo rõ là local. (Điều này trái ngược với quy tắc trong Python, ở đó bạn phải dùng từ khóa global.)

G = 'hello'  -- toàn bộ 

function test (x,y)  -- x,y chỉ cục bộ trong test
  local a = x .. G
  local b = y .. G
  print(a,b)
end

Các biến cục bộ được ‘phân vùng theo ngữ vựng’, và bạn có thể khai báo bất kì biến nào bên trong một khối lệnh được lồng ghép mà không ảnh hưởng đến các lệnh ngoài khối.

do
  local a = 1
  do
    local a = 2
    print(a)
  end
  print(a)
end

=>
2
1

Có một khả năng khác về tầm ảnh hưởng. Nếu một biến không phải là cục bộ, thì nói chung nó sẽ được đặt trong tầm ảnh hưởng của module đó,  vốn thường là trong bảng toàn bộ (_G nếu bạn muốn viết rõ ra) nhưng được định nghĩa lại bằng hàm module:

-- mod.lua (trong đường dẫn module của Lua)
module 'mod'

G = 1  -- bên trong module này

function fun(x) return x + G end --như trên

-- moduser.lua (ở bất kì đâu)
require 'mod'
print(mod.G)
print(mod.fun(41))

1.18.1 Tại sao các biến không mặc định là có tầm ảnh hưởng cục bộ?

Dĩ nhiên, có vẻ sẽ rất dễ nếu ta chỉ phải khai báo rõ ràng các biến cục bộ trong bối cảnh một đoạn mã lệnh địa phương. Câu trả lời ngắn gọn là: Lua không phải là Python, nhưng thực ra có những lí do hợp lý về việc các biến cục bộ với tầm ảnh hưởng theo ngữ vựng phải được khai báo rõ ràng. Xem  trang wiki.

1.19 require và dofile khác gì nhau?

requiredofile đều nạp và chạy các file Lua. Khác biệt thứ nhất là bạn truyền tên module đến require và một đường dẫn đến file cho dofile.

Như vậy require dùng đường dẫn tìm kiếm nào? Nó được chứa trong giá trị của package.path: trên một hệ thống Unix thì các đường dẫn này sẽ có dạng như (lưu ý các dấu chấm phẩy dùng để phân biệt từng đường dẫn):

$> lua -e "print(package.path)"
./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

Trong hệ thống Windows:

C:\>lua -e "print(package.path)"
.\?.lua;C:\Program Files\Lua\5.1\lua\?.lua;C:\Program Files\Lua\5.1\lua\?\init.
lua;C:\Program Files\Lua\5.1\?.lua;C:\Program Files\Lua\5.1\?\init.lua;C:\Progra
m Files\Lua\5.1\lua\?.luac

Cần lưu ý rằng bản thân Lua không biết gì về thư mục; nó chỉ lấy tên một  module rồi thay thế nó là ‘?’ trong từng chuỗi mẫu và xem liệu file có tồn tại không; nếu có thì nạp file này.

Một thông báo lỗi rất cụ thể sẽ bật lên nếu bạn cố thử require một module mà không tồn tại. Điều này cho bạn thấy rõ ràng cách Lua đang tìm kiếm module đó bằng cách khớp các mục đường dẫn:

$> lua -lalice
lua: module 'alice' not found:
        no field package.preload['alice']
        no file './alice.lua'
        no file '/home/sdonovan/lua/lua/alice.lua'
        no file '/home/sdonovan/lua/lua/alice/init.lua'
        no file './alice.so'
        no file '/home/sdonovan/lua/clibs/alice.so'
        no file '/home/sdonovan/lua/clibs/loadall.so'

Điểm khác biệt thứ hai là require theo dõi những module nào đã được nạp, bởi vậy việc gọi lại chúng sẽ không khiến cho module bị nạp lại.

Điểm khác biệt thứ ba là require cũng sẽ nạp các module nhị phân. Theo cách tương tự, package.cpath được dùng để tìm các module – đó là các thư viện chia sẻ (hay DLL) vốn xuất các hàm khởi tạo.

$> lua -e "print(package.cpath)"
./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so

C:\>lua -e "print(package.cpath)"
.\?.dll;.\?51.dll;C:\Program Files\Lua\5.1\?.dll;C:\Program Files\Lua\5.1\?51.dl
l;C:\Program Files\Lua\5.1\clibs\?.dll;C:\Program Files\Lua\5.1\clibs\?51.dll;C:
\Program Files\Lua\5.1\loadall.dll;C:\Program Files\Lua\5.1\clibs\loadall.dll

package.pathpackage.cpath có thể thay đổi được, bằng cách này sẽ thay đổi biểu hiện của require. Lua cũng sẽ dùng các biến môi trường LUA_PATHLUA_CPATH nếu chúng được định nghĩa, để ghi đè lên các đường dẫn mặc định.

Thế ‘./?.lua’ sẽ khớp với gì? Chuỗi này khớp với bất kì module Lua nào trong thư mục hiện thời, vốn rất tiện dụng cho việc kiểm thử chương trình.

Tại sao lại có mẫu ‘/usr/local/share/lua/5.1/?/init.lua’? Cái này cho phép đặt các gói độc lập hoàn chỉnh chứa những module. Bởi vậy nếu có một thư mục ‘mylib’ trong đường dẫn Lua, thì require 'mylib' sẽ nạp ‘mylib/init.lua’.

Điều gì sẽ xảy ra nếu tên module có chứa dấu chấm, như require 'socket.core? Lua sẽ thay thế dấu chấm bằng các dấu phân cách thư mục phù hợp (như ‘socket/core’ hay ‘socket\core’) và thực hiện tìm kiếm theo dạng này. Bởi vậy trong hệ thống Unix, nó sẽ thay thế ‘?’ trong ‘/usr/local/lib/lua/5.1/?.so’ để cho ra ‘/usr/local/lib/lua/5.1/socket/core.so’ vốn khớp với mẫu đã cho.

1.20 Cách nạp module nhị phân một cách minh bạch?

Một module nhị phân là một thư viện chia sẻ trong đó phải có một điểm dẫn vào với tên gọi đặc biệt. Chẳng hạn, nếu tôi có một module nhị phân là fred thì hàm khởi tạo của nó phải có tên là luaopen_fred. Mã lệnh sau sẽ nạp fred một cách rõ ràng, với một đường dẫn đầy đủ cho trước:

local fn,err = package.loadlib('/home/sdonovan/lua/clibs/fred.so','luaopen_fred')
if not fn then print(err)
else
  fn()
end

(Trong Windows, hãy dùng phần mở rộng .dll)

Lưu ý rằng bất kì hàm nào được xuất bởi một thư viện chia sẻ thì đều có thể gọi được kiểu này, miễn là bạn biết tên nó. Khi viết các phần mở rộng trong C++, điều quan trọng là phải xuất điểm dẫn vào bằng cách viết extern "C", nếu không tên gọi sẽ bị hỏng. Trong Windows, bạn phải đảm bảo rằng ít nhất điểm dẫn vào này phải được xuốt qua lệnh __declspec(dllexport) hay dùng một file .def .

1.21 Các môi trường hàm là gì?

setfenv có thể thiết lập môi trường cho một hàm. Thường thì môi trường của hàm là một bảng có tầm ảnh hưởng toàn bộ, song điều này có thể thay đổi được. Điều này rất có lợi cho sandbox, vì bạn có thể kiểm soát được bối cảnh thực hiện mã lệnh, và ngăn không cho mã lệnh đó thực hiện điều xấu. Nó cũng được dùng để thực hiện phạm vi module.

function test ()
    return A + 2*B
end

t = { A = 10; B = 20 }

setfenv(test,t)

print(test())
=>
50

Các môi trường hàm bị gỡ bỏ từ phiên bản Lua 5.2 trở đi, song vẫn có những cách làm tương đương với tính năng không kém. Chẳng hạn, bạn muốn biên dịch mã lệnh trong một môi trường khởi đầu riêng; hàm load mở rộng có thể là mã lệnh được truyền, là một tên gọi mã lệnh, một kiểu chế độ (như “t” ở đây viết tắt cho text only – chỉ dạng chữ) và một bảng môi trường (có hoặc không đều được).  load biên dịch mã lệnh này và trả về một hàm function cần được lượng gía:

Lua 5.2.0 (beta)  Copyright (C) 1994-2011 Lua.org, PUC-Rio
> chunk = load("return x+y","tmp","t",{x=1,y=2})
> = chunk
function: 003D93D0
> = chunk()
3

Bạn có thể dùng cách này để nạp các đoạn mã lệnh do người dùng viết trong một môi trường được kiểm soát; trường hợp này bảng ‘toàn cục’ chỉ chứa hai biến đã chỉ định.

Ví dụ về setfenv trong Lua 5.1 có thể được viết bằng biến đặc biệt _ENV trong Lua 5.2:

> function test(_ENV) return A + 2*B end
> t = {A=10; B=20}
> = test(t)
50

1.22 Các hàm và toán tử có thể được trùng tải (overload) không?

Hàm không thể trùng tải được, ít nhất là không được lúc biên dịch.

Tuy nhiên, bạn có thể ‘trùng tải’ dựa trên các kiểu của đối số mà bạn nhận được. Chẳng hạn, ta cần một hàm có thể thao tác được với một hay nhiều con số:

function overload(num)
  if type(num) == 'table' then
    for i,v in ipairs(num) do
      overload(v)
    end
  elseif type(num) == 'number' then
    dosomethingwith(num)
  end
end

Các toán tử thông dụng có thể trùng tải được bằng các phương thức meta (metamethods). Nếu một đối tượng (một bảng Lua hay một userdata của C) có một bảng meta thì ta có thể điều khiển được ý nghĩa của các toán tử số học (như + – * / ^), phép nối chuỗi .., phép gọi hàm () và toán tử so sánh == ~= < >.

Việc trùng tải () cho phép tồn tại các ‘đối tượng hàm’ hay functor, vốn có thể được dùng đến mỗi khi Lua cần một đối tượng nào đó gọi được.

Lưu ý một điểm hạn chế về các toán tử trùng tải như ==: cả hai đối số phải có cùng kiểu dữ liệu. Thật không may là bạn không thể tạo ra một lớp SuperNumber rồi gán sn == 0 được. Bạn phải tạo ra một thực thể của SuperNumber gọi là Zero và sử dụng nó vào trong việc so sánh.

Bạn có thể điều khiển ý nghĩa của a[i] nhưng đây không nói riêng về việc trùng tải toán tử [] . __index được kích hoạt khi Lua không thể tìm được một khóa bên trong bảng. __index có thể được đặt là một bảng hay một hàm; các đối tượng thường được thực hiện bằng việc đặt  __index thành bản thân bảng meta đó, và bằng việc đặt các phương thức vào trong bảng meta này. Vì a.fun tương đương với a['fun'], không có cách nào để phân biệt giữa việc tra tìm bằng chỉ số rõ ràng và việc dùng dấu chấm. Một lớp Set nôm na sẽ đặt những phương thức nào đó vào trong bảng meta rồi lưu các phần tử trong tập hợp như những khóa trong bản thân đối tượng. Nhưng nếu Set có phương thức là ‘set’, thì s['set'] sẽ luôn đúng, mặc dù ‘set’ không phải là thành viên của set.

Các toán tử kích thước và hành động khi được gom rác bộ nhớ đều có thể được ghi đè bằng các phần mở rộng C, chứ không thể chỉ bằng Lua 5.1. hạn chế này đã được dỡ bỏ trong Lua 5.2.

1.23 Có cách nào để các hàm nhận số lượng biến đổi các đối số không?

Các hàm Lua rất “dễ tính” với những đối số dư thùa; bạn có thể lấy một hàm không đối số rồi truyền đối số cho nó; và những đối số này đơn giản là bị vứt bỏ.

Cú pháp của một hàm nhận số lượng đối số không định rõ (‘hàm variadic’) cũng giống như trong C:

function vararg1(arg1,...)
  local arg = {...}
  -- dùng arg như một bảng để truy cập các đối số
end

Vậy ... là gì?  Đó là cách viết tắt cho tất cả những đối số không đặt tên. Vậy function I(...) return ... end là một hàm ‘identity’ tổng quát, vốn nhận một số tùy ý các đối số rồi trả lại những gía trị này. Trong trường hợp đó, {...} sẽ được điền bởi những gía trị của tất cả đối số thêm.

Nhưng vararg1 có một vấn đề không nhỏ. nil chắc chắn là một gía trị phải bỏ qua, nhưng sẽ gây nên một lỗ hổng trong bảng arg, nhĩa là # sẽ trả lại gía trị sai.

Một cách làm chắc chắn hơn là:

function vararg2(arg1,...)
  local arg = {n=select('#',...),...}
  -- arg.n is the real size
  for i = 1,arg.n do
    print(arg[i])
  end
end

Ở đây ta dùng hàm select để tìm ra số đối số thực sự được truyền vào. Đây là một dạng mẫu thông dụng với các vararg đối nỗi nó trở thành một hàm mới có tên gọi table.pack trong Lua 5.2.

Đôi khi bạn sẽ thấy đoạn mã lệnh cũ sau; nó tương đương với vararg2:

function vararg3(arg1,...)
  for i = 1,arg.n do
    print(arg[i])
  end
end

Tuy nhiên, arg được dùng theo cách này đã lỗi thời, và sẽ không áp dụng được với LuaJIT và Lua 5.2.

1.24 Làm thế nào để trả lại nhiều gía trị từ một hàm?

Một hàm Lua có thể trả lại nhiều gía trị:

function sumdiff(x,y)
  return x+y, x-y
end

local a,b = sumdiff(20,10)

Lưu ý điểm khác biệt giữa Lua và Python; Python trả lại nhiều gía trị bằng cách ghép chúng vào cùng một bộ (tuple), còn Lua thì trả lại nhiều gía trị theo đúng nghĩa của nó, và làm điều này rất hiệu qủa. Phép gán sau tương đương với gán bội trong Lua

local a,b = 30,10

Nếu hàm của bạn thiết lập nên một bảng kiểu mảng thì nó có thể trả lại nội dung của mảng dưới dạng nhiều gía trị bằng unpack.

Một cách khác để hàm trả lại các gía trị là bằng cách thay đổi đối số bảng:

function table_mod(t)
  t.x = 2*t.x
  t.y = t.y - 1
end

t = {x=1,y=2}
table_mod(t)
assert(t.x==2 and t.y==1)

Ta có thể làm điều này vì bảng luôn được truyền theo tham chiếu.

1.25  Bạn có thể truyền các biến được đặt tên cho một hàm không?

Các tham biến đặt tên thì rất tiện lợi đối với hàm chứa nhiều tham biến, đặc biệt là khi ta thường chỉ dùng một vài trong số đó. Không có cú pháp nào cho các tham biến đặt tên trong Lua, nhưng rất dễ hỗ trợ chúng. Bạn truyền một bảng và khai thác đặc điểm là không cần dùng thêm những cặp  ngoặc nữa trong trường hợp hàm chỉ truyền một đối số dạng bảng:

function named(t)
  local name = t.name or 'anonymous'
  local os = t.os or '<unknown>'
  local email = t.email or t.name..'@'..t.os
  ...
end

named {name = 'bill', os = 'windows'}

Lưu ý cách mà or hoạt động.

Phong cách truyền các tham biến đặt tên này có liên quan đến việc tạo mới một bảng mỗi lần gọi và nó không phù hợp với các hàm sẽ được gọi rất nhiều lần.

1.26 Tại sao không có câu lệnh continue?

Đây là lời phàn nàn thường gặp. Những tác giả của Lua cảm thấy rằng continue chỉ là một trong những cơ chế mới để kiểm soát luồng thực thi chương trình (việc nó không thể hoạt động với quy tắc phạm vi trong repeat/until cũng là một yếu tố thứ hai.)

Trong Lua 5.2, có một câu lệnh nhảy goto để dễ dàng thực hiện nhiệm vụ.

1.27 Có cách xử lý biệt lệ không?

Các lập trình viên Lua thường thích trả lại lỗi từ các hàm. Quy tắc thông thường là nếu hàm trả lại nil hay false thì giá trị thứ hai được trả lại là lời thông báo lỗi.

function sqr(n)
  if type(n) ~= 'number' then
    return nil,'parameter must be a number'
  else
    return n*n
  end
end

Dĩ nhiên, ta biết rằng trong những ứng dụng lớn, tốt hơn thường là để lỗi ngay lập tức gián đoạn chương trình rồi bắt lỗi này bên ngoài. Điều đó giúp ta viết mã lệnh không quá nhồi nhét việc kiểm tra lỗi bằng điều kiện. Lua cho phép một hàm được gọi trong môi trường được bảo vệ qua lệnh pcall. Việc gói đoạn mã lệnh trong hàm khuyết danh sẽ cho ta hiệu quả tương đương như một cấu trúc try/catch:

local status,err = pcall(function()
  t.alpha = 2.0   -- sẽ phát ra lỗi nếu t là nil hay không phải một bảng 
end)
if not status then
  print(err)
end

Cách này có vẻ hơi lủng củng, và chúng ta bị thôi thúc tìm một cách viết khác tinh tế hơn. MetaLua cho ta một giải pháp đẹp bằng cách dùng các macro lúc biên dịch.

Một lợi ích của dạng viết tường minh ở đây là chi phí của việc xử lí lỗi đã hiện rõ; xử lí lỗi đã tạo ra một closure (đoạn lệnh đóng, xem mục 1.28.1) cho việc thực thi từng đoạn lệnh được bảo vệ.

Cái tương đương  với throw hay raise chỉ là hàm error.

Đối tượng bị ‘phát’ bởi error có thể là bất kì đối tượng Lua nào, nhưng nếu nó không phải chuỗi kí tự thì nó cần được chuyển về chuỗi. Ngoài ra, Lua 5.1 không áp dụng __tostring với những thông báo lỗi này khi thông báo một lỗi. Hãy xét:

MT = {__tostring = function(t) return 'me' end}
t = {1,2}
setmetatable(t,MT)
error(t)

Với Lua 5.1, lỗi được thông báo là “(error object is not a string)” (đối tượng lỗi không phải là chuỗi) và với Lua 5.2 đó là “me”, như dự liệu. Biểu hiện này không phải là vấn đề với các lỗi bị bắt một cách rõ ràng bằng pcall, vì bạn có thể trực tiếp xử lí đối tượng lỗi.

1.28 Không có lớp à? Vậy sao bạn lập trình được?

Đúng, không có câu lệnh class nào trong Lua, song lập trình hướng đối tượng (OOP) trong Lua thì không khó. Về một cách làm đơn giản cho thừa kế đơn, hãy xem SimpleLuaClasses; về tổng quát, hãy xem ObjectOrientedProgramming. Lời khuyên chung là, chọn lấy một bộ khung, đơn giản thôi, và thống nhất theo khung đó.

Sau đây là một mẫu từ nguồn tài liệu tham khảo thứ nhất:

-- animal.lua

require 'class'

Animal = class(function(a,name)
   a.name = name
end)

function Animal:__tostring()
  return self.name..': '..self:speak()
end

Dog = class(Animal)

function Dog:speak()
  return 'bark'
end

Cat = class(Animal, function(c,name,breed)
         Animal.init(c,name)  -- must init base!
         c.breed = breed
      end)

function Cat:speak()
  return 'meow'
end

Lion = class(Cat)

function Lion:speak()
  return 'roar'
end

fido = Dog('Fido')
felix = Cat('Felix','Tabby')
leo = Lion('Leo','African')

D:\Downloads\func>lua -i animal.lua
> = fido,felix,leo
Fido: bark      Felix: meow     Leo: roar
> = leo:is_a(Animal)
true
> = leo:is_a(Dog)
false
> = leo:is_a(Cat)
true

Một số phương thức có tên đặc biệt, như __tostring (“metamethods”). Việc định nghĩa hàm này sẽ điều khiển cách biểu thị các đối tượng của ta dưới dạng chuỗi như thế nào.

Dạng hàm function TABLE:METHOD(args) là một cách viết giản tiện khác; nó hoàn toàn tương đương với function TABLE.METHOD(self,args). Nghĩa là, hai lời định nghĩa phương thức dưới đây tương đương nhau:

function Animal.feed(self,food)
...
end

function Animal:feed(food)
...
end

Vẻ đẹp của Lua nằm ở chỗ bạn có quyền tự do lựa chọn cách viết ưng ý. Chẳng hạn, ta có thể định nghĩa cách viết này:

class 'Lion': inherit 'Cat' {
   speak = function(self)
     return 'roar'
   end
}

Nhìn qua, có vẻ nó thật ảo, nhưng thực ra dòng đầu là class('Lion'):inherit('Cat'):({, bởi vậy câu lệnh đã khéo léo sử dụng đặc điểm của Lua chấp nhận các hàm có một đối số mà không cần bao trong ngoặc tròn nếu đối số đó thuộc kiểu chuỗi hay bảng. Một đối tượng ‘class’ dĩ nhiên không thể là một chuỗi, nên quy ước ở đây là các lớp được giữ trong bối cảnh module hiện thời (thường là bảng toàn cục). Khi đó, ta có cơ hội đặt tên các lớp; điều này hỗ trợ hữu ích cho việc gỡ lỗi – chẳng hạn, khi đó có thể dễ dàng đặt cho các lớp như vật một phương thức __tostring mặc định để in ra địa chỉ của chúng cùng với tên gọi.

Đa thừa kế có thể được lập trình, nhưng sẽ tốn thêm thời gian khi chạy so với các khung đơn giản.

Mặt khác, đây là một tranh luận về việc nhất thiết phải có thừa kế không: “Trong các ngôn ngữ hoàn toàn động, ở đó không có kiểm tra lỗi lúc biên dịch, thì không cần phải đảm bảo một cấu trúc chung định trước giữa những đối tượng tương đồng. Một hàm đã cho có thể nhận được bất kì kiểu đối tượng nào, miễn là nó thực hiện được các phương thức này nọ”.

Vấn đề đi theo việc không có một khung OOP ‘thực thụ’ xuất hiện khi ta tích hợp mã lệnh Lua có dùng một khung khác không tương thích. Khi đó, tất cả những gì bạn phải giả định là một đối tượng có thể gọi được bằng cách viết a:f(). Việc dùng lại các lớp bằng thừa kế chỉ có thể khi bạn biết cấu trúc của lớp đó thế nào. Điều này có thể coi là một vấn đề làm cản trở sự chấp thuận dùng phong cách OOP cổ điển trong Lua.

Nếu thừa kế không thành vấn đề, thì cách viết sau đây sẽ nhanh chóng tạo nên các ‘đối tượng béo’:

function MyObject(n)
    local name -- this is our field
    local obj = {}  -- this is our object

    function obj:setName(n)
        name = n
    end

    function obj:getName()
        return name
    end

    return obj
end
...
> o = MyObject 'fido'
> = o:getName()
fido
> o:setName 'bonzo'
> = o:getName()
bonzo

Cái bảng thực sự được trả lại chỉ chứa hai phương thức; trạng thái của đối tượng hoàn toàn được bao gói trong các closure. Biến phi-địa phương name (hay còn gọi là ‘upvalue’) biểu diễn cho trạng thái của đối tượng.

Lưu ý rằng nếu mã lệnh này được viết lại để cho ‘.’ được dùng thay cho ‘:’, thì bạn có thể dùng cách viết dấu chấm với những đối tượng này; nghĩa là o.getName(). Tuy nhiên đây không phải điều hay, vì những lập trình viên Lua đều dự liệu việc sử dụng ‘:’ và sau này bạn có thể phải thay đổi cách lập trình.

Nói chung, cách làm này cho ta khả năng điều động phương thức nhanh nhất với giá phải trả là phát sinh những closure của phương thức cùng với các bảng của chúng cho mỗi đối tượng. Nếu chỉ có ít đối tượng thôi thì cái giá này cũng đáng.

1.28.1 Closure là gì?

Trong Lua, sức mạnh của hàm nhiều phần là nhờ các closure. Nó giúp ta coi các hàm như những đối tượng được tạo theo cách động, như các bảng. Hãy xét hàm sau vốn trả lại một hàm khác:

function count()
   local i = 0
   return function()
      i = i + 1
      return i
   end
end

Mỗi lần được gọi, hàm này sẽ trả lại một closure mới.

> c1 = count()
> = c1()
1
> = c1()
2
> c2 = count()
> = c2()
1
> = c1()
3

Để điều này hoạt động được, biến i phải được xử lí một cách đặc biết. Nếu tất cả các hàm đều trả lại cùng một biết, thì chúng phải luôn trả lại giá trị được chia sẻ tiếp theo của i. Nhưng ở đây, mỗi hàm vẫn giữ một bản sao của i riêng cho mình – biến này được gọi là một giá trị đặc biệt (upvalue) của hàm. Như vậy một định nghĩa closure, đó là hàm kết hợp với upvalue bất kì. Điều này có ý nghĩa quan trojgn: các hàm (như những đối tượng) có thể bao gói trong nó những trạng thái. Đây là một lí do tại sao việc thiếu vắng khái niệm lớp nghiêm chỉnh lại không đáng ngại trong Lua.

Nó cho phép lập trình theo phong cách hàm. Hãy xét việc tạo lập một hàm mới trong đó đối số thứ nhất được gắn vào một gía trị nào đó (gọi là áp dụng hàm từng phần)

function bind1(val,f)
    return function(...)
        return f(val,...)
    end
end
...
> print1 = bind1('hello',print)
> print1(10,20)
hello   10  20

Một lần nữa, fval là những upvalue; mỗi lần gọi mới bind1 đều tạo ra một closure mới với tham chiếu riêng của nó đến fval.

1.29 Có cách nội suy chuỗi, chẳng hạn như “$VAR is expanded” không?

Không trực tiếp được hỗ trợ, nhưng bạn vẫn dễ dàng làm được bằng string.gsub.

res = ("$VAR is expanded"):gsub('%$(%w+)',SUBST)

$ rất ‘kì diệu’, bởi vậy ta cần cho nó thoát nhóm chuỗi, và dạng mẫu %w+ cho ‘chữ cái hoặc chữ số’ được giữ lại truyền cho SUBST như ‘VAR’. (Một dạng mẫu chuẩn hơn cho tên biến trong Lua là ‘[_%a][_%w]*’).

Ở đây SUBST có thể là một chuỗi, một hàm hay một bảng chứa các cặp khóa-trị. Bởi vậy việc dùng os.getenv sẽ cho phép ta mở rộng các biến môi trường và dùng thứ kiểu như {VAR = 'dolly'} sẽ thay thế kí hiệu này với các gía trị tương ứng trong bảng.

Cosmo đã đóng gói và mở rộng dạng mẫu này một cách an toàn với các mẫu con:

  require "cosmo"
  template = [==[
  <h1>$list_name</h1>
  <ul>
   $do_items[[<li>$item</li>]]
  </ul>
  ]==]

  print(cosmo.fill(template, {
      list_name = "My List",
      do_items  = function()
          for i=1,5 do
             cosmo.yield { item = i }
          end
      end
  }
  ))
  ==>
 <h1>My List</h1>
 <ul>
 <li>1</li><li>2</li><li>3</li><li>4</li><li>5</li>
 </ul>

Cách định dạng chuỗi kiểu Python có thể thực hiện được trong Lua:

print( "%5.2f" % math.pi )

print( "%-10.10s %04d" % { "test", 123 } )

Cách lập trình rất ngắn và hay:

getmetatable("").__mod = function(a, b)
        if not b then
                return a
        elseif type(b) == "table" then
                return string.format(a, unpack(b))
        else
                return string.format(a, b)
        end
end

Các chuỗi Lua đều chia sẻ một bảng meta, bởi vậy mã lệnh này đè lên toán tử % (chia dư) trong mọi chuỗi.

Với phương án này cũng như các cái khác, xem trang Nội suy chuỗi trên Wiki.

LuaShell là một ứng dụng biểu diễn rất hay trong đó nội suy chuỗi và tự động tạo hàm có thể cho bạn một ngôn ngữ lập trình dòng lệnh tốt hơn:

require 'luashell'
luashell.setfenv()

-- echo là một hàm lập sẵn
-- lưu ý rằng môi trường và các biến địa phương đều truy cập được qua $
echo 'local variable foo is $foo'
echo 'PATH is $PATH'

cd '$HOME'    -- cd cũng là hàm lập sẵn

-- hàm ls được tạo ra một cách động khi được gọi 
-- hàm ls sẽ rẽ nhánh và thực hiện /bin/ls
ls ()
ls '-la --sort=size'

1.30 Các lời gọi đuôi được tối ưu thì dùng vào việc gì?

Đôi khi một vấn đề có thể được diễn đạt tự nhiên theo hướng đệ quy, nhưng dường như đệ quy quá tốn kém vì thường tất cả các khung ngăn xếp đều phải được lưu trong bộ nhớ.

Hàm sau đây định nghĩa một vòng lặp vô hạn trong Lua:

function F() print 'yes'; return F() end

Hãy xét cách làm này cho một Máy Trạng thái Hữu hạn (Finite State Machine, FSM)

-- Định nghĩa trạng thái A
function A()
  dosomething"in state A"
  if somecondition() then return B() end
  if done() then return end
  return A()
end

-- Định nghĩa trạng thái B
function B()
  dosomething"in state B"
  if othercondition() then return A() end
  return B()
end

-- Chạy máy FSM, bắt đầu từ trạng thái A
A()

Máy này luôn ở một trạng thái A hay B, và có thể chạy vô hạn vì các lời gọi đuôi được tối ưu hóa về thứ tương tự như một lệnh goto.

1.31 Về việc đóng gói thành ứng dụng độc lập?

srlua thực hiện công việc cơ bản; bạn có thể gắn một đoạn mã lệnh Lua vào với Lua interpreter, và đưa ra một file thực thi độc lập.

Tuy nhiên, phần lớn các file lệnh quy mô hẳn hoi đều có mã lệnh phụ thuộc; L-Bia nhằm cung cấp một giải pháp toàn diện hơn.

Một giải pháp tốt là Squish vốn là một công cụ để nhồi nhiều file mã lệnh Lua cùng những module của chúng vào một file Lua duy nhất, để từ đó xử lý bằng srlua.

1.32 Làm thế nào để tôi nạp và chạy mã lệnh Lua (vốn nhiều khả nghi)?

Nhiệm vụ là phải thực thi được mã lệnh có nguồn gốc đáng ngờ.

-- tạo lập một môi trường
local env = {}

-- chạy mã lệnh trong môi trường này 
local function run(code)
  local fun, message = loadstring(code)
  if not fun then return nil, message end
  setfenv(fun, env)
  return pcall(fun)
end

Đầu tiên, mã lệnh được biên dịch thành một hàm, sau đó ta đặt môi trường cho hàm này, rồi cuối cùng hàm đó được gọi bằng pcall, nhờ đó ta có thể bắt được lỗi. Dĩ nhiên, bạn có thể dùng loadfile ở đây.

Lưu ý rằng nếu bạn muốn nạp mã lệnh rồi thực hiện nó lặp đi lặp lại thì bạn nên  you should store the compiled function and pcall it whenever needed.

Mã lệnh có thể tới từ bất kì đâu. Đây là vấn đề ‘sand-box’ kinh điển (xem Sandboxing.) Mấu chốt của việc làm cho mã lệnh đó phải càng an toàn càng tốt là hạn chế mối trường của hàm. Nếu không có lời gọi setfenv, hàm đó sẽ chạy trong môi trường toàn cục và dĩ nhiên có thể gọi được bất kì hàm Lua nào, do đó tiềm năng gây nguy hại. Như vậy cách tiếp cận sand-boxing bao gồm việc bắt đầu với một môi trường trống không và chỉ bổ sung những hàm nào bạn muốn người khác dùng đến, ngoại trừ những hàm ‘không an toàn’.

Với việc bao gồm những module của riêng bạn vào trong môi trường sand-box,  cần lưu ý rằng module('mymod',package.seeall) rõ ràng là một rủi ro an toàn. Lời gọi này hoạt động bằng cách tạo nên một môi trường mới trong bảng mymod, và sau đó đặt __index_G – sao cho bất kì ai cũng có thể truy cập các hàm toàn cục qua module của bạn, đơn giản chỉ bằng cách mymod._G ! Do vậy, hãy hạn chế sử dụng package.seeall.

Với Lua 5.2, một hàm tương đương sẽ là:

function run (code)
   local fun, message = load(code,"tmp","t",env)
   if not fun then return nil, message end
   return pcall(fun)
end

1.33 Về việc nhúng Lua vào trong một ứng dụng?

Một trong những công dụng đáng kể nhất của Lua là trong việc trưng ra những chương trình con bên trong thông qua trình thông dịch hiệu quả của Lua, giusp cho người dùng và người phát triển có thể mở rộng và tùy biến tính năng của một trình ứng dụng. Lua cũng là một ứng cử viên lí tưởng cho các mã lệnh phân theo module và chạy trên nhiều nên tảng khác nhau bên trong trình ứng dụng, hay giữa các phần của ứng dụng đó. Những chương trình ứng dụng có dùng đến Lua theo cách này bao gồm Adobe Lightroom, World-of-Warcraft, VLC, Lighttpd, và còn nữa.

Việc nhúng Lua chỉ làm tăng thêm 150-200K vào dự án của bạn, tùy theo những thư viện được chọn là gì. Lua được thiết kế là ngôn ngữ mở rộng và rất dễ đảm bảo rằng các đoạn lệnh do người dùng bổ sung đều hoạt động trong một môi trường ‘an toàn’ (xem Sandboxing.) Thậm chí bạn không phải nhúng mặt tiền của trình biên dịch Lua, và chỉ dùng phần cốt lõi với các đoạn mã lệnh đã biên dịch sẵn. Điều này sẽ giúp giảm nhu cầu bộ nhớ xuống chỉ còn khoảng 40K.

Những nguyên nhân khiến Lua thành một ngôn ngữ mở rộng lý tưởng đều gắn với những lời phàn nàn thường thấy từ các người viết mã lệnh rằng Lua ‘không đi kèm theo các bộ công cụ thêm (battery)’. Phần lõi của Lua phụ thuộc chặt vào những thứ cung cấp bởi ANSI C, do vậy không có thứ gì phụ thuộc vào nền tảng (hệ thống). Thế nên, việc tích hợp nó vào trong dự án của bạn thường rất dễ dàng.

Việc tích hợp với C/C++ đặc biệt rất dễ (xem mục 4.4 hay Ví dụ API đơn giản), còn những cách buộc qua lại với những ngôn ngữ hay nền tảng thông dụng đều có thể được (xem cách buộc mã lệnh vào với Lua).

1.34 Về việc viết tài liệu cho mã lệnh Lua?

Mã lệnh đối với máy tính là một thứ để giao tiếp với con người: như Donald Knuth đã nói “Thay vì tưởng tượng ra nhiệm vụ chính của người là hướng dẫn cho máy biết phải làm gì, thì ta hãy tập trung vào việc giải thích cho người biết những điều mà ta muốn máy làm”. (Sau một vài tháng, bạn chính là người cần lời giải thích đó.)

LuaDoc thường hay được dùng nhất. Phong cách của nó tương tự với các công cụ phát sinh tài liệu khác như JavaDoc:

--- Bắt đầu bằng ba dấu gạch ngang; đây là câu tóm lược.
-- Bạn có thể viết thêm các dòng ở đây;
-- Chúng có thể chứa HTML, và thực ra bạn cần viết rõ <br>
-- để xuống dòng.
-- @param first tên riêng của người (string)
-- @param second tên họ của người (string)
-- @param age số tuổi; (optional number)
-- @return một đối tượng  (Person)
-- @usage person = register_person('john','doe')
-- @see Person
function register_person(first,second,age)
...
end

Chú thích đoạn mã của bạn theo cách có cấu trúc như trên dù sao cũng rất có ích, bởi Lua không ghi rõ kiểu của các đối số trong hàm. Nếu người dùng phải đoán xem cần truyền vào hàm dữ liệu nào qua việc đọc mã lệnh bên trong thì chứng tỏ mã lệnh đó chưa được viết tài liệu kĩ lưỡng. Hoặc việc đặt quy ước cho kiểu đối số cũng có ích.

Tuy nhiên, những tài liệu tự động phát sinh thì hiếm khi đáp ứng đủ. Một bộ mã lệnh thử nghiệm được viết tài liệu kĩ mới có ích gấp đôi trong trường hợp này.

1.35 Làm cách nào để kiểm tra kiểu của các đối số thuộc hàm?

Việc quy định kiểu dữ liệu động đưa tới mã lệnh đơn giản và tổng quát. Hàm

function sqr(x) return x*x end

sẽ hoạt động với mọi gía trị có thể nhận được, nghĩa là một con số hoặc một đối tượng đã định nghĩa phương thức meta tên __mul . Đồng thời, do type() trả lại kiểu Lua thực sự, ta có thể làm nhiều việc tùy theo những gì ta đã truyền. Khỏi cần nói, sự linh hoạt này có cái gía của nó, đặc biệt là trong những chương trình lớn. Việc lập tài liệu kĩ lưỡng cho hàm trở nên thiết yếu (xem mục 1.34).

Dĩ nhiên, trình biên dịch không đọc phần tài liệu này. Có những cách kiểm tra kiểu trong đó ta nhúng thông tin về kiểu vào trong mã lệnh Lua. Một phong cách lập trình là dùng assert đối với mọi đối số hàm:

function sqr(x)
    assert(type(x) == "number", "sqr expects a number")
    return x*x
end

Bây giờ ta thu được một lời thông báo ý nghĩa rõ ràng, với gía phải trả là một đoạn lệnh viết thêm và có thể nhiều thời gian chạy chương trình. Trong Lua, assert() không phải là một macro như ở C, bởi vậy không thể đơn giản mà ngắt nó đi được. Số lượng mã lệnh phải gõ thêm vào có thể được giảm bớt bằng cách lập các hàm phụ trợ thích hợp, nhưng vẫn còn rất nhiều phép kiểm tra. Một vấn đề khác là các kiểu dữ liệu đối số hàm được gói trọn trong phần lập trình của hàm đó, và vì vậy không thể truy cập được mà không thực hiện qua phân tích mã lệnh một cách khéo léo (dù rằng cách làm này không vững chắc).

Một cách làm đầy triển vọng là dùng các decorator:

sqr = typecheck("number","->","number") ..
function (x)
    return x*x
end

Bây giờ dấu ấn (signature) của sqr đã rõ ràng và nổi lên trên, dễ dàng tách được khỏi mã nguồn, và việc kiểm tra kiểu dữ liệu khi chạy thực sự có thể tắt/bật được.

Sự ràng buộc về kiểu như ‘number’ ở đây là quá cụ thể, nhưng ta có thể định nghĩa và sử dụng các hàm true/false như is_number().

Bạn rất nên cân nhắc sử dụng Metalua, vốn là một bộ biên dịch macro cho Lua thông minh và mở rộng được (theo nghĩa đầy đủ của Lisp về các ‘macro sạch’ (hygenic macro)). Cú pháp này trở nên hợp lệ:

-{ extension "types" }

function sum (x :: list(number)) :: number
    local acc :: number = 0
    for i=1, #x do acc=acc+x[i] end
    return acc
end

1.36 Làm thế nào để kết thúc thực thi một cách êm thấm?

Hàm error sẽ phát ra một lỗi với lời thông báo cho trước, và nếu nó không được xử lý thì pcall sẽ tạo ra một stack trace. Điều này hữu ích khi phát triển chương trình, song không đẹp mắt những người sử dụng. Thường ta lập một hàm quit kiểu như:

function quit(msg, was_error)
    io.stderr.write(msg,'\n')
    os.exit(was_error and 1 or 0)
end

os.exit kết thúc chương trình ngay, không cần phải kích hoạt bộ thu gom rác bộ nhớ. (Trong Lua 5.2 có một tham số thứ hai, tùy chọn, cho os.exit để đóng trạng thái Lua và bắt buộc việc thu rác.)

The behaviour of error can be modified by setting debug.traceback=nil so that it doesn’t produce a stack trace. (This is a nice example of Monkey patching; modifying Lua’s operation by changing a library function dynamically.)

Cách làm vá víu này cũng ảnh hưởng hàm assert. assert nhận hai đối số, một điều khiện phải-đúng và một thoogn báo lỗi. Nó không phải là một câu lệnh và trả về đối số thứ nhất. Vì các hàm Lua thường trả về một gía trị nil và chuỗi báo lỗi khi thất bại trong qúa trình chạy, song cách viết sau lại hoạt động được vì đối số thứ nhât truyền cho assert sẽ là nil, và đối số thứ hai sẽ là chuỗi báo lỗi.

f = assert(io.open(file))

Thường những người dùng một module chỉ muốn xem stack trace những gì lên quan đến mã lệnh của bản thân họ, chứ không quan tâm những chi tiết bên trong của lỗi (bất kì ai đã dùng một thư viện Java với biểu hiện hỏng thì đều biết việc này.) Cái stack trace tạo bởi error có thể điều khiển được thông qua một tham biển ‘level’ tùy chọn để chỉ định chỗ nào trong stack gọi (call stack) phải chịu trách nhiệm với lỗi được phát sinh. Một giải pháp tổng quát hơn được Lua Carp thực hiện, vốn được phỏng theo module carp của Perl.

1.37 module() làm việc như thế nào?

module được dùng để tạo ra một module mà sau đó có thể được require. Dù không bắt buộc, song nó đơn giản hóa việc tạo lập các module chuẩn của Lua.

-- testm.lua (đâu đó trong đường dẫn Lua)
local print = print
module ("testm")

function export1(s)
    print(s)
end

function export2(s)
    export1(s)
end

Sau lệnh require ("testm") bạn có thể gọi các hàm đã xuất khẩu theo dạng testm.export2("text") v.v. Bởi vậy điều đầu tiên module thực hiện là tạo lập một bảng chứa các hàm mới của bạn; nó cũng đảm bảo rằng require sẽ trả lại bảng này (thay vì chỉ trả lại true).

Thứ hai là, nó đặt môi trường hàm của đoạn mã lệnh thành cái bảng này. Điều này khiến cho nó hoạt động như một không gian tên (‘namespace’) ở các ngôn ngữ khác; bên trong module testm, bất kì hàm nào cũng có thể trực tiếp thấy các hàm khác mà không phải đề cập đến testm. Điều này nghĩa rằng bất kì hàm nào đều mặc định là ẩn, bởi vậy ta đã phải tạo một bí danh địa phương đến print.

Người ta có thể thấy điều này thật hạn chế, bởi vậy module thường được khai báo như:

module ("testm",package.seeall)

Điều này làm thay đổi bảng module sao cho mọi trường hợp tra cứu không thỏa đều được thực hiện trong phạm vi toàn bộ, bởi vậy dòng khai báo cục bộ của print trở nên thừa. (Điều này được làm bằng cách cho bảng đó một bảng meta với __index chỉ đến bảng toàn cục _G); lưu ý rằng việc truy cập các biến toàn cục thế này sẽ chậm hơn so với khai báo rõ là biến cục bộ.

Có thể viết  module (...) ở đầu module. Khi được nạp, module này sẽ được gọi với tên của module được xác định thông qua require. Điều đó nghĩa là bản thân module không chứa tên của nó, và có thể được di chuyển tùy ý.

Ý nghĩa thiết yếu của module được chứa trong đoạn mã sau:

function mod(name)
   module ('test',package.seeall)
   function export()
    print 'export!'
   end
end

mod('test')

test.export()

Như vậy, lời gọi module biến môi trường của hàm mod thành test, sao cho bất kì hàm nào được định nghĩa bên trong đều thực ra là ở trong bảng test.

1.37.1 Lời chỉ trích về module()

Lời chỉ trích thứ nhất là là package.seeall bộc lội bảng toàn cục trong những trường hợp bạn thự sự muốn hạn chế truy cập đến tính năng. Nó dẫn đến việc ‘bao gói rò rỉ’ khiến cho bất kì người dùng module này dễ dàng truy cập các hàm hay các bảng toàn cục thông qua bản thân module đó, ví dụ testm.io.

Chỉ trích thứ hai là một module có dùng module để tạo nên các bảng toàn cục và xuất khẩu các đối tượng phụ thuộc. module "hello.world" tạo ra một bảng hello (nếu chưa có sẵn) và world là một bảng bên trong nó. Nếu hello.world yêu cầu fred thì fred sẽ tự động trở thành có sẵn với tất cả những ai dùng hello.world, vốn có thể lệ thuộc vào chi tiết mã lệnh đã lập trình và họ sẽ nhầm lẫn nếu mã lệnh thay đổi.

Xem thông tin ở đây.

Lưu ý rằng module() đã lỗi thời từ Lua 5.2; các nhà phát triển nên dùng một phong cách đơn giản, không màu mè để viết nên module.

1.37.2 Mọi thứ sau module()?

Phong cách viết module này vẫn còn chuyển đổi tốt giữa Lua 5.1 và Lua 5.2, ở đó mọi hàm xuất khẩu đều sẽ được đặt vào một bảng module:

-- mod.lua
local M = {}

function M.answer()
  return 42
end

function M.show()
   print(M.answer())
end

return M

Lưu ý rằng không có biến toàn cục nào được tạo ra, và thực tế không có tên module nào được chỉ định. Bởi vậy bạn cần gán module cho một biến trước khi sử dụng:

local mod = require 'mod'

Nhược điểm của phong cách ‘không màu mè’ này là ở chỗ mỗi tham chiếu hàm module phải được đề cập rõ module chủ (tức là phải qualify); đây có thể là vấn đề khi ta chuyển đổi mã lệnh bằng module(). Phương án sau khai báo các hàm ngay từ đầu:

local answer, show

function answer()  return 42 end

function show() print(answer()) end

return {answer=answer,show=show}

Cách này có ưu điểm rằng: (a) hàm xuất khẩu được đề cập rõ ở cuối và (b) mọi truy cập đến hàm đều có tính cục bộ và do vậy sẽ nhanh hơn. (Những module chứa rất nhiều hàm sẽ đạt đến giới hạn biến cục bộ vào khoảng 240, song có thể đây là trường hợp qúa hiếm.)

1.38 Các bảng yếu là gì?

Để hiểu được bảng yếu, bạn cần hiểu một vấn đề chung trong thu hồi rác bộ nhớ. Bộ thu hồi rác phải bảo thủ ở mức cao nhất có thể, và không suy đoán bất cứ điều gì về dữ liệu; nó không phải nhà tuyên đoán. Ngay khi các đối tượng được giữ trong một bảng, chúng được coi là đã tham chiếu đến và sẽ không bị thu hồi miễn là bảng đó được tham chiếu.

Các đối tượng được tham chiếu bởi một bảng yếu sẽ được thu hồi nếu không có tham chiếu sẽ bị thu hồi nếu không có tham chiếu đến chúng. Nếu một metatable của bảng có trường tên là __mode thì nó sẽ trở thành bảng yếu; __mode có thể là một trong hai ‘k’ (khóa yếu) hay ‘v’ (giá trị yếu), hoặc cả hai. Việc đặt một giá trị vào trong bảng với các giá trị yếu sẽ đồng nghĩa với việc bảo bộ thu hồi rằng đây không phải một tham chiếu quan trọng và có thể được thu hồi một cách an toàn.

Lưu ý rằng chuỗi không được coi là một giá trị (như số, khác với một đối tượng như bảng) về phương diện này. Điều đó đảm bảo cho việc chuỗi kí tự không bị thu hồi khỏi bảng với khóa yếu.

1.39 Khác biệt giữa những cách viết chuỗi kí tự?

Không có khác biệt gì giữa chuỗi trong nháy đơn hay nháy kép, và bạn có thể dùng một trong hai cách, dù rằng thống nhất sử dụng sẽ tốt hơn. Chẳng hạn, người ta dùng nháy đơn cho các chuỗi bên trong chương trình và nháy kép cho các chuỗi cho phép người dùng/hệ thống xem.

Những chuỗi này có thể chứa các kí tự thoát kiểu C (như \n) và lưu ý rằng 12 chính là hằng số 12 trong hệ thập phân ! Các dấu [[..]] bao quanh chuỗi dài thì không đọc những kí tự thoát này. Theo quy ước, dấu kết thúc dòng thứ nhất sẽ không được tính đến, như:

s = [[
Một chuỗi với chỉ một dấu kết thúc dòng.
]]

Cái giá phải trả cho sự đơn giản này là các biểu thức như T[A[i]] sẽ không được hiểu đúng, bởi vậy Lua cho phép [=[...]=] trong đó số lượng các dấu = phải như nhau. (Đây cũng là chế độ dùng cho những lời chú thích kéo dài, với dấu -- đứng trước.) Lưu ý rằng với Lua 5.0 cặp [[..]] vẫn được cho phép, song đặc điểm này đã lỗi thời.

Do phải xử lý dấu kết thúc dòng nên những chuỗi dài như thế này không thích hợp cho việc nhúng dữ liệu nhị phân trực tiếp vào chương trình.

1.40 Các vấn đề tương thích giữa Windows và Unix?

Ở đây, ‘Unix’ là để nói chung cho hệ điều hành tựa-POSIX như Linux, Mac OS X, Solaris, v.v.

package.config là một chuỗi trong đó ‘kí tự’ đầu tiên là dấu phân cách thư mục, bởi vậy package.config:sub(1,1) là dấu gạch chéo xuôi hay gạch chéo ngược. Một quy tắc chung là nên dùng mã này khi lập các dường dẫn.

Một khác biệt lớn giữa các bản dựng cho Windows và Unix là package.path mặc định được đặt ở vị trí của file chạy trong Windows, còn ở  Unix thì đặt tại /usr/local/share/lua/5.1. Bởi vậy sẽ dễ hơn nếu ta cài đặt Lua cho người dùng trong Windows, nhưng Lua tôn trọng các biến môi trường LUA_PATHLUA_CPATH.

Lua phụ thuộc trực tiếp hơn vào các thư viện chạy (runtime) C của hệ thống, hơn đa số ngôn ngữ văn lệnh khác, nên bạn phải để ý những khác biệt về hệ thống. Hãy dùng kí hiệu “rb” với io.open nếu bạn cần tương thích với khâu nhập/xuất số liệu nhị phân Windows. Hãy cẩn thận với os.tmpname vì nó không trả lại một đường dẫn đầy đủ với Windows (chèn giá trị của biến môi trường TMP lên đầu tiếp theo là dấu gạch chéo ngược.) os.clock được thực hiện rất khác trên Windows.

Tương tự, os.time có thể sẽ làm đổ vỡ Lua nếu truyền các kí hiệu định dạng không tương thích. (Điều này không còn là vấn đề trong Lua 5.2, bởi phiên bản này trước hết sẽ thực hiện khâu kiểm tra.)

Với hệ GUI Windows, os.execute có thể sẽ gây khó chịu, và io.popen đơn giản sẽ không hoạt động – song có những thư viện mở rộng cho việc đa nền sẽ giúp ích trong trường hợp này.

(Xem tiếp Phần 2)

 

1 Phản hồi

Filed under Lua

One response to “Ngôn ngữ lập trình Lua: những hỏi-đáp bên lề

  1. Pingback: Ngôn ngữ lập trình Lua: những hỏi – đáp bên lề (phần 2) | Blog của Chiến

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s