Proposal: Function Definition Named/Default Argument Syntax Sugar

classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

Proposal: Function Definition Named/Default Argument Syntax Sugar

Andrew Starks
I like named arguments much more so than positional ones. In fact, I
hate positional arguments and refuse to use them. Therefore, all of my
functions look like this:

local myfunc = function(args)
  local name = args.name or nil
  local age = args.age or error("You have to pass me an age, dimwit.")

--...
end

This is relatively okay, but I tend to think of arguments that I pass
in as "special" and in need of being explicitly "syntax-ed." Having a
single argument called "args" requires me to take up the "this was
what I was thinking" slack in comments or when I assign default
values, which I may not actually need to do, but now kind-of-need-to
because there is no other code-way to say that I expect an argument to
be there.

I've thought some about how one might introduce some syntax sugar and
I now more fully-appreciate that finding a good way to accomplish this
goal has some potential ugliness.

At a minimum, it would be super-fancy if I could type something like this:

local myfunc = function{name, --optional or the name of the person
    age, --should be your age, in years, as a whole number
}

local args = ...
  --etc.
end

This has some coolness. I still need to do my default assignments and
error checking, but that's normal. I can at least see the arguments
that I expect and have a place to comment them. The reality is that I
imagine that lua could pretty much interpret this code as:

local myfunc = function(...)
local args = ...
  --...
end

.. and you know what? That's okay with me. It makes things clearer
_for future me_, even as it does nothing within the language.

So we could end my proposal there with a nice, simple, probably
(hopefully) low impact suggestion, which would help me to make things
clearer and not break anything for anyone else.

That said... one could imagine:

local myfunc = function{name = nil, --optional or the name of the person
  age = error("You have to pass me an age, dimwit.") --should be your
age, in years, as a whole number
}
  --...
end

This is cool for simple "this or that" assignments, which advances
things without tarting the language up, too much.

One can see how we could descend into hell with this approach by
asking how I might do error checking or other complicated maneuvers?

local myfunc = function{name = type(??) == "string" --etc...


That is, since the table isn't real yet, so much of what you do isn't
available until later. One approach to this would lead to all manner
of possible confusion (which I just got done arguing against!):

local myfunc = function(args = {name = type(args.name) == "string" and
args.name or args.name ~= nil and error("Has to be nothing or a
string") or nil, --should be a string
  age = args.age or error("You have to pass me an age, dimwit.")
--should be your age, in years, as a whole number
})
  --...
end


Realizing, of course, that "args" does not yet exist and so would run
afoul of Lua's scoping rules. We'd have to pretend that the scope
within a function's arguments parens was "other", which never ends
well.

Or, perhaps, it's as though you may take the assignment of defaults
within function arguments all of the way through by saying that these
rules always apply, even when table arguments are not used, such that:

function(local name = type(name) == "string" and name or name ~= nil
and error("Must be string or nothing") or nil, local age = age or
error("Need an age."))
--etc...
end

This is like saying that when you assign defaults within function
args, it's like you're short handing the subsequent:

local name = type(name) --etc.


I think I hate that solution, but I'm not sure. For now, how about try 3:


local myfunc = function{name = type((...).name) == "string" and
(...).name ) or (...).name  == nil and (...).name or error("Has to be
nothing or a string"), --should be a string
  age =(...).age or error("You have to pass me an age, dimwit.")
--should be your age, in years, as a whole number
}
  args = ...
end

Where '...' was permissible as the reference for the default value in
cases where a single table was the only accessible argument within the
function. If you wanted to get hyper-ugly, you could allow
"(...)[1].name" and still allow varargs. This translates into:

local myfunc = function(...)
 args= ...
args.name = type(args.name) == "string" and args.name or args.name ~=
nil and error("Has to be nothing or a string") or nil
args.age = args.age or error("You have to pass me an age, dimwit.")
--should be your age, in years, as a whole number

end

That seems to not break much...

I'm aware that this has probably been hashed outm ad naseum.  I
searched the archives for conversations related to this idea and while
I know that they are there, the closest that I could find were
comments related to "my_func{name="someone", age=32}", which I
understand and depend upon.

I'm just looking for a way to express what I expect within a table
argument and to do so in the same spirit as the sugar that we get with
a function call that has a single table as the argument. Of course, I
only propose this if it contributes to Lua's simplicity.

Best Regards,

Andrew Starks

Reply | Threaded
Open this post in threaded view
|

Re: Proposal: Function Definition Named/Default Argument Syntax Sugar

Dirk Laurie-2
2012/6/13 Andrew Starks <[hidden email]>:
> I like named arguments much more so than positional ones. In fact, I
> hate positional arguments and refuse to use them.

It depends on the number of arguments.

One argument: daft to make a whole table just for it.
Ten arguments: daft to depend only on position.

> Therefore, all of my functions look like this:
>
> local myfunc = function(args)
>  local name = args.name or nil
>  local age = args.age or error("You have to pass me an age, dimwit.")
>

There is also the possibility of a mixture.  For example, only today
I wrote a function that can be called this way:

   subst { expr, x=1, y=2, z=3 }

The great thing about the fact that this is legal in Lua is that the
number and the names of the extra parameters may vary.  It's way more
powerful than the named parameters of Python etc, in which the names
have to be predictable.

> I'm just looking for a way to express what I expect within a table
> argument and to do so in the same spirit as the sugar that we get with
> a function call that has a single table as the argument. Of course, I
> only propose this if it contributes to Lua's simplicity.

Having sugar in the same spirit sounds very attractive, especially
in the southern hemisphere where it is very cold right now :-)

But seriously, the simplicity of Lua would be well served by this
aspect of your proposal: the syntax of a valid call sequence should
be valid definition syntax too.  How about the following:

   1. Defining a function with {...} instead of (...) means that it takes
a single table-valued argument, with the specified fields initialized.
   2. The implicit name for that argument should clearly be `self`.
   3. Which combines nicely with object-oriented programming.

Example:

   function fct {expr, x=1, y=2, z=3}
      ...
   end

has the same semantics as (_proto being invisible to the user)

   function fct(self)
      local _proto = {expr, x=1, y=2, z=3}
      for k,v in pairs(_proto) do self[k] = self[k] or v end
      ...
   end

At a later call, `fct(tbl)` would ensure that tbl[1], tbl.x, tbl.y and
tbl.z are defined on return.  And if `fct` is assigned to `obj.init`, then
merely `obj:init()` would initialize those fields.

Reply | Threaded
Open this post in threaded view
|

Re: Proposal: Function Definition Named/Default Argument Syntax Sugar

Patrick Rapin
With a metatable and some helper functions, you can have almost what you need :

local function required(msg)
  return function(value) return value or error(msg, 2) end
end
local function check_type(...)
  local types = {...}
  return function(value)
    local t = type(value)
    for i=1,#types do
      if t == types[i] then return value end
    end
    error("Expected value of type "..table.concat(types, " or ")..", got "..t)
  end
end
function get_args(args, canvas)
  return setmetatable({}, {__index=function(t, k)
    local v = rawget(canvas, k)
    if type(v) == 'function' then return v(args[k]) end
    return args[v] or v
  end})
end


Function get_args must be called at the beginning of all named
parameters functions.
Local functions required and check_type are just example of validating
functions.
With that above code, you can then write something like this:

function person(args)
  args = get_args(args, {
    name = required "Please provide a name", -- your name
    age = check_type("number"), -- your age
    address = "(no street address)", -- your street address
   })

   print(args.name, args.age, args.address)
end

person{name = "John Doo", age=56}
--> John Doo 56  (no street address)
person{}
-- Error: Please provide a name

Reply | Threaded
Open this post in threaded view
|

Re: Proposal: Function Definition Named/Default Argument Syntax Sugar

Andrew Starks
In reply to this post by Dirk Laurie-2
On Wed, Jun 13, 2012 at 12:39 PM, Dirk Laurie <[hidden email]> wrote:
<snip>

>   1. Defining a function with {...} instead of (...) means that it takes
> a single table-valued argument, with the specified fields initialized.
>   2. The implicit name for that argument should clearly be `self`.
>   3. Which combines nicely with object-oriented programming.
>
> Example:
>
>   function fct {expr, x=1, y=2, z=3}
>      ...
>   end
>
> has the same semantics as (_proto being invisible to the user)
>
>   function fct(self)
>      local _proto = {expr, x=1, y=2, z=3}
>      for k,v in pairs(_proto) do self[k] = self[k] or v end
>      ...
>   end
>
> At a later call, `fct(tbl)` would ensure that tbl[1], tbl.x, tbl.y and
> tbl.z are defined on return.  And if `fct` is assigned to `obj.init`, then
> merely `obj:init()` would initialize those fields.
>


At first I thought "of course the self is the answer!", but now I fear
I'm missing something....

local my_class = {}
my_class.properties = {x = 100, y  = 200}

function my_class:my_method{expr, x=1, y=2, z=3}
  --...
end

How does that code work? I either trampled on x and y or do I have
self.self? Or is it the case that I can't use colon notation and
named/default arguments table?

As much as I must admit that 'self' is far better looking and more
readable, think that '...' might be more pragmatic and actually,
perhaps in the way that less change is better, more correct . To use
your semantic example:

expr = "a - b" --otherwise it's nil and doesn't show a default, which
is fine, of course.

function fct(...)
    local _proto = {expr, x=1, y=2, z=3}
    for k,v in pairs(_proto) do
                (...)[k] = (...)[k] or v
        end
  return (...)
  end

local a = fct{"b + d", y=4,z=5}

print(a[1], a.x, a.y, a.z)

--> b + d 1 4 5

so that your proposed syntax would be equivalent:

function fct{expr, x=1, y=2, z=3}
    return (...)
end

However, one other thing needs addressing, as I see it:

function fct{expr, [x]=1, [y] = 2, [z] = 3}
   return(...)
end

What is x, y and z? What if they're nothing, right now? What if they
aren't nothing, right now? If the user doesn't assign "x" and "x" is
not assigned when the function definition is created, then does that
throw an error? I don't think that the question is a show-stopper, but
it needs to be dealt with. Since you don't know, and can't know, what
the object keys will be ahead of time, they make assigning default
values to them.... impossible?

I think that the "(...).key" notation in my initial example is so
ugly, that perhaps your definition is best when there are no (). That
is to say, with no () and only a {} after the function name,
"(...)[key] or " is inserted before every assignment. I think this
small addition seems reasonable to me and not too confusing.

On its own, I like this idea and think that it would be a big advantage.

That said, I also like the idea of being able to express the same
default value assignments that are usually done immediately after the
parens. Without your improvement, you would need the "(...)[key]"
method, if no () are allowed:

local function fct{(...).expr or expr, x = type((...).x) == "number"
and (...).x or (...).x == nil and 1 or error("Das swinehund!") ,
    y= (...).y or 2, z= (...).z or 3}

    return (...)
end

This is clearly too ugly to allow, so either you apply your idea,
another "self"-like keyword (expensive in terms of change) or do away
with the idea that you don't need ().

Example

function fct(arg = {arg[1] or expr, x = type(arg.x) == "number" and
arg.x or arg.x == nil and 1 or error("Das swinehund!"),
    y= arg.y or 2, z= arg.z or 3})

    return (arg)
end

This has the benefit of being consistent, not only with a named
argument tables, but also with ordered arguments, as well:

function fct(my_value = my_value or 1, a = a or  {"my first default
value", some_object})

or:

function fct(my_value = my_value or 1, a = {a[1] or error("can't
forget me!"), a[2] or big_table})

or the crazy:

function fct(a = a or 1, b = b and b + a or 3) end

... given that the above would be equivalent to:

function fct(...)
    local a,b = ...

    a = a or 1
    b = b and b + 1 or 3
end

I'm not sure if this disambiguates the question of object keys:

function fct( args = {args[1] or expr, [args[x] or "x"]=args[x] and
args[x] or 1, [args[y] or "y"] = args[y] and 2 or nil, [args[z] or z]
= args[z]  or 3}
   return(...)
end

Perhaps the best way to think of this is to ask, "How is this dealt
with now?" It isn't, without a loop, so I guess it's not really an
important question...


So, combining your innovation and default arguments within the
function definition, the language behavior might be stated:

"Inside a function's argument definition, the user's arguments are
processed by order of position, where each argument is assigned the
variable name that is in the same position within the function
definition's argument parens. Extra arguments are assigned to the ...
keyword. The value from the caller's environment is then applied to
these new, function-scoped variables.

If  assignments (optional) have been made within the function
definition's argument parens, then they are applied immediately after
the calling environment's values have been applied, again within the
function's scope, just as though the assignments immediately followed
the closing of the argument parens.

When no parens are included and only a single, anonymous table is
used, then "(...)[key] or " is prepended to any assignment, where
"key" is the function definition's index, such that the assignment is
not treated as a static over ride, but instead as a default value, to
be used only when no value was provided by the caller."

I believe that this is the way it works to day (including the
object:method{myvar=default_value} case), with two additional semantic
conveniences, which I believe would add clarity to the code.



Best Regards,

Andrew Starks

Reply | Threaded
Open this post in threaded view
|

Re: Proposal: Function Definition Named/Default Argument Syntax Sugar

Andrew Starks
In reply to this post by Patrick Rapin
On Wed, Jun 13, 2012 at 2:11 PM, Patrick Rapin <[hidden email]> wrote:
> With a metatable and some helper functions, you can have almost what you need :
<snip>

> function person(args)
>  args = get_args(args, {
>    name = required "Please provide a name", -- your name
>    age = check_type("number"), -- your age
>    address = "(no street address)", -- your street address
>   })
>
>   print(args.name, args.age, args.address)
> end
>
> person{name = "John Doo", age=56}
> --> John Doo 56  (no street address)
> person{}
> -- Error: Please provide a name
>


Hmmm.. Automating required arguments is neat and useful, but I see it
as different than what I see as perhaps two different language
changes/enhancements/(painted toenails):

Providing named argument syntax for defining function arguments,
similar to the named argument syntax used to call functions

Providing a way to place default argument assignments within the
argument definition, as an alternative to re-assigning them as a
second step within the function.


--Andrew

Reply | Threaded
Open this post in threaded view
|

Re: Proposal: Function Definition Named/Default Argument Syntax Sugar

Chris Emerson
In reply to this post by Dirk Laurie-2
On Wed, Jun 13, 2012 at 07:39:40PM +0200, Dirk Laurie wrote:
> There is also the possibility of a mixture.  For example, only today
> I wrote a function that can be called this way:
>
>    subst { expr, x=1, y=2, z=3 }
>
> The great thing about the fact that this is legal in Lua is that the
> number and the names of the extra parameters may vary.  It's way more
> powerful than the named parameters of Python etc, in which the names
> have to be predictable.

A bit off-topic, but you can do extra named parameters in Python too with
**kwargs.

Regards,

Chris