หัดเขียนโปรแกรมแบบบ้านๆ ตอนที่ 12 Module

 บทความก่อนหน้า


Module

Environment ใน Lua

ภาษา Lua มีตัวแปรพิเศษอยู่ตัวหนึ่งชื่อว่า _G เป็น table ที่เก็บตัวแปร global, standard library และฟังก์ชันภายในทุกตัวเช่น print() ก็คือ _G.print() เป็นต้น
ตัวอย่าง Lua interpreter

Lua 5.3.5  Copyright (C) 1994-2015 Lua.org, PUC-Rio
> a = 10
> a
10
> _G.a
10
> _G.a = 20
> a
20
> a == _G.a
true
> 
เราสามารถแก้ไขสมาชิกใน _G ได้อิสระซึ่งอาจเกิดปํญหากระทบกับฟังก์ชันภายในได้ ใน Lua 5.1 เรามีฟังก์ชัน getfenv() กับ setfenv() ไว้คอยจัดการกับสภาพแวดล้อมทำให้สร้างเป็น sandbox ที่ไม่กระทบกับสภาพแวดล้อมหลักได้แต่ใน Lua 5.2 ขึ้นไปเรามีวิธีที่เรียบง่ายกว่าโดยอาศัยตัวแปรพิเศษอีกตัวคือ _ENV โดยค่าเริ่มต้นคือ _ENV = _G ซึ่งในบล๊อกคำสั่งของ Lua จะไม่ไปค้นหาใน _G โดยตรงแต่จะไปดูที่ _ENV แทนดังนั้นเราสามารภเปลี่ยน _ENV ในบล๊อกเป็นอีก table เพื่อป้องกันการเข้าถึงสภาพแวดล้อมหลักจากภายในบล๊อกได้

  a = {p = print}                       -- สร้างสภาพแวดล้อมใหม่มีสมาชิก "p" ชี้ไปที่ฟังก์ชัน "print"
  do                                    -- บล๊อกของโปรแกรม
    local _ENV = a                      -- กำหนดให้ใช้สภาพแวดล้อมใหม่ในบล๊อกโปรแกรม
    a = 10                              -- ตัวแปร global ในบล๊อก
    p(a)                                -- พิมพ์ค่าในตัวแปร
  end                                   -- จบบล๊อกกลับสู่สภาพแวดล้อมหลัก
  print(a)                              -- พิมพ์ค่าตัวแปร global ในสภาพแวดล้อมหลัก
  print(a.a)                            -- พิมพ์ค่าสมาชิก "a" ใน table "a"
  
  -- ผลลัพธ์
  --[[
  10
  table: 0x1edb840
  10
  ]]
  
  local pr = print                      -- เก็บฟังก์ชัน "print" ไว้ในตัวแปร local "pr"
  function my_env(t)                    -- สร้างฟังก์ชัน "my_env"
    t.pa = pairs                        -- เก็บฟังก์ชัน "pairs" เป็นสมาชิกของ table "t"
    local _ENV = t                      -- กำหนดให้ "t" เป็นสภาพแวดล้อมใหม่
    pr("inner_ENV =", _ENV)             -- พิมพ์ค่าสภาพแวดล้อมปัจจุบันในฟังก์ชัน
    for k, v in pa(_ENV) do             -- วนลูปแสดงสมาชิกในสภาพแวดล้อมปัจจุบัน
      pr(k, v)
    end
  end
  
  local env = {}                        -- สร้างตัวแปร "env"
  print("ENV=", _ENV, "env=", env)      -- พิมพ์ค่าสภาพแวดล้อมปัจจุบันเทียบกับ "env"
  my_env(env)                           -- เรียกฟังก์ชัน "my_env"
  print"+++++"
  for k, v in pairs(env) do             -- วนลูปแสดงสมาชิกใน "env"
    print(k, v)
  end
  
  -- ผลลัพธ์
  --[[
  ENV=	table: 0x13709f0	env=	table: 0x1378b00
  inner_ENV =	table: 0x1378b00
  pa	function: 0x41b570
  +++++
  pa	function: 0x41b570
  ]]

การนำเข้าโปรแกรมจากไฟล์อื่น

ภาษา Lua สามารถแปลโค้ดในตัวเองได้โดยใช้ฟังก์ชัน load() ดังนั้นเราสามารถอ่านไฟล์เป็นข้อความส่งเข้าฟังก์ชันก็ได้หรือใช้ loadfile() เรียกไฟล์นั้นโดยตรง ซึ่ง dofile() ก็ทำได้เช่นกันแต่ dofile() จะทำงานในไฟล์นั้นเลยขณะที่ loadfile() กับ load() จะส่งค่าเป็นฟังก์ชันต้องเรียกใช้งานอีกที

  -- Ex.1 เนื้อหาใน mycode1.lua ไฟล์ที่ไม่มีการคืนค่า
  print("Hello")
  
  .......
  
  -- การใช้วิธี Load
  local code = io.open("mycode1.lua"):read("a") -- โหลดไฟล์มาเป็นข้อความ
  local f = load(code)                  -- โหลดโค้ดจากข้อความไปเป็นฟังก์ชัน
  f()                                   -- เรียกให้โค้ดที่โหลดมาทำงาน
  
  .......
  
  -- การใช้วิธี Loadfile
  local f = loadfile("mycode1.lua")     -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชัน
  f()                                   -- เรียกให้โค้ดที่โหลดมาทำงาน
  
  .......
  
  -- การใช้วิธี Dofile
  dofile("mycode1.lua")                 -- โหลดโค้ดจากไฟล์และสั่งทำงาน
  
  .......
  
  -- ผลลัพธ์
  --[[
  Hello
  ]]
  
  -- Ex.2 เนื้อหาใน mycode2.lua ไฟล์ที่มีการคืนค่า
  return 2 + 3
  
  .......
  
  -- การใช้วิธี Load
  local code = io.open("mycode2.lua"):read("a")
  local f = load(code)                  -- โหลดโค้ดจากข้อความไปเป็นฟังก์ชัน
  local result = f()                    -- เรียกให้โค้ดที่โหลดมาทำงานเอาผลลัพธ์เก็บใน "result"
  print(result)
  
  .......
  
  -- การใช้วิธี Loadfile
  local f = loadfile("mycode2.lua")     -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชัน
  local result = f()                    -- เรียกให้โค้ดที่โหลดมาทำงานเอาผลลัพธ์เก็บใน "result"
  print(result)
  
  .......
  
  -- การใช้วิธี Dofile
  local result = dofile("mycode2.lua")  -- โหลดโค้ดจากไฟล์และสั่งทำงานเอาผลลัพธ์เก็บใน "result"
  print(result)
  
  .......
  
  -- ผลลัพธ์
  --[[
  5
  ]]
  
  -- Ex.3 เนื้อหาใน mycode3.lua การเข้าถึงตัวแปรและฟังก์ชันที่เป็นแบบ global และ local
  abc = "ตัวแปร global"
  local def = "ตัวแปร local"             -- ไม่สามารถเข้าถึงตัวแปร local จากนอกไฟล์ได้
  
  function add(x, y)
    return x + y
  end
  
  local function sub(x, y)              -- ไม่สามารถเข้าถึงฟังก์ชัน local จากนอกไฟล์ได้
    return x - y
  end
  
  .......
  
  -- การใช้วิธี Load
  local code = io.open("mycode3.lua"):read("a")
  local f = load(code)                  -- โหลดโค้ดจากข้อความไปเป็นฟังก์ชัน
  f()                                   -- เรียกให้โค้ดที่โหลดมาทำงาน
  print(abc, def)
  print(add(1, 2), sub)
  
  .......
  
  -- การใช้วิธี Loadfile
  local f = loadfile("mycode3.lua")     -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชัน
  f()                                   -- เรียกให้โค้ดที่โหลดมาทำงาน
  print(abc, def)
  print(add(1, 2), sub)
  
  .......
  
  -- การใช้วิธี Dofile
  dofile("mycode3.lua")                 -- โหลดโค้ดจากไฟล์
  print(abc, def)
  print(add(1, 2), sub)
  
  .......
  
  -- ผลลัพธ์
  --[[
  ตัวแปร global     nil
  3     nil
  ]]
นอกจากจะสามารถโหลดไฟล์โปรแกรมแล้วยังสามารถใช้กับไฟล์ที่ผ่านการแปลงเป็น bytecode ด้วยโปรแกรม luac ได้ด้วย

การสร้าง Module

โดยทั่วไปในการเขียนโปรแกรมไฟล์หนึ่งๆ อาจมีการนำเข้าฟังก์ชันจากไฟล์อื่นเก็บไว้เรียกใช้ในโปรแกรม การนำเข้าไฟล์ตามตัวอย่างข้างต้นเราสามารถเข้าถึงตัวแปร global ได้เลยแต่จะมีปัญหาเวลาที่เราโหลดหลายไฟล์และมีชื่อตัวแปรหรือฟังก์ชันซ้ำซ้อนกัน เราสามารถใช้การคืนค่าของไฟล์ในตัวอย่าง Ex.2 ข้างบนโดยใช้ return ในการส่งออกฟังก์ชันที่ใช้งาน

  -- Ex.4 เนื้อหาใน mycode4.lua
  local function add(x, y)
    return x + y
  end
  return add                            -- ส่งออกฟังก์ชัน add ไปให้ไฟล์ที่เรียกใช้งาน
  
  .......
  
  -- การใช้วิธี Load
  local code = io.open("mycode4.lua"):read("a")
  local f = load(code)                  -- โหลดโค้ดจากข้อความไปเป็นฟังก์ชัน
                                        -- "f" เทียบได้กับ function() return add end
                                        -- "f()" จึงได้ "add" ดังนั้น "f()(1,2)" จึงเท่ากับ "add(1,2)"
  local result = f()(1, 2)              -- เรียกให้ฟังก์ชัน "add" ที่เก็บอยู่ใน "f" ทำงานเอาผลลัพธ์เก็บใน "result"
  print(result)
  
  .......
  
  -- การใช้วิธี Loadfile
  local f = loadfile("mycode4.lua")     -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชัน
  local add = f()                       -- เรียก "f" ทำงานคืนค่าฟังก์ชัน "add" ไปเก็บในตัวแปร "add"
  print(add(1, 2))
  -- หรือ
  local add = loadfile("mycode4.lua")() -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชันและเรียกใช้ฟังก์ชันเอาผลลัพธ์ฟังก์ชัน "add" เก็บในตัวแปร "add"
                                        -- loadfile("mycode4.lua") == function() return add end
                                        -- (function() return add end)() จะได้ "add"
  local result = add(1, 2)
  print(result)
  
  .......
  
  -- การใช้วิธี Dofile
  local add = dofile("mycode4.lua")     -- โหลดโค้ดจากไฟล์และสั่งทำงานเอาผลลัพธ์ฟังก์ชัน "add" เก็บในตัวแปร "add"
  print(add(1, 2))
  
  .......
  
  -- ผลลัพธ์
  --[[
  3
  ]]
ในกรณีที่ต้องการส่งออกฟังก์ชันหลายตัวในหนึ่งไฟล์ เราสามารถใช้ตัวแปร table ในการเก็บฟังก์ชันทั้งหมดที่ใช้งานได้และเรียกไฟล์ที่เก็บฟังก์ชันที่ให้ไฟล์อื่นใช้งานว่า module เหมือนเป็นการกำหนด namespace ให้ฟังก์ชันป้องกันการสับสนกับฟังก์ชันชื่อเหมือนกันจากมอดูลอื่น

  -- Ex.5 เนื้อหาใน mycode5.lua
  local M = {}
  function M.addi(x, y)
    return x + y
  end
  
  function M.subi(x, y)
    return x - y
  end
  return M
  
  .......
  
  -- การใช้วิธี Load
  local code = io.open("mycode5.lua"):read("a")
  local f = load(code)                  -- โหลดโค้ดจากข้อความไปเป็นฟังก์ชัน
  local result = f().addi(1, 2)         -- เรียกฟังก์ชัน "addi" ที่อยู่ในมอดูล "M" ในฟังก์ชัน "f" ทำงาน
  print(result)
  local mymod = f()                     -- เรียกให้โค้ดที่โหลดมาทำงานเอาผลลัพธ์มอดูล "M" เก็บในตัวแปร "mymod"
  local func_sub = mymod.subi           -- เอาฟังก์ชัน "subi" ที่อยู่ในมอดูล "mymod" ไปใส่ตัวแปร "func_sub"
  result = func_sub(5, 2)               -- เรียกฟังก์ชัน "func_sub" ทำงาน
  print(result)
  
  .......
  
  -- การใช้วิธี Loadfile
  local f = loadfile("mycode5.lua")     -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชัน
  local mymod = f()                     -- เรียกให้โค้ดที่โหลดมาทำงานเอาผลลัพธ์ "M" เก็บในตัวแปร "mymod"
  print(mymod.addi(1, 2))
  print(mymod.subi(5, 2))
  -- หรือ
  local mymod = loadfile("mycode5.lua")() -- โหลดโค้ดจากไฟล์ไปเป็นฟังก์ชันและเรียกใช้ฟังก์ชัน
  local result = mymod.addi(1, 2)       -- เรียกใช้ฟังก์ชัน "addi" ในมอดูล "mymod" เก็บผลลัพธ์ในตัวแปร "result"
  print(result)
  print(mymod.subi(5, 2))               -- เรียกใช้ฟังก์ชัน "subi" ในมอดูล "mymod"
  
  .......
  
  -- การใช้วิธี Dofile
  local mymod = dofile("mycode5.lua")   -- โหลดโค้ดจากไฟล์และสั่งทำงานเอาผลลัพธ์เก็บในตัวแปร "mymod"
  print(mymod.addi(1, 2))
  print(mymod.subi(5, 2))
  -- หรือ
  local func_add = dofile("mycode5.lua").addi -- โหลดโค้ดจากไฟล์และสั่งทำงานเอาเฉพาะฟังก์ชัน "addi" เก็บในตัวแปร "func_add"
  print(func_add(1, 2))
  local func_sub = dofile("mycode5.lua").subi -- โหลดโค้ดจากไฟล์และสั่งทำงานเอาเฉพาะฟังก์ชัน "subi" เก็บในตัวแปร "func_sub"
  print(func_sub(5, 2))
  
  .......
  
  -- ผลลัพธ์
  --[[
  3
  3
  ]]
แต่การใช้ load(), loadfile(), dofile() ยังมีปัญหาบ้างเช่นต้องระบุพาธเต็มของไฟล์ที่ต้องการโหลดกรณีที่ไฟล์ไม่ได้อยู่ที่เดียวกันและกรณีที่มีการโหลดไฟล์เดิมซ้ำก็จะได้มอดูลใหม่ทำให้เปลืองหน่วยความจำ

  local mod1 = dofile("mycode5.lua")
  local mod2 = dofile("mycode5.lua")
  print(mod1, mod2)
  
  -- ผลลัพธ์
  --[[
  table: 0x2559ef0        table: 0x2475a30
  ]]
ดังนั้น Lua จึงมีอีกฟังก์ชันที่ใช้จัดการกับมอดูลโดยตรงคือ require() โดยจะไปค้นหามอดูลใน package.loaded ในกรณีที่มีการโหลดมอดูลนั้นมาก่อนก็จะไม่โหลดซ้ำแต่จะใช้มอดูลที่เก็บไว้ในนี้แทนถ้าไม่มีค่อยใช้ชื่อไปค้นในพาธที่เก็บใน package.path โดยไปหาไฟล์ .lua ชื่อเดียวกับชื่อมอดูลหรือหาไฟล์ init.lua ในโฟล์เดอร์ชื่อเดียวกับชื่อมอดูลแทน ถ้าไม่เจอค่อยไปหาไฟล์นามสกุล .so ( หรือ .dll ใน Windows ) ที่เป็นไฟล์ไบนารีที่เขียนจากภาษา C จากพาธที่เก็บใน package.cpath แทนตามลำดับกรณีที่โหลดสำเร็จก็จะเก็บมอดูลนั้นไว้ที่ package.loaded

  local mod1 = require("mycode5")
  local mod2 = require("mycode5")
  print(mod1, mod2)
  print(package.loaded.mycode5)
  
  -- ผลลัพธ์
  --[[
  table: 0x26d0eb0        table: 0x26d0eb0
  table: 0x26d0eb0
  ]]

ดังนั้นจึงไม่มีปัญหาการซ้ำซ้อนเช่นในมอดูล B มีการเรียกใช้มอดูล A และในมอดูล C ก็มีการเรียกใช้มอดูล A เช่นกันและในโปรแกรมของเรามีการใช้งานทั้งมอดูล B และ C ก็จะมีการโหลดมอดูล A แค่ตัวเดียว
กรณีที่ต้องการโหลดมอดูลที่คอมไพล์เป็น lua bytecode ก็ไปเพิ่มพาธใน package.path เอาได้

$ ls                                    # "ls" เป็นคำสั่งดูไฟล์ใน Linux
mycode5.lua  test.lua                   # มีสองไฟล์คือไฟล์มอดูลกับไฟล์ทดสอบ
$ luac -o mymod.luac mycode5.lua        # คอมไพล์ไฟล์ "mycode5.lua" ไปเป็น Lua bytecode ชื่อ "mymod.luac"
$ ls
mymod.luac  mycode5.lua  test.lua       # ได้ไฟล์ที่เป็น bytecode เพิ่มมา
$  


  -- ไฟล์ test.lua
  package.path = package.path .. ";./?.luac" -- เพิ่มให้หาไฟล์นามสกุล .luac ในโฟลเดอร์เดียวกัน ( Linux/Mac ถ้าเป็น Windows พาธจะเป็น Drive: กับ \ แทน / )
  
  local mymod = require("mymod")
  print(mymod.subi(10, 2))
  
  -- ผลลัพธ์
  --[[
  8
  ]]
  

Sub module

เราสามารถสร้างมอดูลที่ประกอบไปด้วยกลุ่มของมอดูลได้จากตอนที่แล้วเรื่อง OOP เราจะเอาตัวอย่างตอนที่แล้วมาทำเป็นมอดูลดังนี้

  โครงสร้างไฟล์
  |_ person                             -- มอดูลหลัก "person" เป็นโฟลเดอร์ไปเรียกไฟล์ person/init.lua
  |   |_ init.lua
  |   |_ person.lua                     -- มอดูลย่อย "person" person/person.lua
  |   |_ employee.lua                   -- มอดูลย่อย "employee" person/employee.lua
  |_ main.lua                           -- โปรแกรมหลัก



  -- init.lua
  local M = {}
  M.Person = require("person.person")   -- โหลดคลาส "Person" ( "person.person" == person/person.lua )
  M.Employee = require("person.employee") -- โหลดคลาส "Employee" ( "person.employee" == person/employee.lua )
  return M
  
  -----------------------
  -- person.lua
  local Person = {}
  function Person.new(n, w)
    local obj = {}
    local class = "Person"
    local name = n
    obj.word = w
    
    function obj.getclass()
      return class
    end
    
    function obj.getname()
      return name
    end
    
    function obj.say(w)
      if w then obj.word = w end
      return name.." say: "..obj.word
    end
    
    return obj
  end
  return Person                         -- ส่งออกมอดูล Person
  
  -----------------------
  -- employee.lua
  local Person = require("person.person") -- โหลดคลาส "Person" ( "person.person" == person/person.lua )
  local Employee = {}
  function Employee.new(n, w, s)
    local obj = Person.new(n, w)
    local class = "Employee"
    local salary = tonumber(s) or 0
    
    obj.getbaseclass = obj.getclass
    
    function obj.getclass()
      return class
    end
    
    function obj.setsalary(s)
      if type(s) == "number" then
        salary = s
      end
    end

    function obj.getsalary()
      return salary
    end

    function obj.say(s)
      obj.setsalary(s)
      return obj.word..", I'm "..obj.getname().."\nMy salary is "..salary.."."
    end
    
    return obj
  end
  return Employee                       -- ส่งออกมอดูล Employee
  
  -----------------------
  -- main.lua
  local Employee = require("person.employee") -- "Employee" == person/employee.lua
  local Person = require("person").Person -- "Person" == "Person" ใน person/init.lua
  local john = Person.new("John", "Hello")
  local jack = Employee.new("Jack", "Hi.", 20)
  print(john.say())
  print(jack.say())
  print(Person, Employee)
  print(package.loaded.person["Person"]) -- "Person" ใน person/init.lua
  print(package.loaded["person.person"]) -- person/person.lua
  print(package.loaded.person.Employee) -- "Employee" ใน person/init.lua
  print(package.loaded["person.employee"]) -- person/employee.lua
  local P = require("person")           -- "P" == person/init.lua
  local jane = P.Person.new("Jane", "Bye. :)")
  print(jane.say())
  print(P.Person, P.Employee)
  
  -- ผลลัพธ์
  --[[
  John say: Hello
  Hi., I'm Jack
  My salary is 20.
  table: 0x9f88e0	   table: 0x9f8fd0
  table: 0x9f88e0
  table: 0x9f88e0
  table: 0x9f8fd0
  table: 0x9f8fd0
  Jane say: Bye. :)
  table: 0x9f88e0	   table: 0x9f8fd0
  -- package.loaded["person"] == {Person, Employee}
  -- package.loaded["person"].Person == package.loaded["person.person"]
  -- package.loaded["person"]["Employee"] == package.loaded["person.employee"]
  ]]
  
จบแล้วครับสำหรับเรื่องมอดูลในภาษา Lua นอกจากนี้ยังสามารถใช้มอดูลที่เป็น binary ที่เขียนในภาษา C เช่นพวกไฟล์ .so หรือ .dll ได้แต่ต้องเขียนโดยใช้ C API ของ Lua ซึ่งเราจะไม่กล่าวถึงเพราะจะเกินเนื้อหาสำหรับมือใหม่หัดเขียนโปรแกรมไป

ความคิดเห็น