Re: Keyword Arguments

Kevin Edwards <[email protected]> Sat, 16 Jun 2012 16:33:35 -0500
Newsgroups gmane.comp.lang.io
Message-ID <[email protected]>
On 6/15/2012 5:59 PM, Steve Dekorte wrote:
> On 2012-06-15 Fri, at 02:02 AM, Kevin Edwards wrote:
>> As a simple and contrived example, consider a Rectangle object.  
>> Do we package that as 2 Points (x1, y1) and (x2, y2)?  Or as 2 
>> Ranges (x1, x2) and (y1, y2)?  Or flat (x1, x2, y1, y2)?  In OO, 
>> if we implement the logic for one format (like intersect, union, 
>> expand, contract, etc.), then we either have to convert to/from 
>> the other formats (more data moves), or we'll have to (manually) 
>> duplicate the core logic for each format we use (which can impact 
>> development time and locality due to size).
>
> Exactly, we can ask the other object to describe itself in a way 
> we understand. While FP would require large switch statements 
> scattered around all the functions of the system to handle every 
> type - all of which would break upon the introduction of a new type.

I think FP would use some sort of dispatch mechanism like pattern 
matching rather than static switching, but perhaps more interesting 
is that in FP, functions are viewed as always applying to only a 
single argument, which is very similar to your ideal view.

In fact, your ideal view looks like keyword args when compared to 
FP's positional currying.  Whereas you would avoid positional 
binding, FP avoids name (keyword) binding.  I think both are useful 
forms of expression and they are technically equivalent.


> Example:
>
>     aPerson setAge(thePersonsAge)
>     aPerson print
>     aPerson canRetire
>
>     vs.
>
>     aPerson print(thePersonsAge)
>     aPerson canRetire(thePersonsAge)
>
>
> etc
>
> It's clear that the age is being stored in the wrong place and 
> that passing it in constantly is a bad design choice.
> To me this suggests that anything that encourages developers to 
> pass more data also encourages them to make bad design decisions.

I agree with you entirely here, I'm just saying that keyword 
argument syntax could do the exact same thing as your first 
example.  e.g.:

   aPerson = Person(age = thePersonsAge)
   aPerson print
   aPerson canRetire

Note that the same age setter would be used as in your example.  You 
wouldn't need to implement a monolithic constructor to handle that 
-- it would be modular, as in your example.

To be clear, I'm not advocating the traditional implementation of 
keyword args.  Instead, I'm suggesting that it would roughly 
correspond to `Person clone do( age = thePersonsAge ) eval` because 
that is basically what an eager method call does -- it clones the 
definition, binds the arguments, and returns the result.  The syntax 
is just a natural expression of the relationships involved.


>> You also mentioned validation elsewhere.  e.g. setA can perform 
>> validation.  `a=1` could perform validation, too, couldn't it?  
>> More generally, I like the idea of custom setters and setSlot 
>> using the same syntax and control path (the same as any dict for 
>> that matter).
>
> Well, you can get this by using ::= to create the slot and then 
> calling the setter method (which is considered good coding 
> practice) instead of using = .

Yeah, the user should not need to be aware of whether a setter is 
overridden, and you achieve that by always creating and using setX 
even if it just calls the raw setter for x.  So, you pay the penalty 
of a custom setter right off the bat just in case you want to 
override it in the future.

But if you move to using Slots, then you can transparently intercept 
any behavior of a Slot (get, set, update, delete, etc.) using a 
single control path, and setX would no longer exist in the same 
namespace as x (similarly for updateX, deleteX, etc. if you ever 
need to override those behaviors).


> Also, you might look at how self unified assignment with methods. 
> Maybe there is something there.

Could you elaborate?  I thought Self did roughly the same thing as 
Io, but calls it `x:` rather than `setX`.

If `x(1)` becomes Io's setter syntax, then you'll probably end up 
using extra messages to accomplish traditional application.  For 
example:

   F clone x(1) y(2) eval

Here we are using "clone" to initiate the application and using 
"eval" to indicate the end of it and force a simplification.  That 
whole chain is what `F(x=1, y=2)" traditionally means.  And, as I 
mentioned previously, we could add a little syntax to prevent the 
eager eval if we want.


>> i think part of the answer to performance is a more flexible 
>> dispatcher which can take the `at` and `put` together, so that an 
>> explicit Slot object doesn't actually have to be created if it 
>> doesn't already exist.  But this may bear some similarity to 
>> multiple dispatch, so I'm interested in learning your thoughts 
>> about that anti-pattern. :-)
>
> Multiple dispatch makes the deep assumption that a notion of 
> external type exists. But what notion of type can deal with 
> something like a proxy which is, in effect, all possible types?

Proxies have a distinct type that is defined at a meta-level 
relative to the proxied type.  For example, a perfectly transparent 
Proxy will respond to all messages identically to how the proxied 
type would respond, and an external ProxyViewer object would be used 
to look inside the Proxy and control the Proxy's underlying behavior.

We use proxies specifically for this special meta-behavior which 
defines the Proxy type distinctly from other types.  If you don't go 
to the meta-level to define Proxy, and instead try to merge that 
meta-behavior into the base behavior, you end up with an imperfect 
Proxy.


> Or from another angle: any system that depends on a notion of type 
> that is anything more than asking an object what it choses to do 
> at the moment violates encapsulation in ways that have serious 
> consequences for what the system can and can't do.

That is a fascinating and profound angle. :-)

You're right that the notion of type imposes constraints on what an 
object can and can't do, but that can actually be very useful 
because it explicitly tells us the bounds within which the object 
can change while maintaining a coherent system.  Granted, a lot of 
type systems unnecessarily over-constrain objects or prohibit 
changing constraints at run-time, but that does not mean it has to 
be that way.

For example, we can instrument an `if(<cond>, <then>)` call and 
change its implementation in all sorts of flexible ways, but 
fundamentally it must always evaluate <then> based upon the 
condition.  If we break that constraint, then we break the larger 
system relative to the intent of the programmer.  That constraint 
defines the type of `if`.

So, exposing the essential constraints on objects and messages 
actually frees the system to do a whole lot more, like safely reason 
about code in order to perform intent-preserving substitutions, cut 
out dead branches, or dispatch to specialized solutions from 
multiple messages, as in your `aDict at(key) put(value)` example.

Phew!  Sorry for the long e-mail, but I'm hoping it is constructive 
and worth reading. :-)

Kevin