Jump to content


Photo

Lua Guide for programmers


    3 replies to this topic

    #1 timstix

    timstix

      Newbie

    • Members
    • Pip
    • 6 posts

      Posted 07 January 2015 - 05:22 PM

      This has been copied from a post that Arcon made in the legacy forums. 

       

      http://legacy.window...985#entry218985

       

      [Edit by Arcon: I actually changed a few things in this post, so it's not a direct copy. Many things in the old guide were outdated (mostly regarding our API, since large parts of it changed) and some were entirely incorrect (since I was a Lua noob myself when I wrote it).]

       

      Since Lua will be of interest to anyone with moderate programming skills, but at the same time has some oddities that programmers from other languages or environments will find... well, odd, I thought I'd write up my experiences learning Lua to help others avoid some Lua beginner's mistakes. Fortunately, it's not that much of an issue, now that an innocent mistake doesn't crash POL anymore, but it would still help to know some tricks to getting around in this language.

      What's familiar?

      As many interpreted scripting languages, Lua has a focus on list management and string manipulation. That's why it provides a few syntactic shorthands for dealing with such constructs, as well as functions dealing with issues regarding those subjects. Lists can be explicitly declared using braces: list = {1, 2, 3}.

      Here are a few keywords that will give people an idea of some of Lua's design aspects:

      • Interpreted
      • Dynamically and weakly typed
      • Operator overloading
      • First class functions
      • Case-sensitive
      • Object-oriented (ish)



      Objects

      Lua is provides an object-oriented interface, although it's not natively object-oriented. Lua "objects" also have some drawbacks that will be explained later. For people with an object-oriented background you can say that class methods are accessed by the . (dot) operator, while accessing object methods is done by the : (colon) operator. There is a reason for that that will be explained later. Assuming a class cls, an object of that class obj and a method called method, these two are the same:

       

      cls.method(obj, arg1, arg2, ...)
      

       

      and

       

      obj:method(arg1, arg2, ...)
      

       


      Programming for Windower, you'll mostly need the string and table classes. For a complete reference of functions for those two (and others), check the Lua 5.1 API.

      Lua also has a notion of a null value, named nil. It has a few special properties, that will be explained later.

      Scoping and syntax

      Lua does not use braces to denote blocks, but instead uses dothen and similar tokens to denote the beginning of a block, and end to denote the end.

      Aside from that, Lua is reminiscent of JavaScript, in that it does not usually require semicolons or other tokens to denote the end of a statement, but can confuse the interpreter in certain situations.

      Types and coercion

      As many other scripting languages, Lua uses dynamic weak typing. This implies that variables do not need to have a fixed type, and can be reassigned something else entirely where necessary. It also means that functions and operators do not work on fixed types, but can get varying types of arguments. Sometimes this forces Lua to coerce types.

      This does not happen often, however. Lua only converts numbers to strings and vice versa, where appropriate. The operator decides the outcome (.. is the concatenation operator):

       

      > = "2"+5
      7
      > = 4/"2"
      2
      > = "a"..5
      a5
      

       

      Another kind of coercion happens for truth-value checking. Similar to a few other languages, Lua treats everything as true, except for false and nil.

      This means that if varname then ... end can be used to check if varname has been defined, but it also has some pitfalls; if varname has been declared but set to false, it will still evaluate to false, so the check for nil should be explicit.

      Specifically for Windower, people will want to output strings to either the Windower console or to the FFXI chatlog. Lua provides a tostring function, which can take any argument at all and output a string representation of it. This is not needed for numbers, but for other values such as objects, nil or boolean values.

      Variadic functions

      Also like some other languages, it provides an interface for functions that take optional arguments, keyword arguments, or argument lists of arbitrary length.

      If a function is called less arguments than specified, the remaining arguments will be nil. If a function is called with more arguments than specified, the superfluous arguments will be lost, unless the function is specifically declared to accept a variable number of arguments. This is done with the ... operator.


      It's important to note that neither of these will give an error, meaning error-checking has to be implemented manually, where required.

      What's different?

      This section will largely mirror the previous one, only highlight the sometimes subtle, sometimes glaring and sometimes entirely incomprehensible differences Lua presents when compared to other languages.

      Operators

      Lua is very limited to assignment operators. It has no in-place assignment operators whatsoever. ++ and -- do not exist, neither do += and any similar forms.

      For comparison operators, the usual is available, only inequality is checked with ~= instead of the usual !=.


      Arithmetic operators are largely the same, only exponentiation has its own operator and is denoted by ^. Bitwise operators are not present in the native library although we do provide the BitOp library, which offers functions that provide bitwise operations.

      Logical operators are written out: and and or. Notable here is that or returns the first non-falsey value (as described in the type coercion above). This is often use to define default operators:

       

      foo = function(bar)
          print(bar or 10)
      end
      
      > = foo(5)
      5
      > = foo('baz')
      baz
      > = foo()
      10
      

       

      and returns the last non-falsey value and is similarly used, especially together with or, to get the effect of a ternary conditional:

       

      > = true and 'this' or 'that'
      this
      > = false and 'this' or 'that'
      that
      

       

      However, this does not work if the 'this' value is false, so the programmer has to be wary of that.

       

      Additionally, string concatenation is done by .. (double dot). This can be confusing, as Lua also uses . (dot) for accessing class methods, and ... (triple dot) for variadic arguments.

      # returns the length of both tables and strings.

      There is no way to create new operators, but some existing operators can be overloaded rather inconveniently using metatables (reminiscent of JavaScript's prototypes). That's a construct that won't be explained further at this point, but may be included at a later date for advanced programmers.

      Strings

      Lua has some rather weird string notions. It provides the usual string representation with double quotes "this is a string", and like some languages also provides single-quoted strings 'this is another'. There is no difference between the two, but there is a catch: neither of these methods work over more than one line. To provide a multiline string interface, Lua uses double brackets:

       

      [[this
      is a
      multiline string]]
      

       


      String splitting deserves a special mention, as there isn't any in Lua. For that you need to make your own function or use the stringhelper.lua in the /libs.

      Comments

      Comments are prefixed with a -- (double hyphen). Everything from there until the end of the line is considered a comment and not parsed. However, this only works for one line. Similarly to strings, multiline comments are enclosed in double brackets, but preceded by --:

       

      --[[this is a
      multiline comment]]
      

       


      Tables

      Tables, as mentioned before, are Lua's analogon to arrays and lists. However, they are very different in certain regards. For starters, tables are implemented as hash maps, and always map a key to a value. That means there's no way to define pure lists, but instead {'a','b','c','d'} is identical to {[1]='a', [2]='b', [3]='c', [4]='d'}. You access and assign table values with the [] operator, similar to many other languages.

       

      > t = {a=4, b='c'}
      > t2 = {}
      > t2[1] = t
      > t2[2] = t['a']
      > t2['bla'] = t[2]
      > = t2[2]
      4
      

       

      Surprisingly (to some), this does not error. t[2] was never defined, but tables return nil for undefined keys. This is a problem when trying to iterate over integer keys, as the table assume it ended as soon as it encountered a nil value, even if higher keys still have other values.

      Also, if no keys are provided at the definition of a table, a table will be 1-indexed, not 0-indexed, as people may know from other languages:

       

      > t = {4, 5, 6}
      > = t[1]
      4
      > = t[2]
      5
      > = t[#t]
      6
      > = t[0]
      nil
      

       


      The latter works, because t[0] is undefined, and undefined values all return nil.

      Another interesting point is that string-keyed tables can be accessed both with bracket syntax (t['bla']) as well as dot syntax (t.bla). The "object-oriented" feature with classes and objects we talked about earlier is really just that, accessing table keys, with tables being the objects. The operator is merely syntactic sugar for the following:

       

      > s = 'foo'
      > = string.rep(s, 3)
      foofoofoo
      > = s:rep(3)
      foofoofoo
      

       

      It can only be used for function calls and only if the "object" (i.e. table) you use it on has the "class" (i.e. another table) set as its metatable.

       

      Unlike in some languages, it's not possible to slice tables without writing a custom function for that. However, there will be functions provided in libraries that can be included in Windower addons.

      Tutorial

      So much for the similarities and differences, now for some specifics about how to actually program in Lua. This will mostly cover syntax and how to use certain features of the language.

      Assignment

      Lua has a global and a local scope. The global scope crosses between functions and variables are global by default. To restrict a variable to the local scope (within a function, loop or block), you need to explicitly declare them with the local keyword:

       

      foo = function()
          x = 5
          local y = 2
          print(x, y)
      end
      
      > foo()
      5       2
      > print(x, y)
      5       nil
      

       

      A useful assignment method to append an element to a table is as follows:

       

      > t = {'a', 'b', 'c'}
      > = t[3]
      c
      > = t[4]
      nil
      > t[#t+1] = 'd'
      > = t[4]
      d
      

       

      This can be used in a loop, to populate a table by appending a value in each iteration.

      Another useful feature is multiple assignment, especially when coupled with another Lua feature, multiple return. In some languages, returning multiple values requires putting them into a data structure, such as a list or array, and then extracting them again. In Lua, there's a syntactic mechanism for that:

       

      foo = function()
          return 13, 7
      end
      
      > a = foo()
      > print(a)
      13
      > a, b = foo()
      > print(a, b)
      13      7
      > a, b, c = foo()
      > print(a, b, c)
      13      7       nil
      

       


      Loops

      Lua provides a standard while loop (while cond do body end) and a self-explanatory repeat loop (repeat block until condition). It also provides a for loop, with different syntax:

       

      > startnum = 3
      > endnum = 13
      > stepsize = 2
      > for i = startnum, endnum, stepsize do print(i) end
      3
      5
      7
      9
      11
      13
      

       

      stepsize is optional and defaults to 1. It also offers an iterator-based for loop. The two main iterators are pairs and  ipairs, which iterate over all table keys and numeric integral table keys respectively. The latter is often used to iterate over a traditional array (or the Lua equivalent, a table with consecutive integer keys starting at 1):

       

      > for key, value in pairs({foo = 3, bar = 1, baz = 2}) do print(key, value) end
      foo     3
      bar     1
      baz     2
      

       

      Iterating over tables

      The previous looping function can be used on some tables, namely tables with integer keys that don't skip any values:

       

      > t = {'a', 'b', 'c'}
      > for i = 1, #t do print(i) end
      a
      b
      c
      

       


      All other tables will have to be iterated over by making pairs of the key/value pairs. This is done with the appropriately named pairs function:

       

      > t = {a='c', b='a', c='b'}
      > for key, val in pairs(t) do print(key, val) end
      a       c
      c       b
      b       a
      

       


      As you can see, this does not maintain the original input order, and thus behaves like a regular hashmap. For array-tables, you can use ipairs to iterate over the values in an ascending order, but that will only work if the keys are sequential integers with no gaps. It will not work for non-numeric keys at all.

      Functions and arguments

      A few function definitions were already shown above, as well as some normal function behavior. Functions are defined with a simple function(arg1, arg2, ...) body end block. The argument handling was also mentioned before, except for variadic arguments. Assume we want to write a max() function, which takes an arbitrary number of arguments, and returns the maximum of those. To do that, we use this:

       

      max = function(...)
          -- Code here
      end
      

       

      When this function is called, the ... will contain the arguments, but not in a table format, instead in a raw sequence format, such as returned by a function that returns multiple arguments:

       

      max = function(...)
          local a, b, c = ...
          print(a, b, c)
      end
      
      > max(1, 2, 3)
      1       2       3
      > max(1)
      1       nil     nil
      

       

      Most often, you'll probably want the values in a table. To achieve that, simply wrap the three dots in table braces:

       

      max = function(...)
          local args = {...}
      end
      

       

      From here we can iterate over it, as defined above, to find the highest value:

       

      max = function(...)
          local args = {...}
          local highest
          for i = 1, #args do
              if highest == nil or highest < args[i] then
                  highest = args[i]
              end
          end
          return highest
      end
      

       

      This will return nil if no arguments were provided, because then highest will never be assigned to.

      Handling Windower functions

      Finally, a short explanation of how to handle the Windower-related functions and behavior.

      Player data and mob array

      Both player data and mob data is stored in Lua tables. The mob array holds all the information you have available on monsters, NPCs, PCs (including yourself) and objects. You can access it, by providing an index. The first confusing thing is that player characters have two IDs. One permanent ID which is your global character identifier. Additionally, every object in a zone has an associated ID within that zone, and as a player character, you also get one as soon as you zone in. For anything that deals with players inside a zone, you'll need to use the latter so-called mob index or just index.

      In contrast, the player array contains information about your own character. This has information about your equipment, your inventory, job, subjob, etc, whereas the mob array only holds information about you that it also has for any other player character.

      The player array can be accessed by the windower.ffxi.get_player() function, and the mob array with the windower.ffxi.get_mob_by_index() function. For example, to get the name of your current target, you would do this:

       

      player = windower.ffxi.get_player()
      index = player.target_index
      target = windower.ffxi.get_mob_by_index(index)
      target_name = target.name
      

       

      Events

      Addons are event-driven, meaning they can't do things on their own, and instead react to events. This even includes sending commands, because they're caught with the appropriate event. A full list of events can be found here. The function to register events with is called, appropriately, windower.register_event. The first arguments to that function are strings that represent an event name. The last argument needs to be a function that should be executed every time any of the previously provided events are triggered.


      For example, if we wanted to get the name of a character who /waves at us, we would have to use the "emote" event:

       

      windower.register_event('emote', function(sender, target, emote, motion_only)
          if emote == 8 and target == winodwer.ffxi.get_player().index then -- /wave has ID 8
              local name = windower.ffxi.get_mob_by_index(sender).name
              -- Do something with name here
          end
      end)
      

       


      Interface functions

      These functions are designed to communicate with external functions, such as FFXI related Windower functions. The full list of the functions available can be found here.

      For example, let's amend the previous function to a /tell to the person who waved at us:

       

      windower.register_event('emote', function(sender, target, emote, motion_only)
          if emote == 8 and target == winodwer.ffxi.get_player().index then
              local name = windower.ffxi.get_mob_by_index(sender).name
              windower.send_command('input /tell '..name..' What\'s up?') -- Escape the apostrophe
          end
      end)
      

       

      Register Windower commands

      One of the most important parts is how to control the addons you write. For that, you need to be able to send commands from the FFXI chatlog (or the Windower console) to Lua. This is done with the following syntax: //lua command <command> [arg1[, arg2[, ...]]. (Note: You can abbreviate //lua command to //lua c.)

      This is caught addon-side with an event, as mentioned above, specifically the "addon command" event. It takes an arbitrary number of arguments, separated by whitespaces in the chat log. The <command> is whatever you define it as in the _addon table.

       

      If you have an addon called "Position" which outputs your target's x or y position depending on what you specify, you could write the following command handler:        

       

      _addon.command = 'pos'
      
      windower.register_event('addon command', function(xory)
          if xory == 'x' then
              windower.ffxi.get_mob_by_index(windower.ffxi.get_player().target_index).x_pos
          elseif xory == 'y' then
              windower.ffxi.get_mob_by_index(windower.ffxi.get_player().target_index).y_pos
          else
              windower.add_to_chat(160, 'No axis or invalid axis specified.')
          end
      end)
      

       


      Now if you type //lua c pos xx gets passed as the first argument to the specified event function, which is called xory, and that argument is then further analyzed.

      Let's assume we want to write an addon "MTell" that sends a /tell to multiple people for whatever reason. We want

       

      //lua c mtell name1, name2, ... send <message>
      

       

      to expand to

       

      input /tell name1 message; wait 2; /tell name2 message; wait 2; ...
      

       

       


      where the number of names should be variable. So send should be a token word, that separates the name list from the message.

      To achieve that, we would have to define the "addon command" event function variadic, because the number of arguments can be variable. A possible solution would be this:

       

      windower.register_event('addon command', function(...)
          local args = {...}
      
          local index = 1
      
          local names = {}

          while args[index] ~= 'send' do         names[#names+1] = args[index]         index = index + 1     end
          index = index + 1     local words = {}     while args[index] ~= nil do         words[#words+1] = args[index]         index = index + 1     end
          local message = table.concat(words, ' ')     local command_msg = ''     for i = 1, #names do         command_msg = command_msg..'input /tell '..names[i]..' '..message        if i < #names then             command_msg = command_msg..'; wait 2; '         end     end     windower.send_command(command_msg) end)

       

      Loading/Unloading

      Loading and unloading can be used to setup certain variables that are necessary, but it can also be used to define aliases. In our previous, for instance, typing out //lua c mtell in front of the arguments is pretty annoying in itself. So what we can do, is put this in the "load" event:

       

      windower.register_event('load', function()
          windower.send_command('alias mt lua c mtell')
      end)
      

       

      And similarly, remove that alias again when unloading the addon:

       

      windower.register_event('unload', function()
          windower.send_command('unalias mt')
      end)
      

       

      Now to send our multitell, all we have to do is //mt playerA playerB send sup guys and it would send sup guys to both of them.

      Library functions

      A few of the issues mentioned above can be remedied by helper functions, but some of them are kinda ugly to implement. Additionally, you would not want to implement them again in every addon you use. If you find something helpful that more than one addon can use, feel free to add to certain libraries.

      Libraries are included by require('library_name'). This will first search in ./ then in ../libs/ for a file titled library_name.lua. All the functions and variables defined there will be pushed in the global scope and can be used in your addon.

      Logging library

      Unfortunately, there's no actual debug mode for this, so to debug we need to log as much as possible. Also unfortunately, the add_to_chat(color, msg) function is not very well suited for quick debugging. In addition to being too verbose and requiring a color number, it cannot take multiple arguments, and trying to concatenate something that isn't a string to an output string will result in an error, if not explicitly casted. For that, this library provides a log function to remedy those issues. It automatically converts every argument to a string, concatenates them by a whitespace and outputs the result into the FFXI chatlog:

       

      > require('logger')
      > 
      > log('sample', 5, nil, false)
      sample 5 nil false
      

       

      Another problem is printing tables. Trying to convert them to a string will result in an object ID being printed, which is of extremely little use to anyone. For that, this library adds a print() and vprint() (vertical print) method to the table namespace:

       

      > require('logger')
      > t = {1,2,3}
      > table.print(t)
      {1, 2, 3}
      > t = {a=5, b='c', eff=false}
      > table.vprint(t)
      {
          a=5,
          b='c',
          eff=false
      }
      

       

      Strings

      This library provides a few function defined in the string namespace. This means that when this library is loaded, all strings will provide these functions as object methods.
       

      > require('strings')
      > 
      > str = 'Random string'
      > = str:at(1)
      R
      > = str:at(2)
      a
      > = str:at(3)
      n
      > = str:at(-4)
      r
      > 
      > str = '/a/b/c/d/'
      > t = str:split('/')
      > = table.concat(t, ', ')
      a, b, c, d
      > 
      > = str:slice(2, 5)
      a/b/
      > = str:slice(4)
      b/c/d/
      > = str:slice(-3)
      /d/
      

       


      Tables

      Tables are tricky, because unlike strings, they don't default to the table namespace when trying to access instance methods. Meaning that the table namespace does not automatically link to tables. However, the table helper library introduces a new table notation (called T-tables), denoted by T:

       

      > require('tables')
      > 
      > t_old = {1,2,3,4,5}
      > = table.concat(t_old, '/')
      1/2/3/4/5
      > = t_old:concat('/')
      Error: attempt to call method 'concat' (a nil value)
      > t_new = T{1,2,3,4,5}
      > = table.concat(t_new, '/')
      1/2/3/4/5
      > = t_new:concat('/')
      1/2/3/4/5
      > 
      > t_trans = T(t_old)
      > = t_trans:concat('/')
      1/2/3/4/5
      

       

      All you have to do is define your tables with T{} instead of the regular {}. Existing tables (like the mob array or player array) can be converted to T-tables by using T() on them.

      In addition to that, this library also provides some useful functions that extend the table namespace:

       

      > require('tables')
      > require('logger')
      > 
      > t = T{1,2,3,4,5,6,7,8,9,10}
      > ts = t:slice(3, 7)
      > ts:print()
      {3, 4, 5, 6, 7}
      > 
      > double = function(num)
      >     return 2*num
      > end
      > tm = t:map(double)
      > tm:vprint()
      {
          2,
          4,
          6,
          8,
          10,
          12,
          14,
          16,
          18,
          20
      }
      > 
      > iseven = function(num)
      >     return num%2 == 0
      > end
      > tf = t:filter(iseven)
      > tf:print()
      {2, 4, 6, 8, 10}
      

       

      Maths

      This is nothing special, just a select few functions added to the math namespace. Also allows numbers to be indexed with the : operator, like strings and T-tables.

       

      > require('maths')
      > 
      > = math.round(5.7)
      6
      > = math.round(math.pi, 5)
      3.14159
      > 
      > = math.pi:round() == math.round(math.pi, 5)
      true
      > 
      > require('tables')
      > 
      > t = {12.5, -3.4, -5.004, -1.5, 12.5, 98.5, -12.22}
      > 
      > tsgn = t:map(sgn)
      > t:print()
      {1, -1, -1, -1, 1, 1, -1}
      


      #2 ___

      ___

        Advanced Member

      • Members
      • PipPipPip
      • 40 posts

        Posted 13 January 2015 - 12:35 AM

        Nice guide.  If I may say so without being taken out back and beaten on though, Lua seems unpleasant to me.  Double .. for string concatenation, type happy end syntax, really bad handling of multi line strings... bonuses for not having curly brackets but if that's the cost I'd rather have curly's.  Tables are a unique, interesting and odd data structure that seem like an array library would serve the same purpose.

         

        I also read that for python vs lua that one of the disadvantages of lua is it doesn't have a very robust library compared to python.  With all the downsides I'm having a hard time understanding why Lua has been chosen for so many MMO addons not just here.  Maybe someone could enlighten me?  I know when it gets down to it most coding stuff is opinion and preference and taste though advents will throw things at you to try to convert you to their way of thinking... though it is just that imo... ways of thinking.  In college my professors tried to sell us on OOP as the end all of coding design.  I just didn't like it and doing php/web programming most of the systems I work on don't have OOP at all but rather work off a function/methods based system.  Example: Drupal.  OOP has lofty ideas but in my experience it gets in the way more than it helps... of course that could be very different for other coding tasks and industries. 

         

        Lua *seems* to favor building your own stuff over using libraries.  But in my opinion a well written library tested and versioned over years is going to not only be more bug free and feature rich but run faster and better than one man on the fly solution.  It also eats a lot of time.  I really don't know much about it so that's why I'll end with reiterating a question:

        Why was Lua chosen for so many mmo plugins?  (My brother tells me WoW uses Lua extensively).



        #3 Iryoku

        Iryoku

          Advanced Member

        • Windower Staff
        • 488 posts

          Posted 13 January 2015 - 01:24 AM

          I think everyone on the Windower dev team, myself included, wishes we could have used Python instead of Lua, but Python wasn't an option. One of the biggest problems is that it's very difficult to have multiple interpreters running at the same time, which we needed to keep Addons isolated from each other. Lua is also much lighter weight. The entire lua interpreter, including all built-in libraries is only 170 KB or so. It's also among the fastest interpreted languages in existence which is important for games; plain unmodified vanilla Lua 5.1 is about as fast as V8, one of the fastest JavaScript interpreters available, and LuaJIT, which is a highly optimized interpreter and JIT compiler for Lua 5.1, is significantly faster with performance rivaling compiled languages in many cases.

           

          Are there problems with Lua? Certainly! I've been known to rant for hours at a time about how much I hate the Lua C API, and I'm really not a fan of the fact that variables are global by default, or the fact that indexes start at 1 instead of 0. However, on the whole, for the types of uses it sees in video games, its pros outweigh its cons, and thus it's used quite widely in the industry.



          #4 Arcon

          Arcon

            Advanced Member

          • Windower Staff
          • 1189 posts
          • LocationMunich, Germany

          Posted 13 January 2015 - 06:11 AM

          As Iryoku said it was a decision out of necessity. Of the choices we had, this was the only one that made the most sense for our needs. I agree with pretty much everything you said, and if you read the Lua guide above you'll see some hidden contention for the language showing through between the lines. Don't think there is anyone here who hasn't bitched about some Lua silliness at some point to each other on IRC. Here are some of my major dislikes with the language:

          • Bad grammar. It doesn't allow indexing literals, it does not consider unary plus to be a valid operator for numbers, it doesn't allow expression statements, it doesn't allow Lua keywords to be used as table keys in the shorthand syntax etc.
          • Verbose syntax. All those "then", "do" and "end" tokens require a lot of typing and imo it makes the code less readable, because you have to distinguish between delimiting words and code a lot more than you have in other languages. I much prefer braces myself, or even Python's absence of delimiters due to white-space scoping. Both are both easier to write and easier to read.
          • 1-indexing. Not only does this make some functions uncomfortable to use, it often makes them slightly slower as well, because they have to add/subtract 1 to get the desired result. There are many articles on why 0-indexing makes more sense in the programming field. Also it makes interoperability with C-side functions harder as they need to account for that difference.
          • Default global variables. More an annoyance than a bad design choice, but it's still noticeable and has actually caused some strange bugs for us in the past that were hard to debug, because one of the libraries we wrote (since Lua doesn't ship its own) forgot one local keyword and created a global variable with the same name as one used by an addon. I find this a really strange choice, as you'll normally want most variables to be local, global variables are a rather special case.
          • No default libraries (only a handful of string, table and system functions). Some actually consider that an advantage, because it makes Lua compact. Those people obviously don't realize that it only means that you have to write those libraries yourself and bloat it because of that regardless, only that you have to put more work into it yourself and get slower and worse code in return :)
          • No real "no value" value. Lua's "nil" is a simple but far from ideal solution which is technically not distinct from "no value", although it claims to be and treats it like that for most cases. This causes many problems for things like table iteration, because you can't do that if the table contains nil values, as iterators will abort if they encounter one of those, because a nil value indicates a missing value to it, as such it thinks the table is fully iterated. This also means you can't have keys in tables that contain no value, because that is the same as if that table key didn't exist in the first place (since Lua considers them the same). This has been bugging me quite a bit when designing our libraries and I had to work around it with quite ugly hacks in some places.
          • Not all operators are overload-able. The above issue would be much simpler to deal with if we could overload boolean conversion, for example, but that's a no go. Other boolean operators (and, or, not) are also not overload-able.
          • Operators are generally a mess in Lua. Lack of a lot of common operators, like all the in-place assignment operators for one (from ++ to /=), no bitwise operators and no ternary conditional operator. And why they chose to use ~= for inequality instead of the ubiquitous != is still a mystery to me. Also, the # operator is borderline useless and not guaranteed to be constant time.
          • No concept of table properties. This would avoid many issues of metatable design which are a bit too complex to go into here. Basically as soon as you want to give a data structure a property you're just inserting a key into that table. So that table can't, for example, have a "sort" method and at the same time contain a value with the key "sort", because "methods" are just syntactic sugar for calling a function with that specific key. So you can't effectively define methods on tables.

          Note that this list is not exhaustive and only touches on the Lua side of the implementation. The C side is even worse, but too technical to be of value to this discussion. In short, it's far from ideal. It does, however, also have its good parts, some of which I actually like a lot. The metatable concept, for one, reminds of JavaScript's prototypes, but is easier to handle because the concept is simpler. That allows for a lot of flexibility and you can adapt the language to what you need it to be. To some degree, anyway, unfortunately the issues above are not fixable with it.

           

          But again, as was mentioned before, it was the best choice for us at the time. It probably still is, but even if it wasn't it's too late to change to Python (or anything else) now, because I'm sure as hell not porting all our addons. Not to mention that I won't be the one to break it to all our GearSwap users.






          1 user(s) are reading this topic

          0 members, 1 guests, 0 anonymous users