Optional comment sections in Ada function header - function

When a reader starts to read the function code, he should already have a very good idea of what it is does, how it does it, and what problems he might meet. I'm trying to write clean, structured, well-commented code that is easy to understand. And I'm reading Ada Style Guide and some things I didn't understand well enough, what can i write for optional sections (for exapmle: #Error_Handling, #Pre, #Post).
I want to represent this Function like an example. Using the above guidelines, a standard function header may be derived:
-- ---------------------------------------------------------------------------
-- #Function: Arithmetic_Mean
--
-- #Description:
-- Function to find the mean of a numeric vector. The program should
-- work on a zero-length vector (with an answer of 0.0).
-- #Usage: (opt)
-- #Parameter:
-- +Num: Given array
-- #Return: Averages/Arithmetic mean or zero
-- #Error_Handling: (opt)
-- #Pre: (opt)
-- #Post (opt)
type Mean_Numbers is array (Natural range <>) of Float;
function Arithmetic_Mean (Num : Mean_Numbers) return Float is
Sum : Float := 0.0;
begin
if Num'Length > 0 then
while Num'First <= Num'Last loop
Sum := Sum + Num(Num'First );
end loop;
return Sum / Float (Num'Length);
end if;
return 0.0;
end Arithmetic_Mean;
And here is another example:
-------------------------------------------------------------- ... --
-- #Function: Get_Index
-- #Description:
-- Returns the minimum index of Item in A.
-- #Parameters:
-- +A: the array
-- +Item: element searched for
-- #Return:
-- The minimum index of Item in A.
-- #Pre:
-- true
-- #Post:
-- if exists 1 <= I <= UPPER_BOUND: A(I) = Item then
-- result = min {1 <= k <= UPPER_BOUND | a(j) = item }
-- else
-- result = 0

The #Pre and #Post tags should document your module's approach to Design by Contract. As you observed, any precondition must be true for successful execution, and any postcondition is a promise to be fulfilled by your code. The #Error_Handling tag identifies how you deal with violations of the other two.
As a concrete example, your implementation of Arithmetic_Mean silently ignores an empty input array, returning a mean of zero; it propagates any exceptions that are raised. These are the behaviors that should be documented.
Several benefits accrue:
The API programmer can clearly state the intended behavior.
The API client can distinguish among possible sources of error.
A reviewer can verify whether the intention matches the implementation.
See also Introduction to Ada: Design by contracts, which illustrates Ada 2012 support for enforcing contracts. The Rationale for Ada 2012 offers an overview of the topic and related aspects.

Related

I am a beginner and using pascal. I don't know how to code the output. I am expecting the output to be the result of the function

I am a beginner and using pascal and i am trying this problem that i attach. What should i do next? I am trying to make this program work by showing the "ayam" Function. But i don't know how to code the rest.
function ayam(a, b:integer) :
integer;
var
m: integer;
begin
readln(a) ;
readln(b) ;
readln(m) ;
begin
if (b=0) then
ayam:= 1 else
ayam:= a * ayam(a, b-1) mod m;
end;
writeln(ayam);
end;
The first thing I should say is that when a question is to do with coursework, as is obviously the case here, there is an unwritten convention that we do not write an answer which simply does your programming task for you. So in the following, I have not commented on whether the code inside your ayam function actually correctly implements what is required - that is for you to do.
Secondly, you did not say which Pascal implementation you are using. I have written the code of this answer in Lazarus, the IDE for FreePascal (aka FPC), which is the best of the freeware Pascal implementations currently available.
The first thing to do with your code is to re-format it the way it should be formatted,
using indentation of statements to reflect the structure of the steps you are
trying to code. Basically, your eye should be able to follow the code structure
as easily as possible. This gives something like this:
function ayam(a, b:integer) : integer;
var
m: integer;
begin
readln(a) ;
readln(b) ;
readln(m) ;
begin
if (b=0) then
ayam:= 1
else
ayam:= a * ayam(a, b-1) mod m;
end;
writeln(ayam);
end;
With that change made, it is pretty obvious that there is something wrong with
the logic of your code, because a and b are declared as input variables to
the ayam function but your code actually reads in their values via the
readln statements inside the function. This is what made me think that you think
you have done all that's necessary to write your program. In fact, you have not. What you are missing
is some code which sets up the input parameters a and b (and m, see below) in some way and then
invokes your ayam function to calculate its value and then does something with
the result. At the moment, your code simply defines ayam but takes no steps
to use it. So, to get your ayam function to do anything, you need to write a complete
Pascal program that uses it. Something like this:
program ayamprogram;
{ the following declares some variables for the program to use }
var
varA,
varB,
varM,
varResult : Integer;
{ next, we define the ayam function }
function ayam(a, b, m:integer) : integer;
begin
begin
if (b=0) then
ayam:= 1
else
ayam:= a * ayam(a, b-1, m) mod m;
end;
end;
{ now we write the code of the program, which reads in the values to be
used by ayam, then invokes ("calls") ayam to calculate its value and
assign its value to the varResult variable and then outputs the calculated
result using the writeln statement
}
begin
{ this is the code of the program }
readln(varA);
readln(varB);
readln(varM);
varResult := ayam(varA, varB, varM);
writeln('function ayam evaluates to ', varResult);
readln; { this causes the program to pause so you can see the result of the writeln statement }
end. { end of program }
Note that I have used different variable names for the variables which are supplied to ayam, varA, varB, varM, from the variable names used inside ayam to avoid confusion between them.
Note also that as it is bad practice to read user input inside an executing function, the value to be used for M is read in before ayam is called and is supplied to ayam as a third parameter.
A point you need to consider regarding the expression
ayam:= a * ayam(a, b-1, m) mod m
is whether the mod m should operate on the value of ayam(a, b-1, m) or on the entire expression including the a * as well; if so, parentheses can enforce the evaluation order you need, by writing
ayam:= (a * ayam(a, b-1, m)) mod m

How to check the input? Ada language

I've just started learning Ada and I cannot figure out how to keep the program running when the user input is beyond the declared range of a variable. I'd like to print info about bad range of input and then ask user for input again.
This is my simple code:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Main is
type Score is range 0..100;
a : Score;
begin
Put_Line ("Enter value range 0-100: ");
a := Score'Value(Get_Line);
if a'Valid then
Put_Line ("You entered" & Score'Image (a));
else
Put_Line ("Bad range of input");
end if;
end Main;
Shouldn't I use the "range" in order to check the input, but rather some if's with >, < restrictions?
My other approach was to try this with exceptions, but it also doesn't work as I want it to:
with Ada.Text_IO;
with Ada.IO_Exceptions;
use Ada.Text_IO;
procedure Main is
type Score is range 0..100;
a : Score;
begin
loop
begin
Put_Line ("Enter value range 0-100: ");
a := Score'Value(Get_Line);
Put_Line ("You entered" & Score'Image (a));
exit;
exception
when Ada.IO_Exceptions.Data_Error =>
Put_Line("Bad range of input");
end;
end loop;
end Main;
I believe the problem is in my lack of understanding this language, but I hope there is some kind of easy solution for this, thanks for any help.
Now you know a magical incantation that works, but I doubt you understand why it works, or why your other incantations didn't work. I will go into painful pedagogical detail about that, in hopes that some of it might be useful or of interest.
In Ada, when you declare a type, the type is anonymous, and the name (Score) you give is the name of the first-named subtype. The first-named subtype may have constraints that don't apply to the anonymous base type. For some types, including integer types, it's possible to refer to the anonymous base type with 'Base.
Since you declared Score using range, it is a signed integer type and its base type is (roughly) symmetrical around zero. So your declaration is equivalent to something like
type Score'Base is range -128 .. 127;
subtype Score is Score'Base range 0 .. 100;
(this is not Ada and will not compile).
Score'Value returns a value of Score'Base (ARM 3.5 (53)), so if you input "101" or "-3", Score'Value will succeed and return the appropriate value. When you assign that value to your variable of subtype Score, a check is performed that the value is in the range of Score; when that fails, Constraint_Error is raised. If you input an invalid image, such as "200" or "xyz", Score'Value fails and raises Constraint_Error. So you have two kinds of incorrect input resulting in two different failures, both of which happen to raise the same exception.
Your first version failed because you never got to the if statement. Your second version failed because Ada.Text_IO.Get_Line never raises Data_Error.
When dealing with numeric input, I advise that a complete line be read into a String and you then parse out the value(s) from that String, as you have done. However, 'Value will reject some input that you might want to consider valid. For example, you might want to accept "23 skidoo" and get the value 23 from it. For that, you might want to instantiate Ada.Text_IO.Integer_IO for your numeric (sub)type and use the Get function that takes a String parameter:
package Score_IO is new Ada.Text_IO.Integer_IO (Num => Score);
...
Score_IO.Get (From => "23 skidoo", Item => A, Last => Last);
will set A to 23 and Last to the index of '3' in From (2).
HTH

Initializing ROM from array using functions, Synthesis ERROR (VHDL)

Ok, so I have a problem with a ROM initialization function.
Before I get into the problem let me explain a bit the nature of my problem and my code.
What I want to do is generate N number of ROMs which I have to use as an input for a module that runs a matching algorithm.
My problem is that I have a file with my signatures (let's say 64 in total) which I want to load in my different ROMs depending on how many I've generated (powers of 2 in my case, e.g. 8 roms of 8 signatures each).
I figured the best way to do it is load the whole text file into an array (using a function outside of the architecture body) which I will then use (again in a function) to load the data into a smaller array which will then be my ROM.
It seemed to me that the synthesizer would then just ignore the big array since I don't actually use it in my architecture.
Problem now is that since the second array input arguments are signal dependent, the synthesizer just ignores them and ties my array to zero (line 57).
Does anyone know how else if there's a way to make this architecture synthesize-able?
library ieee;
use ieee.std_logic_1164.all;
use IEEE.std_logic_signed.all;
use std.textio.all;
entity signatures_rom_partial is
generic(
data_width : integer := 160;
cycle_int : integer :=32;
rom_size : integer := 4
);
port ( clk : in std_logic;
reset : in std_logic;
readlne: in integer range 0 to cycle_int-1; -- user input for array data initialization
address: in integer range 0 to rom_size-1; -- address for data read
data: out std_logic_vector(data_width-1 downto 0) -- data output
);
end signatures_rom_partial;
architecture rom_arch of signatures_rom_partial is
type rom_type is array (0 to cycle_int-1) of bit_vector (data_width-1 downto 0); -- big array for all signatures, not used in arch
type test_type is array (0 to rom_size-1) of std_logic_vector (data_width-1 downto 0); -- smaller ROMs used in arch
--Read from file function--
----------------------------------------------------------------------------------------------------------
impure function InitRomFromFile (RomFileName : in string) return rom_type is --
file RomFile : text is in RomFileName; --
variable RomFileLine : line; --
variable rom : rom_type; --
--
begin --
for i in rom_type'range loop --
readline (RomFile, RomFileLine); --
read (RomFileLine, rom(i)); --
end loop; --
return rom; --
end function; --
----------------------------------------------------------------------------------------------------------
--Function for smaller ROM initialization--
----------------------------------------------------------------------------------------------------------
impure function initPartRom (rom : rom_type; readlne : integer) return test_type is --
variable test_array : test_type; --
--
begin --
for j in test_type'range loop --
test_array(j) := to_stdlogicvector(rom(j+readlne)); --
end loop; --
return test_array; --
end function; --
----------------------------------------------------------------------------------------------------------
constant rom : rom_type := InitRomFromFile("signatures_input.txt");
signal test_array : test_type := initPartRom(rom , readlne); --(LINE 57) SYNTHESIZER IGNORES THESE INPUT ARGUMENTS
begin
process(clk,reset)
begin
if reset='1' then
data<=(others=>'0');
elsif (clk'event and clk='1') then
data <= (test_array(address));
end if;
end process;
end rom_arch;
Synthesis is done using Xilinx ISE, simulation is done with Modelsim in which, my creation works fine :)
Thanks for any help!
With Xilinx ISE (14.7) it is possible to synthesize ROMs and RAMs even if the initial data is read from an external file by function.
The only requirement is, that the reading function must be computable at synthesis time. This is not true for your code because readlne is not static at the point the function is called. You should change it to a generic instead of an input and assign a different value to this generic for every other ROM instance. Then it should work as intended.
A sample implementation of how to read initialization data from a text file in (Xilinx) .mem format can be found in VHDL Library PoC in the namespace PoC.mem.ocrom or PoC.mem.ocram

VHDL, using functions in for generate statement

VHDL, using functions in for generate statement
I have a component that should be instantiated about 8000 times, I used for-generate statement with the help of some constant values for reducing amount of code, but I had to declare a function for parametrization of component connections.
My function looks like this:
function dim1_calc (
cmp_index : integer;
prt_index : integer
) return integer is
variable updw : integer := 0;
variable shft_v : integer := 0;
variable result : integer := 0;
begin
if (cmp_index < max_up) then
updw := 1;
else
updw := 2;
end if;
case prt_index is
when 1 =>
shft_v := cnst_rom(updw)(1) + (i-1);
when 2 =>
shft_v := cnst_rom(updw)(2) + (i);
--
--
--
when 32 =>
shft_v := cnst_rom(updw)(32) + (i);
when others =>
shft_v := 0;
end case;
if (updw = 1) then
if (shft_v = min_up & ((prt_index mod 2) = 0)) then
result <= max_up;
elsif (shft_v = max_up & ((prt_index mod 2) = 1)) then
result <= min_up;
elsif (shft_v < max_up) then
result <= shft_v;
else
result <= shft_v - max_up;
end if;
else
--something like first condition statements...
--
--
end if;
return result;
end function;
and part of my code that uses this function plus some related part looks like this:
--these type definitions are in my package
type nx_bits_at is array (natural range <>) of std_logic_vector (bits-1 downto 0);
type mxn_bits_at is array (natural range <>) of nx_bits_at;
--
--
--
component pn_cmpn is
port(
clk : in std_logic;
bn_to_pn : in nx_bits_at(1 to row_wght);
pn_to_bn : out nx_bits_at(1 to row_wght)
);
end component;
--
--
--
signal v2c : mxn_bits_at(1 to bn_num)(1 to col_wght);
signal c2v : mxn_bits_at(1 to pn_num)(1 to row_wght);
--
--
--
gen_pn : for i in (1 to pn_num) generate
ins_pn : pn_cmpn port map (
clk => clk,
bn_to_pn(1) => b2p (dim1_calc(i, 1)) (dim2_calc(i, 1)),
bn_to_pn(2) => b2p (dim1_calc(i, 2)) (dim2_calc(i, 2)),
.
.
.
bn_to_pn(32) => b2p (dim1_calc(i, 32)) (dim2_calc(i, 32)),
pn_to_bn => p2b (i)
);
end generate;
I know that using too many sequential statements together is not appropriate in general, and I'm avoiding them as much as possible, but in this case I assumed that this function won't synthesize into some real hardware, and synthesizer just calculates the output value and will put it in corresponding instantiations of that component. Am I right? or this way of coding leads to extra hardware compared to just 8000 instantiations.
PS1: Initially I used "0 to..." for defining ranges of the 2nd and 3rd dimension of my arrays, but because of confusion that were made in dimension calculation function based on for-generate statement parameter, I replaced them with "1 to...". Is that an OK! coding style or should I avoid it?
PS2: Is there a way that port mapping part in above code combines into something like this:
(I know this is strongly wrong, it's just a clarification of what I want)
gen_pn : for i in (1 to pn_num) generate
ins_pn : pn_cmpn port map (
clk => clk,
gen_bn_to_pn : for j in (1 to 32) generate
bn_to_pn(j) => b2p (dim1_calc(i, j)) (dim2_calc(i, j)),
end generate;
pn_to_bn => p2b (i)
);
end generate;
Let me give another example
Assume that I have a component instantiation like this:
ins_test : test_comp port map (
clk => clk,
test_port(1) => test_sig(2)
test_port(2) => test_sig(3)
test_port(3) => test_sig(4)
);
Is there a way that I can use for generate here? something like:
ins_test : test_comp port map (
clk => clk,
gen_pn : for i in (1 to 3) generate
test_port(i) => test_sig(i+1)
end generate;
);
PS3: Is it possible to call a function inside another function in VHDL?
Functions are usable this way. If you encounter problems, I am sure they will regard details in the design or design tools, rather than the basic approach.
One potential issue is that the function refers to some external "things" such as max_up, i, cnst_rom whose declarations are not part of the function nor parameters to it. This makes it an "impure function" which - because it refers to external state or even modifies it - has restrictions on calling it (because the external state may change, results may depend on order of evaluation etc).
If you can make it pure, do so. I have a feeling that max_up, cnst_rom are constants : if they aren't used elsewhere, declare them local to the function. And i probably ought to be a parameter.
If this is not possible, make the external declarations constants, and preferably wrap them and the function together in a package.
This will just generate the values you need in a small, comprehensible, maintainable form, and not an infinite volume of hardware. I have used a complex nest of functions performing floating point arithmetic then fiddly range reduction and integer rounding to initialise a lookup table, so fundamentally the approach does work.
Potential pitfall:
Some design tools have trouble with perfectly valid VHDL, if its use is slightly unorthodox. Synplicity cannot synthesise some forms of function (which DO generate hardware) though has no trouble with the equivalent procedure returning the result through an OUT parameter!. XST is considerably better.
XST parsing my lookup table init has an absurd slowdown, quadratic in the number of function calls. But only if you are using the old VHDL parser (the default for Spartan-3). Spartan-6 uses the new parser and works fine ( under a second instead of half an hour!) as do Modelsim and Isim. (haven't tried Synplicity on that project)
Some tools object to unorthodox things in port maps : you may get away with function calls there; or you may have to workaround tool bugs by initialising constants with the calls, and using those constants in the port maps.
And your supplementary questions:
PS1) The correct coding style for an array range is ... whatever makes your intent clear.
If you find yourself mentally offsetting by 1 and getting confused or even making errors, STOP! and improve the design.
Some good array indexing styles:
type colour is (red, green, blue);
subtype brightness is natural range 0 to 255;
hue : array (colour) of brightness;
gamma : array (brightness) of brightness;
-- here 0 is a legitimate value
channel : array (1 to 99) of frequency;
PS2) I think you're asking if you can nest generate statements. Yes.
Details may be awkward and difficult, but yes.
PS3) Yes of course! You can even declare functions local to others; eliminating the possibility they will be accidentally called somewhere they make no sense. They (impure functions) can access the execution scope of the outer function (or process), simplifying parameter lists.
Q1 - in this case I assumed that this function won't synthesize into some ...
It depends on which synthesizer you're using. See this relevant question and comments below.
Q2 - PS1: Initially I used "0 to..." for defining ranges of the ...
Surely it's OK. And please allow we to post a suggestion on coding style here. (from this book)
When defining the loop parameter specification, either use a type (or subtype) definition, or use predefined object attributes (e.g., PredefinedObject'range, PredefinedObject'length - 1 downto 0). Avoid using discrete range (e.g., 1 to 4).
This rule makes the code more reusable and flexible for maintenance.
Q3 - PS2: Is there a way that port mapping part in above code combines into ...
I think this is why you asked the 4th question. So refer to the next answer:).
Q4 - Is it possible to call a function inside another function in VHDL?
Though I can't find some official reference to this, the answer is yes.
PS: Coding rules are defined by the synthesizer tools. So the best way to find an answer is to try it yourself.

Define default values for function arguments

In the Lua wiki I found a way to define default values for missing arguments:
function myfunction(a,b,c)
b = b or 7
c = c or 5
print (a,b,c)
end
Is that the only way? The PHP style myfunction (a,b=7,c=5) does not seem to work. Not that the Lua way doesn't work, I am just wondering if this is the only way to do it.
If you want named arguments and default values like PHP or Python, you can call your function with a table constructor:
myfunction{a,b=3,c=2}
(This is seen in many places in Lua, such as the advanced forms of LuaSocket's protocol modules and constructors in IUPLua.)
The function itself could have a signature like this:
function myfunction(t)
setmetatable(t,{__index={b=7, c=5}})
local a, b, c =
t[1] or t.a,
t[2] or t.b,
t[3] or t.c
-- function continues down here...
end
Any values missing from the table of parameters will be taken from the __index table in its metatable (see the documentation on metatables).
Of course, more advanced parameter styles are possible using table constructors and functions- you can write whatever you need. For example, here is a function that constructs a function that takes named-or-positional argument tables from a table defining the parameter names and default values and a function taking a regular argument list.
As a non-language-level feature, such calls can be changed to provide new behaviors and semantics:
Variables could be made to accept more than one name
Positional variables and keyword variables can be interspersed - and defining both can give precedence to either (or cause an error)
Keyword-only positionless variables can be made, as well as nameless position-only ones
The fairly-verbose table construction could be done by parsing a string
The argument list could be used verbatim if the function is called with something other than 1 table
Some useful functions for writing argument translators are unpack (moving to table.unpack in 5.2), setfenv (deprecated in 5.2 with the new _ENV construction), and select (which returns a single value from a given argument list, or the length of the list with '#').
In my opinion there isn't another way. That's just the Lua mentality: no frills, and except for some syntactic sugar, no redundant ways of doing simple things.
Technically, there's b = b == nil and 7 or b (which should be used in the case where false is a valid value as false or 7 evaluates to 7), but that's probably not what you're looking for.
The only way i've found so far that makes any sense is to do something like this:
function new(params)
params = params or {}
options = {
name = "Object name"
}
for k,v in pairs(params) do options[k] = v end
some_var = options.name
end
new({ name = "test" })
new()
If your function expects neither Boolean false nor nil to be passed as parameter values, your suggested approach is fine:
function test1(param)
local default = 10
param = param or default
return param
end
--[[
test1(): [10]
test1(nil): [10]
test1(true): [true]
test1(false): [10]
]]
If your function allows Boolean false, but not nil, to be passed as the parameter value, you can check for the presence of nil, as suggested by Stuart P. Bentley, as long as the default value is not Boolean false:
function test2(param)
local default = 10
param = (param == nil and default) or param
return param
end
--[[
test2(): [10]
test2(nil): [10]
test2(true): [true]
test2(false): [false]
]]
The above approach breaks when the default value is Boolean false:
function test3(param)
local default = false
param = (param == nil and default) or param
return param
end
--[[
test3(): [nil]
test3(nil): [nil]
test3(true): [true]
test3(false): [false]
]]
Interestingly, reversing the order of the conditional checks does allow Boolean false to be the default value, and is nominally more performant:
function test4(param)
local default = false
param = param or (param == nil and default)
return param
end
--[[
test4(): [false]
test4(nil): [false]
test4(true): [true]
test4(false): [false]
]]
This approach works for reasons that seem counter-intuitive until further examination, upon which they are discovered to be kind of clever.
If you want default parameters for functions that do allow nil values to be passed, you'll need to do something even uglier, like using variadic parameters:
function test5(...)
local argN = select('#', ...)
local default = false
local param = default
if argN > 0 then
local args = {...}
param = args[1]
end
return param
end
--[[
test5(): [false]
test5(nil): [nil]
test5(true): [true]
test5(false): [false]
]]
Of course, variadic parameters completely thwart auto-completion and linting of function parameters in functions that use them.
Short answer is that it's simplest and best way . in lua , variables by default equal with nil . this means if we don't pass argument to lua functions ,the argument is exits but is nil and lua programmers uses of this lua attribute for set the default value .
also it's not a way for set default value but you can use following function
this function create a error is you don't pass values to arguments
function myFn(arg1 , arg2)
err = arg1 and arg2
if not err then error("argument") end
-- or
if not arg1 and arg2 then error("msg") end
but it's not a good way and better is don't use of this function
and in diagrams shows optional argument in [,arg]
function args(a1 [,a2])
-- some
end
function args ( a1 [,a2[,a3]])
-- some
end
As always, "Lua gives you the power, you build the mechanisms". The first distinction to make here is that between named parameters and the commonly used parameter list.
The parameter list
Assuming all your args are given in the parameter list as follows, they will all be initialized. At this point, you can't distinguish between "wasn't passed" and "was passed as nil" - both will simply be nil. Your options for setting defaults are:
Using the or operator if you expect a truthy value (not nil or false). Defaulting to something even if false is given might be a feature in this case.
Using an explicit nil check param == nil, used either as if param == nil then param = default end or the typical Lua ternary construct param == nil and default or param.
If you find yourself frequently repeating the patterns from point (2), you might want to declare a function:
function default(value, default_value)
if value == nil then return default_value end
return value
end
(whether to use global or local scope for this function is another issue I won't get into here).
I've included all three ways the following example:
function f(x, y, z, w)
x = x or 1
y = y == nil and 2 or y
if z == nil then z == 3 end
w = default(w, 4
print(x, y, z, w)
end
f()
f(1)
f(1, 2)
f(1, 2, 3)
f(1, 2, 3, 4)
note that this also allows omitting arguments inbetween; trailing nil arguments will also be treated as absent:
f(nil)
f(nil, 2, 3)
f(nil, 2, nil, 4)
f(1, 2, 3, nil)
Varargs
A lesser known feature of Lua is the ability to actually determine how many arguments were passed, including the ability to distinguish between explicitly passed nil arguments and "no argument" through the select function. Let's rewrite our function using this:
function f(...)
local n_args = select("#", ...) -- number of arguments passed
local x, y, z, w = ...
if n_args < 4 then w = 4 end
if n_args < 3 then z = 3 end
if n_args < 2 then y = 2 end
if n_args < 1 then x = 1 end
print(x, y, z, w)
end
f() -- prints "1 2 3 4"
f(nil) -- prints "nil 2 3 4"
f(1, nil) -- prints "1 nil 3 4"
f(1, nil, 3) -- prints "1 nil 3 4"
f(nil, nil, nil, nil) -- prints 4x nil
Caveat: (1) the argument list got dragged into the function, hurting readability (2) this is rather cumbersome to write manually, and should probably be abstracted away, perhaps time using a wrapper function wrap_defaults({1, 2, 3, 4}, f) that supplies the defaults as appropriate. Implementation of this is left up to the reader as an exercise (hint: the straightforward way would first collect the args into a garbage table, then unpack that after setting the defaults).
Table calls
Lua provides syntactic sugar for calling functions with a single table as the only argument: f{...} is equivalent to f({...}). Furthermore, {f(...)} can be used to capture a vararg returned by f (caveat: if f returns nils, the table will have holes in it's list part).
Tables also allow implementing named "arguments" as table fields: Tables allow mixing a list and a hash part, making f{1, named_arg = 2} perfectly valid Lua.
In terms of limitations, the advantage of table call is that it only leaves a single argument - the table - on the stack rather than multiple arguments. For recursive functions, this allows hitting the stack overflow later. Since PUC Lua drastically increased the stack limit to ~1M this isn't much of an issue anymore; LuaJIT still has a stack limit of ~65k however, and PUC Lua 5.1 is even lower at around 15k.
In terms of performance & memory consumption, the table call is obviously worse: It requires Lua to build a garbage table, which will then waste memory until the GC gets rid of it. Garbage parameter tables should therefore probably not be used in hotspots where plenty of calls happen. Indexing a hashmap is also obviously slower than getting values straight off the stack.
That said, let's examine the ways to implement defaults for tables:
Unpacking / Destructuring
unpack (table.unpack in later versions (5.2+)) can be used to convert a table into a vararg, which can be treated like a parameter list; note however that in Lua the list part can't have trailing nil values, not allowing you to distinguish "no value" and nil. Unpacking / destructuring to locals also helps performance since it gets rid of repeated table indexing.
function f(params)
local x, y, z, w = unpack(params)
-- use same code as if x, y, z, w were regular params
end
f{1, 2, nil}
if you use named fields, you'll have to explicitly destructure those:
function f(params)
local x, y, z, w = params.x, params.y, params.z, params.w
-- use same code as if x, y, z, w were regular params
end
f{x = 1, w = 4}
mix & match is possible:
function f(params)
local x, y, z = unpack(params)
local w = params.w
-- use same code as if x, y, z, w were regular params
end
f{1, 2, w = 4}
Metatables
The __index metatable field can be used to set a table which is indexed with name if params.name is nil, providing defaults for nil values. One major drawback of setting a metatable on a passed table is that the passed table's metatable will be lost, perhaps leading to unexpected behavior on the caller's end. You could use getmetatable and setmetatable to restore the metatable after you're done operating with the params, but that would be rather dirty, hence I would recommend against it.
Bad
function f(params)
setmetatable(params, {__index = {x = 1, y = 2, z = 3, w = 4}})
-- use params.[xyzw], possibly unpacking / destructuring
end
f{x = 1}
in addition to the presumably garbage params table, this will create (1) a garbage metatable and (2) a garbage default table every time the function is called. This is pretty bad. Since the metatable is constant, simply drag it out of the function, making it an upvalue:
Okay
local defaults_metatable = {__index = {x = 1, y = 2, z = 3, w = 4}}
function f(params)
setmetatable(params, defaults_metatable)
-- use params.[xyzw], possibly unpacking / destructuring
end
Avoiding metatables
If you want a default table without the hackyness of metatables, consider once again writing yourself a helper function to complete a table with default values:
local function complete(params, defaults)
for param, default in pairs(defaults) do
if params[param] == nil then
params[param] = default
end
end
end
this will change the params table, properly setting the defaults; use as params = complete(params, defaults). Again, remember to drag the defaults table out of the function.