Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lua: When is it possible to use colon syntax?

Tags:

lua

While I understand the basic difference between . and :, I haven't fully figured out when Lua allows to use the colon syntax. For instance, something like this does work:

s = "test"
-- type(s) is string.
-- so I can write a colon function for that type
function string:myFunc() 
  return #self 
end

-- and colon function calls are possible
s:myFunc()

However the same pattern does not seem to work for other types. For instance, when I have a table instead of a string:

t = {}
-- type(t) is table.
-- so I can write a colon function for that type
function table:myFunc() 
  return #self 
end

-- Surprisingly, a colon function call is not not possible!
t:myFunc() -- error: attempt to call method 'myFunc' (a nil value)
-- But the verbose dot call works
table.myFunc(t)

Moving on to another type:

x = 1
-- type(x) is number.
-- So I was expecting that I can write a colon function 
-- for that type as well. However, in this case even this
-- fails: 
function number:myFunc() 
  return self 
end
-- error: attempt to index global 'number' (a nil value)

I'm currently trying to make sense of this. Is it correct to conclude that

  • certain types like string allow both colon-function-definitions and colon-function-calls.
  • other types like table allow only colon-function-definitions but not colon-function-calls.
  • yet other types like number allow neither.

What exactly is the reason for these differences? Is there a list of all types, showing which type of colon-syntax they support? Is there maybe a workaround for the number case, allowing to write e.g. x:abs()?

like image 615
bluenote10 Avatar asked Oct 10 '15 09:10

bluenote10


3 Answers

The first example on string works because, all strings share the same metatable, and it's stored in the table named string.

From string:

The string library provides all its functions inside the table string. It also sets a metatable for strings where the __index field points to the string table. Therefore, you can use the string functions in object-oriented style. For instance, string.byte(s,i) can be written as s:byte(i).


The second example on table doesn't work because, every table has its own metatable, the table named table is just a collection of all the functions of table library.


Types like numbers don't support metatable by default.

like image 109
Yu Hao Avatar answered Nov 04 '22 12:11

Yu Hao


As a total Lua newbie, it took me a while to understand @Yu Hao's answer, so I'll try to add some details for other beginners. Please correct me if anything is wrong.

As far as I can see, a call like x:someFunc() works if [*]:

  • x has a metatable
  • and the metatable has a field __index
  • which points to a table containing a function someFunc.

As Yu Hao has pointed out, strings automatically get a metatable pointing to the table string, e.g.:

th> s = 'test'

th> getmetatable(s)
{
  __mod : function: 0x40c3cd30
  __index : 
    {
      upper : function: builtin#82
      rep : function: builtin#79
      split : function: 0x40ffe888
      gfind : function: builtin#87
      find : function: builtin#84
      reverse : function: builtin#80
      lower : function: builtin#81
      len : function: 0x40af0b30
      tosymbol : function: 0x40ffe8a8
      myFunc : function: 0x41d82be0 -- note: this comes from our custom function string:myFunc()
      dump : function: builtin#83
      byte : function: builtin#76
      char : function: builtin#77
      gmatch : function: builtin#87
      match : function: builtin#85
      sub : function: builtin#78
      gsub : function: builtin#88
      format : function: builtin#89
    }
}

So in this case s:myFunc() works automatically. In order to use the colon syntax for a table, we can manually set its metatable:

th> function enableColonForTable(t)
..>   meta = {__index = table}
..>   setmetatable(t, meta)
..> end

th> t = {}

th> enableColonForTable(t)

th> t:insert(1) -- works now!

Another observation is that it actually does not matter whether __index points to a table with exactly the same name as the type. Instead of meta = {__index = table}, we also could do:

th> arbitraryScope = {}

th> function arbitraryScope:test() return "something" end

th> t = {}

th> setmetatable(t, {__index = arbitraryScope})
{}

th> t:test()
something   

That is also the key difference to the case of a number. While there are existing tables called string and table, there is no existing table called number. This is why even defining e.g. function number:abs() has failed before. But we can still make it work:

th> number = {}

th> function number:abs() return math.abs(self) end

th> x = -123

th> debug.setmetatable(x, {__index = number})
-123    

th> x:abs()
123 

Note that we had to use debug.setmetatable instead of setmetatable here. The difference between the two seems to be that setmetatable sets the metatable only for the given instance, while debug.setmetatable sets the metatable for the whole type. Apparently setting an individual metatable for numbers is forbidden (and would not make much sense anyways). This means that (in contrast to tables) newly constructed numbers now have the given metatable by default, so this works:

th> y = -42

th> y:abs()
42  

[*] Update

As pointed out by Tom Blodget, x:someFunc() also works if x itself serves as a namespace, i.e., it is a table with a method field someFunc. For instance you could do table:insert(1). But now the namespace (the table called table) is passed as self and you would have added data to the namespace:

th> print(getmetatable(table)) -- note: "table" does not have a metatable
nil 

th> table:insert(1) -- yet a colon syntax call works

th> table
{
  prune : function: 0x4156bde0
  getn : function: 0x41eb0720
  maxn : function: builtin#90
  remove : function: 0x41eb08c8
  foreachi : function: 0x41eb05b8
  sort : function: builtin#93
  concat : function: builtin#92
  unpack : function: builtin#16
  splice : function: 0x4156bdc0
  foreach : function: 0x41eb0688
  1 : 1
  pack : function: builtin#94
  insert : function: builtin#91
}
like image 6
bluenote10 Avatar answered Nov 04 '22 11:11

bluenote10


Supplementary answer:

First, note that a function is a value (aka "first-class citizen").

: is one of three indexing operators in Lua. An indexing operator returns the value of a "field" from an object that can be indexed—be it a function or any other type.

The indexing operators are, in order of generality:

  1. expression [ expression2 ]
  2. expression . identifier
  3. expression : identifier ( parameter-list )

The latter two are just "syntactic sugar" and can be rewritten in the form of any above it.

You would use the second if "expression2" would always the same string that is a valid Lua identifier and you want to hardcode it.

You would use the third if the value returned by indexing "identifier" against "expression" will always be a function that you want to call with the value returned by "expression" as an implicit first parameter. Such a function call is called a "method call."

Also, note that the language/compiler doesn't care/know if the field value is a function or not (you'd get an error at runtime if you try to call a value that isn't a function). Nor does it care/know if the function is a method or not (the function probably won't behave as you intended if you don't pass it appropriate parameters).

Now, the type of the expression value must be any type that can be indexed. Note that expressions don't have compile-time types so if the expression value is of a type that cannot be indexed, that is a runtime error, too. Indexable types are: table and any object with an __index metamethod. Other answers provide details on these.

like image 4
Tom Blodget Avatar answered Nov 04 '22 12:11

Tom Blodget