Re: Keyword Arguments
"dennisf486" <[email protected]> Mon, 17 Sep 2012 05:28:18 -0000
| Newsgroups | gmane.comp.lang.io |
|---|---|
| Message-ID | <[email protected]> |
I found this thread fascinating because I'm directly working on two things affected by these issues. First, I found it necessary to implement a little multiple dispatch for the game engine I'm writing in Io. Second, I'm designing a statically typed language that is nevertheless partly inspired by Io, and I'm faced with the keyword arguments issue, the "how to parse a: b c d: e f" thing Jeremy Tregunna brought up, and other questions brought up in this thread.
But before that I want to throw in a comment about all this talk of perfect vs. imperfect proxies: I think what you want here is Gilaad Bracha's mirrors. The idea is just that by requiring all meta operations to go through a mirror object constructed from the target object, you side-step questions like where methods like "forward" should go on a proxy because the metaobject api is no longer part of the interface of "Object". Then it would be a non-issue.
Now, multiple dispatch: my use of it is perhaps the exception that proves the rule in support of Steve's argument. You see, I needed multiple dispatch precisely for the situations in which encapsulation had to be broken. What I did was I took all the icky code I had in the engine that knew about the internal details of two different kinds of objects, and encapsulated it into a third location which would be called via a multiple dispatch when those two kinds of objects interact.
My game engine pulls together multiple 3rd party libraries written in C/C++. The physics library classes do not know about the graphics classes and vice versa. As much as possible I tried to keep the Io scripts which use them also agnostic of the other libraries. However there are situations where cross cutting issues or interface between two disparate systems are inevitable. For instance to tie a graphics object to a physics object I have to initialize a (C++) object called a motionState and attach it to both the graphics object and the physics object (in different ways).
Moreover there are different combinations of physics and graphics objects, and I only want to attach them together in this way for some combinations and not others. (And there are other systems too such as AI that I left out of this example.)
I didn't implement a pervasive multiple dispatch system, just a collection of objects I call Interactions which are asked if they respond to the two types of game objects being considered when those game objects are first created.
Anyhow just as Steve said, it immediately tied me to certain a priori assumptions about types of objects because a) I had to assign type labels to objects to make it work and b) I was never able to figure out a way to make it conform more closely to the Io spirit of dynamic typing where you completely allow target objects to choose their own way to respond. (The difficulty with having multiple dispatch while still allowing objects to completely choose their response is because of the chicken and egg problem that object A's response must depend on object B's whose response must depend on object A's and so on.)
It would be a fallacy, however, to move from "I can't figure out a way to reconcile them" and "Steve can't figure out a way to do it" to "that cannot be done". And it would be a further mistake of logic to say "therefore multiple dispatch shouldn't be chosen". All we can say with absolute correctness is that, as far as people who've thought long and hard about it can say, multiple dispatch is at odds with [a certain fundamentalist school of] object orientation. But I compare this choice to philosophies of mathematics where accepting certain premises forces you to reject others to come up with a consistent system, but you can instead choose to accept premises you rejected before, replacing some of the original ones, and also put together a consistent system. I think we're in an historical period where many competing theories of computation will and should be explored, and I think we'll go through many periods of the pendulum swinging one way and another before it's clear which is the most fruitful.
Anyway to quickly wrap up the other points I brought up -
Steve saying he wants to eliminate compile time so that everything is run time is interesting to me because in the language I'm working on now, I plan to use staged compilation to achieve a similar the goal of having the meta language be just the same as the implementation language; but instead of eliminating compiling I just plan to run the compiler recursively. My point being that I think you can go left or right here and end up in a similar end state.
For the language I'm working on I plan to use ":" to mean "having type of" so that
a : Int
creates a label (or variable) "a" having type "Int". The kicker is the thing on the "type" side can be any expression including function calls, so that you can write functions that compute types. The problem I have now is that complex expressions involving ":" are ambiguous as to grouping. My point is it's possible ":" is just a poor choice for many kinds of syntax because parens or brackets are much more unambiguous. (For instance Powershell puts types in [square brackets]. It looks dorky but it's clear.) I may use ":" syntax anyway thought because it is easier to type.
Finally, to get back to keyword arguments - I'm really conflicted as to whether to use named arguments or positional ones for the language I'm creating, or perhaps allow both. I don't think named arguments would work for Io because "f(x:=1, y:=2)" would only work if you messed with Io's scope rules which would be a pretty drastic change. The way I plan to make it work in the language I'm designing is to make curly brace blocks define scopes and make structs, tuples, and argument lists all interchangeable & decompose to each other. So, "f( { x:=1, y:=2 } )" would not leak "x" and "y" (because the brace defines a scope, the "{ whatever }" would decompose to a "named tuple" which is the same thing as a struct (i.e. a tuple where the positions have names) and then the struct would in turn splat as the arg list for "f()". Actually, it might be possible to define the braces operator in Io too to enable something like this, if you were dead set on using named arguments?
--- In [email protected], Kevin Edwards <edwakev@...> wrote:
>
> 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
>