Re: Keyword Arguments

Kevin Edwards <[email protected]> Fri, 22 Jun 2012 00:55:23 -0500
Newsgroups gmane.comp.lang.io
Message-ID <[email protected]>
On 6/21/2012 2:10 AM, Steve Dekorte wrote:
> On 2012-06-16 Sat, at 02:33 PM, Kevin Edwards wrote:
>> 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.
>
> The problem is that the keyword arg doesn't save any code, adds 
> new syntax and semantics, and encourages more argument passing, 
> which (it seems we are coming to agree) is typically a sign of bad 
> design.

Yes, but the bad design is not the call syntax itself, since that 
can be superficially fixed by passing frames around or splatting. 
The bad design is having only a monolithic function body to dispatch 
the call to.  It's only in languages where those are one-to-one that 
high arity is typically a sign of bad design.

Keyword arg syntax does save a little code, right?

aPerson := Person(age := thePersonsAge)
aPerson := Person clone do(age := thePersonsAge)

But you're right that it will either add new syntax for targeting 
the inner scope (the cloned Person), or it will break semantics by 
sending setSlot to the inner scope first rather than to the outer scope.

I have an affinity toward exposing this equivalence syntactically, 
but if you're not satisfied with it, that's fine. I'm just happy we 
understand each other and that it has led to some other interesting 
discussion, like proxies... :-)


>>> 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.
>
> You're still making assumptions about what is on the other side of 
> the proxy.  :)
>
> For example, Io's futures are proxies but to objects that may not 
> exist yet. So what "type" is the proxy in that case? If it's a 
> type that responds to everything, then multiple dispatch breaks.

If the proxied type is unknown, then the dispatcher will have to 
wait until that type is known before continuing a test against it. 
It's the same for multiple dispatch as for single dispatch.

To be clear, I think that the base behavior of futures should not 
include meta slots like `forward`, `_future`, `_become`, or `type` 
because those override the proxied value's slots.  i.e. FutureProxy 
is an imperfect proxy because that meta behavior is merged into the 
base behavior.

For example, if a FutureProxy is sent a `type` message, then it 
should respond with the type it is proxying.  If that type is 
unknown, then it should wait on the future result until the type is 
known.  FutureProxy should not say its type is "FutureProxy" at this 
base level (which it currently does).  If you want to know if an 
object is actually a proxy, then you should go through a separate 
ProxyViewer.

Also consider that the `type` of a future result is often known 
before the concrete result actually arrives.  If Io tracked that 
type, then we could get an immediate response to `type` and 
potentially even avoid ever having to wait for the concrete result. 
(This can be generalized into a system for lazy, partial evaluation.)


> Likewise, the proxy may wrap something loaded or created at 
> runtime or discovered across a network, etc and/or it may be an 
> object which changes at runtime. All these possibilities break 
> multiple-dispatch.

Could you spell out for me how these would break multiple dispatch?

 From my perspective, multiple dispatch isn't that unusual. 
Sometimes we manually build static versions of it within single 
dispatch.  When you write a method body that tests multiple values 
(args or their attributes) and then executes different code based 
upon those tests, you are manually implementing a static multiple 
dispatcher.

Similarly, if we didn't have a formal single dispatch system, we 
might manually put a static switch in the body of a function in 
order to dispatch based upon a single arg's type.  Introducing a 
formal single dispatch system allows us to declaratively manipulate 
that switch.


>>> 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.
>
> The system I want to use is one where there is no such thing as 
> non-runtime (no compile time) and anything that prevents changes 
> that can occur at runtime (sans those that can't be caught as 
> runtime exceptions) is AFAICS incompatible with this.

I agree, I want changes to be able to occur at runtime, too, and the 
way to accomplish that is to track the constraints on the system so 
that it knows how to intelligently coordinate changes at runtime and 
manage dependencies.

Treating messages as black boxes actually hinders dynamism because 
we might clobber hidden constraints if we make changes to them. 
Black boxes impose false constraints.


>> 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.
>
> I've actually implemented useful code that broke this assumption. :)
>
> The case was a JIT compiler binding for Io using libJIT. I defined 
> if(), etc as methods that acted on the jitting function object so 
> you would just run the Io code to JIT the method. By reusing Io's 
> evaluation, it avoided having to write an AST walker and a bunch 
> of related code to do the compilation.

If I understand you correctly, you're saying that you override the 
default if(), etc. so that they construct the corresponding libJIT 
model.  That is a valid form of evaluation that does not break the 
`if` type constraint.  Instead, it preserves that constraint across 
the conversion.  i.e. You are converting an Io `if` into a jit_if 
that will still evaluate the <then> based upon the condition.

Note that if your JITer unconditionally overrides `if`, then you are 
assuming a static type for `if`.  i.e. you are assuming that no one 
else has changed the constraints on `if`.

For example, what if we use another library that adds a constraint 
to log all calls to `if`?  How do we safely and automatically 
combine these two constraints: JIT and Logging?  Or is the rule that 
the last one to override `if` wins?  No cooperation?  Such a rule 
will force users to manually integrate constraints into potentially 
combinatorial ad hoc solutions.

I think JIT is actually a good reason to track the intended 
constraints of a system.  I'd imagine that Io's JIT is pretty 
limited because there's no standard way to reason about the intended 
constraints.

Kevin