Jump to content

Associative arrays for named arguments


btherl

Recommended Posts

I often write my functions like this:

[code]function foo($args) {
  if (array_key_exists('dbh', $args)) $dbh = $args['dbh'];
  if (array_key_exists('result', $args)) $result = &$args['result']; # pass by reference
}[/code]

Does anyone else do this?  It's great for optional arguments.  But it's a little annoying having all that code at the top of every function just to process the arguments.  Then again, any decent function will be validating its arguments anyway.

But the benefits when you call such a function are clear, especially when it has many optional arguments.  No more filling in dummy arguments and trying to remember what order they should be in.
Link to comment
Share on other sites

  • 3 years later...

Yes!  I do do this, but I don't know if it is a good practice, either.  I just started it for a few of my projects, and it seems to be working well, and I love the flexibility and the opportunity to make changes later without re-writing all of your calls to that function, but I don't know if there is something I am missing?  I'd like to know what other people think.

Link to comment
Share on other sites

Having used it for a few years now (that original post was from 2006), I think it's great :)  The same idea is very popular in perl too, and perl's syntax is even nicer for it.

 

Thanks for pointing out that other thread.

 

What I do these days is I start functions with simple arguments, and then I switch them to named arguments as needed.  Functions that benefit from named arguments typically aren't called in many places, so this works well.  If there's a function that IS called from many other places, then I can always add a named argument wrapper for it rather than changing all the places where it's called from.  I had to do that once only (and actually that was in Perl, but the same principle applies).

Link to comment
Share on other sites

Really?  This is pretty standard in perl.  Not just because it allows painless addition of new arguments, but because it allows painless passing of just a few from a large selection of possible arguments.  DateTime for example.  Can you imagine doing that with traditional PHP style arguments?

 

As for the idea of designing the system in its totality before starting the coding, that's just not possible in the business I work.  We build the application to spec and then our boss says "Why is it doing X?  It should be doing Y instead".  So we modify it.  Then he says "It shouldn't be doing Y, it should be doing Z".  It's a constant cycle of modification that continues until the end of the application's life.

 

We can't blame our boss either, as a lot of these change requests come from customers wanting features Y and Z, not because he gave us the wrong spec.

Link to comment
Share on other sites

I can't see the benefits of this, all I see is a hindrance. The positives, as you mentioned already, is the emulation of named arguments, but here are some of the negatives to consider...

 

  • Introduces a non-standard coding practise which will increase the learning curve associated with your code, as well as annoy a reasonably number of people.
  • Emulates something that has been excluded from PHP for a reason (it doesn't follow KISS).
  • Makes it more cumbersome to set default values for parameters.
  • Required parameters have to be enforced by the developer instead of by the Zend engine, which is just further overhead in terms of both performance and coding.
  • Unfortunately, PHP doesn't support a shorthand array syntax like ['value1', 'value2'], so having to use array() for every arguments decreases readability.
  • Doesn't allow for type-hinting of objects and arrays, instead, the developer is required to implement this.

 

That's by no means a complete list either. I only ever use arrays for function arguments when I have a large number of optional parameters, where...

 

testFunc('value1', null, null, null, null, null, true, null, false);

 

...is just not practical. I always dislike using them even in such scenarios, as it forces me to do a whole lot more in-code checks.

Link to comment
Share on other sites

I only ever use arrays for function arguments when I have a large number of optional parameters, where...

 

So do I.  As I mentioned earlier, I start with simple function arguments, and convert to named arguments as needed.

 

What's the documentation issue?  Both functions with and without named arguments require documentation.  If you were relying on the argument list itself for documentation, then you've already got documentation issues!

 

Now, on to those criticisms:

 

  • Introduces a non-standard coding practise which will increase the learning curve associated with your code, as well as annoy a reasonably number of people. Yes, this a disadvantage
  • Emulates something that has been excluded from PHP for a reason (it doesn't follow KISS). In the right situation, it does simplify things.  The right situation is functions with large number of optional arguments, where the "simple" argument passing method becomes cumbersome
  • Makes it more cumbersome to set default values for parameters. I believe it is simpler to set default values with named argument style in the right situation.  Traditional argument passing can be problematic here, as all default values must be to the right of the last required value, giving an artificial constraint on argument ordering.  Not to mention that the developer must handle dummy argument values, and the caller must pass the correct dummy value.
  • Required parameters have to be enforced by the developer instead of by the Zend engine, which is just further overhead in terms of both performance and coding. Not an issue in the right situation.  A function with many optional arguments will rarely have its performance dominated by argument parsing time. If it does, you can simply use traditional arguments and put up with the inconvenience.
  • Unfortunately, PHP doesn't support a shorthand array syntax like ['value1', 'value2'], so having to use array() for every arguments decreases readability. Yes, this is a disadvantage.  An unfortunate design decision for PHP, I would say :)  Perl got this right.
  • Doesn't allow for type-hinting of objects and arrays, instead, the developer is required to implement this. I'm not sure what you mean here

 

My claim: Named arguments are superior to traditional argument passing for some functions, in particular those with more than one or two optional arguments.

Link to comment
Share on other sites

Doesn't allow for type-hinting of objects and arrays, instead, the developer is required to implement this. I'm not sure what you mean here

 

In php you can define function arguments to be of a certain type. You'll generate exceptions if argument then passed to these functions aren't the correct type. eg;

 

function foo(array $arg) {}

Link to comment
Share on other sites

Doesn't allow for type-hinting of objects and arrays, instead, the developer is required to implement this. I'm not sure what you mean here

 

In php you can define function arguments to be of a certain type. You'll generate exceptions if argument then passed to these functions aren't the correct type. eg;

 

function foo(array $arg) {}

 

To be clear this only tests for object types, not for all data types. This works for arrays because well.. arrays are objects of type array. But if you try to do something like:

 

function func(int $arg) { ... }

it won't work as you might expect because it's looking for an object of type int, and not the data-type int.

Link to comment
Share on other sites

Doesn't allow for type-hinting of objects and arrays, instead, the developer is required to implement this. I'm not sure what you mean here

 

In php you can define function arguments to be of a certain type. You'll generate exceptions if argument then passed to these functions aren't the correct type. eg;

 

function foo(array $arg) {}

 

public function __construct(array $args)
{
foreach($args as $key => $value)
{
	if(!method_exists($this, '_set' . $key))
	{
		throw new InvalidArgumentException("Unkown attribute '$key'");
	}

	$this->{'set' . $key}($value);
}
}

private function _setBar(Bar $value)
{
$this->_bar = $value;
}

 

As long as you can trust yourself not to set state directly in the constructor.. Could mean more boilerplate code though..

Link to comment
Share on other sites

So do I.  As I mentioned earlier, I start with simple function arguments, and convert to named arguments as needed.

 

I was under the impression you used it regularly for a majority of functions.

 

What's the documentation issue?  Both functions with and without named arguments require documentation.  If you were relying on the argument list itself for documentation, then you've already got documentation issues!

 

Well, there's two reason I can think of. The first is that documentation becomes an absolute requirement as it's not obvious without going through the actual code, which parameters are available to us. In addition to that, the second issue is that PHPDoc doesn't support this practice, hence you have to dump the list of options in the description portion of the PHPDoc block. As a result of all of this, code hinting in IDE's also becomes less effective.

 

I believe it is simpler to set default values with named argument style in the right situation.  Traditional argument passing can be problematic here, as all default values must be to the right of the last required value, giving an artificial constraint on argument ordering.  Not to mention that the developer must handle dummy argument values, and the caller must pass the correct dummy value.

 

In the right situation, you sometimes don't have a choice, but I always consider all available options before resorting to arrays for optional named arguments. Handling dummy values is less of a problem with traditional arguments, than with arrays. If I'm using your framework, and only want to set one of possibly 10 more available options, then you, in your code, will have to set a default value on the 10 options I didn't set. I usually use NULL as the default value for optional arguments (setting an optional argument to NULL in PHP, is essentially the same as skipping it). I can then easily use isset() to determine whether the argument has been set or not, and if not, then set a default value. With boolean arguments however, I will usually set a default value in the function declaration just to make it a little clearer.

 

Not an issue in the right situation.  A function with many optional arguments will rarely have its performance dominated by argument parsing time. If it does, you can simply use traditional arguments and put up with the inconvenience.

 

Performance isn't the main issue here, but rather the coding overhead. Plus, it's always best to use as many of the native language and parser features whenever you can.

 

In summary, yes, there are situations where it makes sense, but it shouldn't be something you implement just so you can have named arguments in PHP. I normally use arrays for optional arguments when my argument list is already 3 or more arguments long - in other words, when readability is affected.

Link to comment
Share on other sites

I was under the impression you used it regularly for a majority of functions.

 

No worries, I kinda realized that after writing the post.  I agree it's silly to use it on most functions.  A lot of code for no benefit.

 

Well, there's two reason I can think of. The first is that documentation becomes an absolute requirement as it's not obvious without going through the actual code, which parameters are available to us. In addition to that, the second issue is that PHPDoc doesn't support this practice, hence you have to dump the list of options in the description portion of the PHPDoc block. As a result of all of this, code hinting in IDE's also becomes less effective.

 

That's a good point.  My workplace has never used automated documentation generation methods, so it's not something I'd considered.  I also don't use an IDE :)  Unless you consider vim with all the code writing add-ons an IDE.

 

In the right situation, you sometimes don't have a choice, but I always consider all available options before resorting to arrays for optional named arguments. Handling dummy values is less of a problem with traditional arguments, than with arrays. If I'm using your framework, and only want to set one of possibly 10 more available options, then you, in your code, will have to set a default value on the 10 options I didn't set. I usually use NULL as the default value for optional arguments (setting an optional argument to NULL in PHP, is essentially the same as skipping it). I can then easily use isset() to determine whether the argument has been set or not, and if not, then set a default value. With boolean arguments however, I will usually set a default value in the function declaration just to make it a little clearer.

 

Do you mean you have to set default values in the called function for unused arguments?  I'm not sure what you mean there.  Here is how I see it, where named arguments mean less mess when calling with only some optional arguments set (though more code in the called function):

 

function annoying($userid, $casefold = false, $include_admins = false, $start = 0, $limit = null, $generate_paging_data = false, $glob_globules = false)
{
  # Here goes type checks, range checks, etc etc
...
}

annoying(123, null, null, null, null, null, true); # We only want to glob the globules.  Defaults for other arguments.

function nice($args)
{
  $userid = $args['userid'];
  $limit = $args['limit']; # Allow null as default value
  ...
  $glob_globules = isset($args['casefold']) ? true : false; # Convert to boolean.  Or we can type check our argument with is_bool() and take action if it isn't

  # Here goes any further range and type checks not done above.
}

nice(array(
  userid => 123,
  glob_globules => true,
));
}

 

Calling nice() looks better than calling annoying() to me, even with the funny looking array() syntax.

 

On the last point, I agree the extra coding is annoying.  You can't get around that in perl either, all you've got is slightly nicer calling syntax.

Link to comment
Share on other sites

Agreed, to an extend. Something like a Service Layer may take a lot of arguments, but arguably a specialized configuration object is in order then.

 

<?php
abstract class Options 
{
public function __contruct(array $args)
{
	foreach($args as $key => $value)
	{
		if(!method_exists($this, 'set' . $key))
		{
			throw new InvalidArgumentException("Unkown attribute '$key'");
		}

		$this->{'_set' . $key}($value);
	}
}
}

class NiceOptions extends Options
{
private $_bar;

protected function _setBar(Bar $value)
{
	$this->_bar = $value;
}

public function getBar()
{
	return $this->_bar;
}
}

class Service 
{
public function nice(NiceOptions $config)
{
	//Do stuff
}
}

Link to comment
Share on other sites

Agreed, to an extend. Something like a Service Layer may take a lot of arguments, but arguably a specialized configuration object is in order then.

 

Yeah, but I think this topic is based around simple functions. Which obviously, should be just that, simple functions. If your functions really need to operate on that much data that they require more than a handful of arguments they should likely be object os some type instead.

Link to comment
Share on other sites

Agreed. It would be cool if PHP supported named arguments as in Python and Smalltalk, but lack thereof is really not large enough an issue with simple interfaces to warrant introducing a lot of pointerplate code. On the other hand, passing arguments as a composite structure to facilitate dependency injection (using a unified constructor) is a good enough reason regardless of the number of arguments. Although arguable not really required for solely that purpose.

Link to comment
Share on other sites

  • 2 weeks later...

I'm preparing for my first 5.3 project, thought I'd share my own little 5.3 options supertype..

 

<?php
namespace kwd\pfnotify\options;
use kwd\pfnotify\exceptions;

abstract class Options 
{
public function __construct(array $args)
{
	foreach($args as $key => $value)
	{
		if(!method_exists($this, 'set' . $key))
		{
			if(!property_exists($this, "_$key"))
			{
				throw new exceptions\InvalidArgumentException(exceptions\InvalidArgumentException::INVALID_KEY, $key);
			}

			$this->{"_$key"} = $value;

			continue;
		}

		$this->{'set' . $key}($value);
	}

	array_walk(get_object_vars($this), function($item, $key) use ($args){

		if(!isset($args[substr($key, 1)]))
		{
			throw new exceptions\InvalidArgumentException(exceptions\InvalidArgumentException::MISSING_KEY, $key);
		}
	});
}

public function __call($method, $arguments)
{
	if(!strpos($method, 'get') === 0)
	{
		throw new \BadMethodCallException("Method '$method' does not exist");
	}

	$ucProp = substr($method, 3);

	$property = '_' . strtolower(substr($ucProp, 0, 1)) . substr($ucProp, 1);

	if(!property_exists($this, $property))
	{
		throw new exceptions\InvalidArgumentException(exceptions\InvalidArgumentException::INVALID_KEY, $property);
	}

	return $this->$property;
}
}

 

You can extend this, define some private props and implement some getters (or not, it'll fetch them anyway). Found my first use for closures as well, albeit a little forced :P

 

EDIT: btw I notice the post above says pointerplate. That's bogus of course. I meant boilerplate.

 

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.