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

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


Function

จากเนื้อหาบทก่อนๆ ฟังก์ชันนั้นคือโปรแกรมย่อยที่เราสามารถเรียกใช้ซ้ำได้เพื่อลดขั้นตอนการเขียนโค้ดของเราประหยัดเวลาและแก้ไขได้ง่าย ซึ่งมีรูปแบบดังนี้
function ชื่อฟังก์ชัน(ตัวแปร1, ตัวแปร2, ..., ตัวแปรN)
  คำสั่งต่างๆ ให้ทำงานในฟังก์ชัน
  return ค่าที่ส่งกลับ1, ค่าที่ส่งกลับ2, ..., ค่าที่ส่งกลับN  -- ส่วนนี้มีหรือไม่ก็ได้ถ้าไม่มีการส่งค่ากลับก็ไม่ต้องใส่
end
หรือทำเป็น Anonymous function ( ฟังก์ชันไม่มีชื่อ ) สำหรับเป็นตัวแปรหรือ argument ในฟังก์ชันอื่น
function (ตัวแปร1, ตัวแปร2, ..., ตัวแปรN)
  คำสั่งต่างๆ ให้ทำงานในฟังก์ชัน
  return ค่าที่ส่งกลับ1, ค่าที่ส่งกลับ2, ..., ค่าที่ส่งกลับN  -- ส่วนนี้มีหรือไม่ก็ได้ถ้าไม่มีการส่งค่ากลับก็ไม่ต้องใส่
end


-- ตัวอย่าง1 สร้าง function ปกติ
function show_add(x, y) -- ฟังก์ชันนี้ไม่มีการส่งค่ากลับแต่พิมพ์ค่าออกมาเลย
  print(x .."+"..y .."="..(x+y))
end
show_add(2,3)

-- ตัวอย่าง2 สร้าง anonymous function ใส่ในตัวแปร
add = function(x,y) return x+y end
print("2+3="..add(2,3))

-- ผลลัพธ์1 และ 2
--[[
2+3=5
]]

-- ตัวอย่าง3
function add_list(t, fn)               -- รับค่าเป็น table กับ function
  if type(t)~="table" or type(fn)~="function" then
    return {},{}                       -- ถ้าพารามิเตอร์1ไม่ใช่ table หรือพารามิเตอร์2ไม่ใช่ function คืนค่าเป็น table เปล่า 2 ตัว
  end
  local new_list = {}                  -- สร้าง table ใหม่
  for i, v in ipairs(t) do             -- วนลูปหาค่าสมาชิกใน table
    if type(v)=="number" then          -- เอาเฉพาะค่าตัวเลขใน table เดิม
      table.insert(new_list,fn(v))     -- เอาค่าใส่ในฟังก์ชันแล้วคืนค่ากลับใส่ table ใหม่
    end
  end
  return t, new_list                   -- คืนค่าเป็น table เดิมกับ table ใหม่
  print("สวัสดี")                        -- ส่วนนี้จะไม่มีวันได้ทำงานเพราะจบที่ return ไปก่อน
end
-- สร้าง anonymous function ใส่เป็น argument ใน function อื่น
local a, b = add_list({2,"a",6,7}, function(x) return x+6 end)
-- add_list(t, fn) จะได้ t={2,"a",6,7} และ fn=function(x) return x+6 end
-- ดังนั้น new_list={2+6,6+6,7+6}=(8,12,13}
print"a list(ค่าเดิม)"
for i, v in ipairs(a) do print(i,v) end
print("b list(ค่าใหม่)")
for i, v in ipairs(b) do print(i,v) end

-- ผลลัพธ์
--[[
a list(ค่าเดิม)
1       2
2       a
3       6
4       7
b list(ค่าใหม่)
1       8
2       12
3       13
]]

Local function

local function ชื่อฟังก์ชัน()
  ......
end

local ตัวแปร = function()
  ......
end

do
  local function say() print"Hi" end
  say()     -- แสดงข้อความ Hi
end
say()       -- error

Argument และ Parameter

Argument คือตัวแปรหรือค่าที่ส่งให้กับฟังก์ชัน ( กรณีที่เรียกใช้โปรแกรมทาง command line ค่าที่ตามหลังโปรแกรมก็เรียก argument เหมือนกัน )
Parameter คือตัวแปรในฟังก์ชันที่สร้างมารับ argument ที่ส่งเข้ามาเช่น

function greet(name)
  print("Hello "..name)
end
local n1 = "John"
greet(n1)
greet("Tom")
จากตัวอย่างข้างบนตัวแปร name คือ parameter ของฟังก์ชัน greet() ส่วนตัวแปร n1 กับข้อความ "Tom" คือ argument

ฟังก์ชันที่คืนค่าหลายค่า

Lua มีกฏอยู่ว่าถ้ามีการเรียกฟังก์ชันที่คืนค่าหลายค่าเพียงตัวเดียวหรือเป็นนิพจน์ตัวสุดท้ายในลิสต์จะคืนทุกค่าแต่ถ้ามีค่าอื่นต่อท้ายในลิสต์จะคืนแค่ค่าแรกค่าเดียว อธิบายไม่ค่อยถูกถ้าใครงงดูตัวอย่างแล้วกัน

function returnArgs(a1, a2, a3)
  return a1, a2, a3
end
print(1, 2, 3, 4)
print(returnArgs(1, 2, 3))
print("a", returnArgs(1, 2, 3))
print(returnArgs(1, 2, 3), "a")
print(returnArgs(1, 2, 3), returnArgs(4, 5, 6))

-- ผลลัพธ์
--[[
1   2   3   4
1   2   3
a   1   2   3
1   a
1   4   5   6
]]

First class function และ High order function

เป็นแนวคิดใน Functional programming โดยที่ First class function นั้นคือการที่ฟังก์ชันมีคุณสมบัติเป็นวัตถุชนิดข้อมูลประเภทหนึ่งดังนั้นจึงสามารถใส่ในตัวแปรหรือเป็น value หรือ key ใน table ได้

function returnNil() return nil end
print(returnNil())                     -- พิมพ์ค่าที่ส่งกลับมาจากฟังก์ชัน returnNil()
print(returnNil)                       -- พิมพ์ค่าในตัวแปร returnNil

-- ผลลัพธ์
--[[
nil
function: 0x12bb9f0                    -- บอกว่าเป็นข้อมูลประเภท function อยู่ที่ตำแหน่งหน่วยความจำ 0x12bb9f0
]]

-- ดังนั้นฟังก์ชั่นข้างบนจึงมีค่าเท่ากับ
returnNil = function() return nil end

local tbl = {retNil=returnNil}         -- table ชื่อ tbl เก็บข้อมูลเป็นฟังก์ชัน returnNil มีคีย์เป็นข้อความ "retNil"
tbl[returnNil] = "return_nil"          -- เพิ่มข้อมูลข้อความ "return_nil" มีคีย์เป็นฟังก์ชัน
print("คีย์","ข้อมูล")
for key, val in pairs(tbl) do print(key,val) end
print(tbl.retNil())                    -- พิมพ์ค่าที่ส่งกลับมาจากฟังก์ชัน

-- ผลลัพธ์
--[[
คีย์                      ข้อมูล
retNil                  function: 0x12bb9f0
function: 0x12bb9f0     return_nil
nil
]]
ต่างกับข้อมูลตัวเลข ข้อมูลตรรกะ และข้อมูลข้อความที่ตัวแปรจะเก็บค่าของข้อมูล แต่ function และ table ตัวแปรจะเก็บข้อมูลเป็นตำแหน่งหน่วยความจำที่เก็บข้อมูลนั้นแทน

local a, b
a = 10
b = a
a = 20
print(a,b)

-- ผลลัพธ์
--[[
20     10
]]

local c, d
c = {a=1}                              -- c เก็บตำแหน่งหน่วยความจำที่เก็บ {a=1} สมมุติเป็น 0x12ca720
d = c                                  -- d เก็บข้อมูลใน c (0x12ca720)
print("c.a", "d.a", "c.b", "d.b")
print(c.a,   d.a,   c.b,   d.b}
c.b = 2                                -- เพิ่มข้อมูลใน 0x12ca720 จาก {a=1} เป็น {a=1, b=2}
print(c.a,   d.a,   c.b,   d.b}
c = {a=2, b=3}                         -- c เก็บตำแหน่งหน่วยความจำที่เก็บ {a=2, b=3} สมมุติเป็น 0x12c91f0
print(c.a,   d.a,   c.b,   d.b}

-- ผลลัพธ์
--[[
c.a   d.a   c.b   d.b
1     1     nil   nil
1     1     2     2
2     1     3     2
]]
ดังนั้นถึงแม้จะสามารถใช้ฟังก์ชันเป็นคีย์ใน table ได้แต่ก็ไม่ควรทำเพราะจะอ้างถึงคีย์ได้ยาก

-- จากตัวอย่างก่อนหน้าเรามี table ชื่อ tbl มีคีย์เป็นฟังก์ชันหนึ่งตัวถ้าต้องการเรียกใช้งานเราต้องจับฟังก์ชันใส่ตัวแปรเพื่อให้อ้างถึงได้ก่อน
local key1
print(":tbl[key]:", ":key:", ":value:")
for k, v in pairs(tbl) do
  if type(k)=="function" then key1 = k end
  print(tbl[k], k, v)
end
print("")
print(":tbl[key1]:", ":key1:")
print(tbl[key1], key1)

-- ผลลัพธ์
--[[
:tbl[key]:              :key:                   :value:
function: 0x12bb9f0     retNil                  function: 0x12bb9f0
return_nil              function: 0x12bb9f0     return_nil

:tbl[key1]:             :key1:
return_nil              function: 0x12bb9f0
]]
ในส่วนของ High order function คือคุณสมบัติที่ฟังก์ชั่นสามารถมี parameter เป็นฟังก์ชันหรือสามารถ return ค่าเป็นฟังก์ชันได้

function map(func,tbl)                 -- รับค่า function (func) กับ table (tbl)
  local result = {}
  for i = 1, #tbl do
    result[i] = func(tbl[i])           -- วนลูปเอาค่าจาก func ที่รับค่าสมาชิกใน tbl ได้ผลลัพธ์ใส่ใน table ใหม่
  end
  return result
end

function add2(val) return val + 2 end  -- สร้างฟังก์ชันบวกค่าที่รับมากับ 2

local x = {2, 3, 4, 5}
local y = map(add2, x)                 -- y == {4,5,6,7}

function addN(N)                       -- สร้างฟังก์ชันรับค่าเป็นจำนวน N
  return function(val)                 -- แล้วคืนค่ากลับเป็นฟังก์ชันบวกค่าที่รับมากับ N
    return val + N
  end
end

local add4 = addN(4)
local z = map(add4, x)                 -- z == {6,7,8,9}

Recursive function

คือฟังก์ชันที่มีการเรียกตัวเอง ตัวอย่างโปรแกรมหา factorial เช่น 5! = 5 x 4 x 3 x 2 x 1

-- แบบใช้ลูป
function fact1(number)
  local fact = 1
  for i = number, 1, -1 do  -- ใช้ for i = 1, number do ก็ได้เหมือนกัน
    fact = fact * i
  end
  return fact
end

-- สามารถเขียนแบบ recursive ได้ดังนี้
function fact2(number)
  if number==1 then
    return 1
  else
    return number * fact2(number-1)
  end
end

print("fact1(5)= ",fact1(5))
print("fact2(5)= ",fact2(5))

-- ผลลัพธ์
--[[
fact1(5)=    120
fact2(5)=    120
]]
ข้อสังเกตถ้าเราสร้าง anonymous function แบบ recursive ใส่ในตัวแปลแบบ local ตามตัวอย่างตอนต้นบทจะ error เพราะตามขั้นตอนการทำงานจะเริ่มจากฝั่งขวาของเครื่องหมาย " = " คือสร้าง anonymous function ขึ้นมาก่อนจะส่งค่า address ที่เก็บฟังก์ชันมาให้ตัวแปรฝั่งซ้ายแต่ตัวแปรแบบ local จะยังไม่ถูกสร้างขึ้นมาทำให้ฟังก์ชันไม่รู้จักจึงไม่สามารถเรียกตัวเองได้วิธีแก้คือให้ประกาศตัวแปรก่อนแล้วค่อยกำหนดให้ตัวแปร = ฟังก์ชัน

local fact = function(n) return (n==1) and 1 or (n*fact(n-1)) end
print(fact(5))                         -- error

local fact
fact = function(n) return (n==1) and 1 or (n*fact(n-1)) end
print(fact(5))                         -- 120

local function fact(n) return (n==1) and 1 or (n*fact(n-1)) end
print(fact(5))                         -- 120
-- สร้างฟังก์ชันแบบนี้ระบุชื่อในตอนสร้างฟังก์ชันเลยจึงไม่ error
และถ้ามีฟังก์ชันที่เรียกใช้ฟังก์ชันอื่นถ้าฟังก์ชันที่ถูกเรียกใช้เป็น local จะต้องถูกประกาศก่อนตามลำดับแต่ถ้าเป็น global จะประกาศส่วนใหนก่อนก็ได้ไม่ต้องเรียงลำดับการเรียกใช้แต่ต้องอยู่ก่อนส่วนโปรแกรมหลัก

Variadic Arguments หรือ Varargs

คือ argument พิเศษที่ใช้แทน arguments หลายๆ ตัวในฟังก์ชันที่ไม่ได้กำหนดจำนวน arguments ที่รับมาโดยใช้จุดสามจุด " ... " เป็นสัญลักษณ์

function testArg(x, ...) return x, ... end
print(testArg("a", 1, 2, "c", 4))
print(testArg(5, "x", 8))

-- ผลลัพธ์
--[[
a   1   2   c   4
5   x   8
]]

การจัดการกับ varargs


function testArg2(...)
  for i = 1, select("#", ...) do       -- select("#", ...) หาจำนวน arguments
    print(select(i, ...))              -- select(ตำแหน่ง, ...) หา argument ที่ตำแหน่งนั้นขึ้นไป
  end 
end
print("test1")
testArg2("a", 1, 2, "c", 4)
print("test2")
testArg2(5, "x", 8)

-- ผลลัพธ์
--[[
test1
a   1   2   c   4
1   2   c   4
2   c   4
c   4
4
test2
5   x   8
x   8
8
]]

การแปลง varargs เป็น table


function arg2tbl1(...) return {...} end

function arg2tbl2(...) return table.pack(...) end

local t1 = arg2tbl1(4,5,6)             -- t1 == {4,5,6}     #t1 == 3
local t2 = arg2tbl2(4,5,6)             -- t2 == {4,5,6,n=3}  t2.n == 3
จากตัวอย่างข้างต้นสามารถแปลง vararg เป็น table เพื่อให้จัดการง่ายขึ้น

function testArg3(...)
  local t = {...}
  for i = 1, #t do                     -- select("#", ...) หาจำนวน arguments
    print(t[i])                        -- select(ตำแหน่ง, ...) หา argument ที่ตำแหน่งนั้นขึ้นไป
  end 
end
print("test1")
testArg3("a", 1, 2, "c", 4)
print("test2")
testArg3(5, "x", 8)

-- ผลลัพธ์
--[[
test1
a
1
2
c
4
test2
5
x
8
]]

Closure

คือคุณสมบัติของฟังก์ชันที่สามารถเข้าถึงตัวแปรที่อยู่ใน scope เดียวกับฟังก์ชันนั้นๆ ได้ (อยู่ใน block เดียวกัน)

do
  local tbl = {"a", "b", "c"}          -- เป็นตัวแปร local ทำงานเฉพาะใน do...end block
  function closure()
    for key, value in ipairs(tbl) do
      print(key, value)
    end
  end
  print("ใน block")
  print(tbl)                           -- เรียกได้เพราะอยู่ใน scope เดียวกัน
  closure()                            -- ฟังก์ชันสามารถเข้าถึงตัวแปร tbl ที่อยู่ใน scope เดียวกันได้
end

print("นอก block")
print(tbl)                             -- มองไม่เห็นตัวแปร tbl เพราะอยู่นอก scope
closure()                              -- ฟังก์ชันยังเข้าถึงตัวแปรได้

-- ผลลัพธ์
--[[
ใน block
table: 0x1d39140
1     a
2     b
3     c
นอก block
nil
1     a
2     b
3     c
]]
ย้อนกลับไปดูฟังก์ชัน addN() ในตัวอย่าง High order function ฟังก์ชันที่ addN ส่งค่ากลับมาก็เป็น closure เช่นกัน

local add_5 = addN(5)                  -- เก็บค่า 5 เข้าในพารามิเตอร์ N ของ addN()
                                       -- แล้ว return ฟังก์ชันไปเก็บในตัวแปร add_5
print(add_5)                           -- พิมพ์ค่าตัวแปร add_5 เป็นตำแหน่งฟังก์ชัน
print(add_5(6))                        -- พิมพ์ค่าที่ส่งกลับจากฟังก์ชัน add_5(6) ได้ 6 + 5

-- ผลลัพธ์
--[[
function: 0x41b010
11
]]
จบแล้วครับกับเรื่อง function พิมพ์เองงงเองถ้ามีข้อสงสัยก็ถามได้ครับ ตอนหน้าเราจะมาว่าถึง Standard Library กัน

บทความถัดไป

ความคิดเห็น