Last active
March 9, 2020 16:18
-
-
Save tbrunz/18ef96476b176f2b48a5b25398b1c787 to your computer and use it in GitHub Desktop.
Lua metatables demonstrated by implementing a Lua 'case statement' object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env lua | |
-- | |
-- Lua metatables demonstrated by implementing a Lua 'case statement' object. | |
-- | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Show test results of executing a case statement. | |
-- | |
--[[ | |
This function is purely for test purposes; | |
It just provides a nicely-formatted output | |
of executing a case statement case. | |
]]-- | |
function show ( CaseStatement, enumLabel, argument ) | |
print( "Executing case statement for enum '"..enumLabel.. | |
"' for case statement '"..CaseStatement.name.. | |
"', result = '"..argument.."'" ) | |
return enumLabel | |
end | |
------------------------------------------------------------------------------- | |
-- | |
-- Programming error: Trap bad table references & throw an error to report. | |
-- | |
handleBadTableRef = function ( table, key, value ) | |
local tableName | |
local errorMsg | |
-- It's possible this function was called without proper arguments. | |
-- Make sure the first argument is a table before we try to access | |
-- its fields. (Otherwise 'rawget' would bark at us.) | |
if type(table) == "table" then | |
-- Get the table's name, but be SURE to use 'rawget'! | |
-- If the 'name' field isn't defined, we'll get 'nil'. | |
-- Otherwise, using 'table.name' we could end up | |
-- looping right back to this method -- endlessly! | |
name = rawget( table, "name" ) | |
else | |
return error "Missing/insensible arguments" | |
end | |
-- It's possible that the key was 'nil' (i.e., not provided)... | |
if key then | |
errorMsg = "Unknown attribute/method, '"..tostring(key).."', in" | |
else | |
errorMsg = "Nil key provided for" | |
end | |
return error( errorMsg.." table '"..tableName.."'") | |
end | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Prototype Case Statement object | |
-- | |
CaseStatement = { } | |
--[[ | |
Use Lua's "metatable" mechanism to trap and handle bad references in code | |
that uses a case statement object. If a 'case' (table key) is referenced, | |
but never defined, the 'handleBadTableRef' function will be called to | |
generate a sensible message & throw an error. | |
]]-- | |
CaseStatement.__index = handleBadTableRef | |
------------------------------------------------------------------------------- | |
-- | |
-- Case Statement Execution: 'doCase' | |
-- | |
-- Use a given enum to execute a case in a specific case statement object. | |
-- All case statement objects inherit this method from 'CaseStatement'. | |
function CaseStatement:doCase ( enum, ... ) | |
local argType | |
-- Reference the case statement name (for messaging). | |
-- If 'name' is missing, an error will be thrown. | |
local caseStatementName = self.name | |
-- Verify that the type of 'enum' is a table. (In Lua, all data types | |
-- have metatables that can resolve methods applied to non-table types.) | |
argType = type(enum) | |
if argType ~= "table" then | |
error( "doCase called on '"..caseStatementName.."' using a ".. | |
argType.." for a case tag enum; must be a table." ) | |
end | |
-- 'enum' is a case tag, which means it's a table with a name, etc. | |
local enumName = enum.name | |
-- Extract the case handler function by treating 'enum' as a table key. | |
-- This will throw an error if the 'enum' case tag is undefined. | |
-- The 'value' the 'key' refers to is the case function to execute. | |
local handler = self[ enum ] | |
-- Validate the handler 'value' as being a function we can call. | |
argType = type(handler) | |
if argType ~= "function" then | |
error( "Case tag '"..enumName.."' for case statement '".. | |
caseStatementName.."' maps to a "..argType..", not a function." ) | |
end | |
-- If we reach this point, the case function is defined, so call it | |
-- with the remaining arguments we were called with, returning all | |
-- the return values it generates. Note that this is a tail call. | |
return handler( ... ) | |
end | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Prototype Enum object | |
-- | |
Enumerator = { } | |
--[[ | |
Use Lua's "metatable" mechanism to trap and handle bad references in code | |
that uses an enumerator object. If an enum (table key) is referenced, | |
but never defined, the 'handleBadTableRef' function will be called to | |
generate a sensible message & throw an error. | |
]]-- | |
Enumerator.__index = handleBadTableRef | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Specific enum object 'This' | |
-- | |
ThisEnum = { } | |
-- Refer non-specific attribute/method lookups to the prototype enum object: | |
setmetatable( ThisEnum, {__index=Enumerator} ) | |
-- Give this enum a name, for diagnostic messages: | |
ThisEnum.name = "ThisEnumerator" | |
-- By using (empty) tables for each enum, all enums, system-wide, are unique... | |
ThisEnum.labelA = { } | |
ThisEnum.labelB = { } | |
ThisEnum.labelC = { } | |
------------------------------------------------------------------------------- | |
-- | |
-- Specific enum object 'That' | |
-- | |
ThatEnum = { } | |
-- Refer non-specific attribute/method lookups to the prototype enum object: | |
setmetatable( ThatEnum, {__index=Enumerator} ) | |
-- Give this enum a name, for diagnostic messages: | |
ThatEnum.name = "ThatEnumerator" | |
-- By using (empty) tables for each enum, all enums, system-wide, are unique... | |
ThatEnum.label1 = { } | |
ThatEnum.label2 = { } | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Specific case statement definition A | |
-- | |
ThisCaseStmt = { } | |
-- Refer non-specific attribute/method lookups to the prototype case object: | |
setmetatable( ThisCaseStmt, {__index=CaseStatement} ) | |
-- Give this case statement a name, for diagnostic messages: | |
ThisCaseStmt.name = "ThisCaseStatement" | |
-- Define the behavior of each case for this case statement: | |
-- | |
ThisCaseStmt[ ThisEnum.labelA ] = function ( arg1 ) | |
return show( ThisCaseStmt, "labelA", arg1 * arg1 ) | |
end | |
ThisCaseStmt[ ThisEnum.labelB ] = function ( arg1, arg2 ) | |
return show( ThisCaseStmt, "labelB", arg1 * arg2 ) | |
end | |
ThisCaseStmt[ ThisEnum.labelC ] = function ( arg1, arg2, arg3 ) | |
return show( ThisCaseStmt, "labelC", arg1 + arg2 + arg3 ) | |
end | |
------------------------------------------------------------------------------- | |
-- | |
-- Specific case statement definition B | |
-- | |
ThatCaseStmt = { } | |
-- Refer non-specific attribute/method lookups to the prototype case object: | |
setmetatable( ThatCaseStmt, {__index=CaseStatement} ) | |
-- Give this case statement a name, for diagnostic messages: | |
ThatCaseStmt.name = "ThatCaseStatement" | |
-- Define the behavior of each case for this case statement: | |
-- | |
ThatCaseStmt[ ThatEnum.label1 ] = function ( argA ) | |
return show( ThatCaseStmt, "label1", argA * argA ) | |
end | |
ThatCaseStmt[ ThatEnum.label2 ] = function ( argA, argB ) | |
return show( ThatCaseStmt, "label2", argA * argB ) | |
end | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- | |
-- | |
-- Test cases for case statements | |
-- Note: We really *should* be using Lua Rock 'busted' for testing... | |
-- | |
-- Test using an indirect case statement (as a variable): | |
Case = ThisCaseStmt | |
result = Case:doCase( ThisEnum.labelA, 5 ) | |
result = Case:doCase( ThisEnum.labelB, 3, 4 ) | |
-- Test intentional errors: | |
--result = Case:doThisCase( ThisEnum.labelC, 3, 7, 11 ) -- Err: bad method | |
--result = Case:doCase( ThisEnum.labelD, 15 ) -- Err: bad enum | |
--result = notaCase:doCase( ThisEnum.labelB, 3, 4 ) -- Err: bad table | |
-- Test using an explicitly-named case statement: | |
result = ThatCaseStmt:doCase( ThatEnum.label1, 13 ) | |
print( "I just executed a case for selector '"..result.."'... " ) | |
------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment