Log In  

A question came up yesterday in the pico-8 discord: are there any nice ways of doing default arguments? For example,

function foo(a,b,c)
  a=a or default_for_a
  b=b or default_for_b
  c=c or default_for_c
  -- do stuff
end

That's five tokens spent per argument; can we do better?

yes

My first thought was doing something like this:

function new_actor(fields)
 return merge(parse"x=64,y=64,w=8,h=8",fields)
end

This is a common pattern I use in my games; merge(A,B) is a custom thing I have that merges tables A and B together by creating a new table, copying all of A's contents into it, and then copying all of B's contents into it. Note that if A and B share any keys, the value from table B will be used for the result, which makes this handy for setting defaults in a table. (and parse is another custom thing that does what you'd expect, turning a string into a table)

However, this wasn't quite what the questioner was asking for; I can set default values for table members with this method, but I can't set local variables. This method wouldn't work to save tokens if you had to say locals.myvar throughout your function instead of just myvar!

Time for some brain thinking... hmm, I recently found out that lua's _ENV table is accessible through pico-8 (although you have to spell ENV with pico-8's puny text mode, funnily enough). This is a bit of a special table that all variables are implicitly accessed through, so typing tonum(myvar) actually looks up the values of tonum and myvar in the _ENV table. To give a concrete example, you can get/set the value of a variable A by using _ENV.A (or _ENV["A"]) instead

One stroke of madness inspiration later, I came up with this positively arcane incantation:
(this is a working pico-8 cart; paste it into an empty cart to see it in action!)

-- defaultargs, by pancelor
env_backup=_𝘦𝘯𝘷
function defaultargs(str,args)
 local locals=setmetatable({},{__index=env_backup})
 for i,str2 in ipairs(split(str)) do
  local parts=split(str2,"=")
  assert(#parts==2)
  local k,v=unpack(parts)
  locals[k]=args[i] or tonum(v)
 end
 return locals
end

function myrect(...)
 local _𝘦𝘯𝘷=defaultargs(
 "x=64,y=64,w=32,h=32",{...})
 rectfill(x,y,x+w-1,y+h-1,5)
end

function _init()
 poke(0x5f2d,1) --enable mouse
end
function _draw()
 cls(0)
 local mousex,mousey=stat(32),stat(33)
 myrect(mousex,mousey)
 assert(x==nil) -- test to make sure the function's local args didn't leak out into global scope
end

It's certainly Something but hey it gets the job done! And it does it with ~60 tokens for defaultargs plus 6 tokens per function that uses it (which very quickly becomes better than the 5~6 tokens needed per argument in the 'a=a or default' way)

So... how does it work? Well, calling 'myrect(mousex,mousey)' ends up calling 'defaultargs(
"x=64,y=64,w=32,h=32",{mousex,mousey})'. The defaultargs function creates a new table called locals that myrect will later set to it's _ENV. So, if we can somehow make locals equal to {x=mousex,y=mousey,w=32,h32}, then when myrect sets it to _ENV, the variables x, y, w, and h will be available in the scope of myrect. However, we also want to have other global variables in scope too, like the rectfill function, so we do some metatable/__index shenanigans to make that work.

Understanding that is the most complicated bit; the rest of defaultargs does the natural thing that sets locals to have the members we want inside of it. (if 'unpack' confuses you, just pretend that line says 'local k,v=parts[1],parts[2]' instead; it does the same thing)

This one code snippet uses 'unpack', '...', metatables and fallback indexing, required puny text, and lua's _ENV table... wow. I'm not exactly sure why making a backup reference to _ENV is necessary; I assume it's something to do with how _ENV is a bit special in lua?

Time for a nap.

Happy token saving!

-pancelor

P#85041 2020-12-04 21:27

some ideas for modifications you could make to this:

  • save some obvious tokens and just write local k,v=unpack(split(str2,"="))
  • pass in a 3rd optional argument to defaultargs that is used to parse 'v'. If no custom parser is passed, default to using tonum
P#85063 2020-12-04 21:30

[Please log in to post a comment]