|
View:
New views
20 Messages
—
Rating Filter:
Alert me
|
| < Prev | 1 - 2 - 3 | Next > |
|
|
Operator overloading revisitedFollowing the decimal discussion where values came up I looked for
information on that. The most substantial I could find was Mark's operator overloading strawman. I think the current strawman has a number of weaknesses. It relies on the (hypothetical) type system which makes it fundamentally different from normal method dispatch. It uses double dispatch for all user-defined operators which makes optimization difficult. It is asymmetrical: the left hand side always controls dispatch. I'd like to propose an alternative approach that avoids these problems. You could call it "symmetric" operator overloading. When executing the '+' operator in 'a + b' you do the following (ignoring inheritance for now): 1: Look up the property 'this+' in a, call the result 'oa' 2: If oa is not a list give an error, no '+' operator 3: Look up the property '+this' in b, call the result ob 4: If ob is not a list give an error, no '+' operator 5: Intersect the lists oa and ob, call the result r 6: If r is empty give an error, no '+' operator 7: If r has more than one element give an error, ambiguity 8: If r[0], call it f, is not a function give an error 9: Otherwise, evaluate f(a, b) and return the result. The way to define a new operator is to add the same function to the 'this+' list of the left-hand side and the '+this' list on the right-hand side. This would probably be handled best by a simply utility function, say Function.defineOperator: function pointPlusNumber(a, b) { return new Point(a.x + b, a.y + b); } Function.defineOperator('+', Point, Number, pointPlusNumber); Using this approach it's easy to extend symmetrically: function numberPlusPoint(a, b) { return new Point(a + b.x, a + b.y); } Function.defineOperator('+', Number, Point, numberPlusPoint); In this case you need two different functions, one for Point+Number and one for Number+Point, but in many cases the same function can be used for both: function addPoints(a, b) { return new Point(a.x + b.x, a.y + b.y); } Function.defineOperator('+', Point, Point, addPoints); This approach is completely symmetric in the two operands. Operator dispatch is fairly similar to ordinary method dispatch and doesn't rely on a type system. Programmers don't have to manually implement double dispatch. It my be a bit more work to do lookup but on the other hand it is straightforward to apply inline caching so often you'll only need to do that work once. It can be extended to deal with inheritance -- you might consider all operators up through the scope chain and pick the most specific one based on how far up the scope chain you had to look. It may look a bit foreign but I think it has a lot of nice properties. Comments? -- Christian _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Sun, Jun 28, 2009 at 7:05 AM, Christian Plesner Hansen <christian.plesner.hansen@...> wrote:
Following the decimal discussion where values came up I looked for Are you referring to <https://mail.mozilla.org/pipermail/es-discuss/2009-January/008535.html>? It starts with: [...suggested restrictions on when this test applies, but all without presuming any "type"s beyond what ES5 already supports...]Define a new nominal type named, let's say, "Operable". (I'm not stuck on I'm glad you started with one of the motivating problem cases: "+". The reason I conditioned the new overloadings on Operable is to handle the "+" case, which, to be upwards compatible, must continue to .toString() on legacy non-number objects. One of Sam's earlier decimal proposals ran aground on this "+" problem. IIUC, your proposal could deal with the legacy "+" problem by duck typing on "this+" and "+this"? This may be adequate. My strawman was written without any concept of value types at the time. In committee, Waldemar then raised the "===" issue. "===" currently has the great virtues that it is a reliable equality test that does not run user code during the comparison, and whose veracity does not depend on user code. NaN and -0 aside, if x === y, then they are observably indistinguishable. Any occurrence of that value of x in a computation state can be substituted for any occurrence of that value of y in that computation state without effecting the semantics of that state. We did not want to lose those virtues, so we do not want to allow user defined abstractions to overload ===. Neither do we want Point(3, 5) === Point(3, 5) to be false. AFAIK, the only way to reconcile these goals is to introduce value types. In earlier internal email I summarized the value-type thinking as: * start with something like my operator-overloading-by-double-dispatch proposal, * but tie the ability to respond to operators to being a value -- an instance of a value type/class. * A value is necessarily frozen and compares "===" based on cycle-tolerant pairwise "===" comparison of named properties [missing from earlier email: as well as [[Prototype]] and [[Class]] properties]. * Therefore, a value is indistinguishable from a shallow copy of the same value. Strings are a precedent. * Values would be able to overload "==" but not "===". * "===" would remain reliable and not cause user code to run during the comparison. It seems to me that the Operable-test and value-type issues are orthogonal to the core suggestion of your email: whether operator dispatch is based on Smalltalk-like double dispatch, in which the left side is asymmetrically in charge, or on mutual agreement which is indeed symmetric. The first bullet could be changed to your proposal without affecting the rest of the value-type issue. I note that your symmetric suggestion avoids the problem of most other symmetric overloading systems, like Cecil, of diffusion of responsibility. Since your's only proceeds under mutual agreement, both operands are responsible for the result. Unfortunately, this means that new libraries like Point, in order to allow "pt + number" or "number + pt", must in practice monkey patch Number.prototype to know about these new overloads. Should Number.prototype in that frame (global object context) already be frozen to prevent such monkey patching, then this Point extension can no longer be loaded into that frame. By comparison, the cool thing about double dispatch is that new types can arrange to work with old types without monkey patching, given only that the old types engage in the generic enabling default behavior and that the new types have explicit cases for the old types it knows about and wishes to work with. Might there be a way to combine the modular extension virtues of double-dispatch with the symmetric-responsibility virtues of your proposal? I don't know. But if so, that would be awesome! -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisited> I note that your symmetric suggestion avoids the problem of most other
> symmetric overloading systems, like Cecil, of diffusion of responsibility. > Since your's only proceeds under mutual agreement, both operands are > responsible for the result. Unfortunately, this means that new libraries > like Point, in order to allow "pt + number" or "number + pt", must in > practice monkey patch Number.prototype to know about these new overloads. > Should Number.prototype in that frame (global object context) already be > frozen to prevent such monkey patching, then this Point extension can no > longer be loaded into that frame. Can you give me some context on freezing built-ins? What's the purpose? If it is to prevent the behavior of numbers to change under people's feet then you might argue that preventing libraries from changing the behavior of '+' is a good thing. Note that adding an operator doesn't actually have to change Number.prototype -- what it changes is the list of operator functions. You could allow that even if Number.prototype is frozen. It would still be "monkey patching" but if you use something less general than a list, say an OperatorCollection that you could only add to, maybe it would be okay. Again, I don't know the exact purpose of freezing types this way so it's hard for me to tell if that solves the problem. _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Sun, Jun 28, 2009 at 2:51 PM, Christian Plesner Hansen <christian.plesner.hansen@...> wrote:
yes then you might argue that preventing libraries from For objects that already have a '+' behavior, yes. The issue isn't only built-in and operators. If library L1 defines constructor C1 that makes objects that respond in a certain way when their foo method is called, then it is monkey patching if library L2 loaded later modifies C1.prototype to alter the foo behavior of these instances. L1 may or may not be designed in the expectation that other libraries will monkey patch it. If it wishes to prevent such monkey patching, it can do so by freezing, say, C1 and C1.prototype. However, if L2 introduces creates distinct objects that respond to foo in a different way, that is not a monkey patch of L1. Hence the introduction of a distinct Operable in the original proposal.
Interesting. This is worth exploring. -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Jun 28, 2009, at 1:24 PM, Mark S. Miller wrote:
"Diffusion" sounds like a problem, a bad thing, but consider (I've quoted this before) the use-case: The generalization of receiver-based dispatch to multiple dispatch provides a number of advantages. For example, multimethods support safe covariant overriding in the face of subtype polymorphism, providing a natural solution to the binary method problem [Bruce et al. 1995; Castagna 1995]. More generally, multimethods are useful whenever multiple class hierarchies must cooperate to implement a method’s functionality. For example, the code for handling an event in an event-based system depends on both which event occurs and which component is handling the event. MultiJava: Design Rationale, Compiler Implementation, and Applications Curtis Clifton, Todd Millstein, Gary T. Leavens, and Craig Chambers So there are use-cases involving responsible classes A and B (event source and sink), no third parties. But there are also the third party cases you're thinking of. The last time this came up (https://mail.mozilla.org/pipermail/es-discuss/2009-January/008715.html), I wrote: Over and over, in Mozilla code and especially on the web, we've seen "code mashups" where one does not always have the money, or even the ability, to monkey-patch class A or class B to understand each other. Wrapping can be done to make a class C with operators, at some cost. Why this is always a feature and never a bug is not clear, and Chambers, et al. have researched a fix to it, viewing it as a bug to motivate their research. I can't find a reply from you on this :-), so let me take this opportunity to ask: how does requiring a third party to create a wrapper class C discharge undiffused responsibility in any useful way? A and B still are not involved in the wrapping done by class C through which A and B can become (wrapped) operands to an operator defined by C. Often the creation of such a wrapper amounts to expending effort to write tedious boilerplate that has non-trivial runtime and space overhead. /be _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Mon, Jun 29, 2009 at 11:21 AM, Brendan Eich <brendan@...> wrote:
Let's try a reductio ad absurdum. It seems to be that the argument you're making applies just as well to w.foo(x) or bar(y,z) In conventional oo reasoning, the first expression is making a request to the object bound to w. The second is making a request to function bound to bar. In both cases, the requested object is responsible for deciding what the next step is in responding to that request. The veracity of the result is according to the responsible object. If there's a bug in this result, the responsible object may not itself be the one that is buggy. However, the blame chain starts there. What if w doesn't respond to a foo request? Currently our choices are 1) rewrite our first expression so that it no longer asks w to foo. 2) modify w or w.[[Prototype]] or something so that w knows how to foo. 2a) Get the provider/author of w to do this and release a new version 2b) Fork or rewrite the source for w 2c) Monkey patch w or w.[[Prototype]] from new module M2 3) Wrap the original w object with a foo-responding decorator 3a) Conventional decoration, where other requests are manually forwarded 3b) In JS, one can decorate by prototype inheritance 3c) If we ever have catchalls, one might be able to use them for decoration All the options above except for #2c maintain the locality of reponsibility that makes objects so pleasant to reason about. 2c has come to be regarded as bad practice. I suggest that one of the main reasons why is that it destroys this locality. w shouldn't be held responsible if it can't be responsible. One of the great things about Object.freeze is that w's provider can prevent 2c on w. The Cecil-style operator overloading argument, extended to this example, would enable a fourth option 4) Allow module M2 to say how w should respond to foo. I grant that #4 is not as bad as #2c. But does anyone care to argue that #4 would be a good thing in general? If not, why would #4 be ok for operators but not method or function calls? -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Jun 29, 2009, at 11:55 AM, Mark S. Miller wrote: On Mon, Jun 29, 2009 at 11:21 AM, Brendan Eich <brendan@...> wrote: It seems to me you are the one making a universal claim, for OOP methods as the one true way, which could be argued to lead to absurd (or just awkward) conclusions such as double dispatch (hard for V8, easy for TraceMonkey :-P) and "reverse+", "reverse-", etc. operator-method bloat.
Functions have their place, so does OOP. But OOP does not mean functions are obsolete -- consider Math.atan2, a function (not a method -- ignore the Math poor man's namespace!) taking two parameters x and y. Either x or y could be blame-worthy for some call (signed zero has a use-case with atan2, the numerics hackers tell me), but there's no single receiver responsible for computing the result given the other parameter as an argument. atan2 is a function, arguably similar to a division operator. "Blame chain" is a good phrase, and of course we've been tracking PLT Scheme Contracts for a while and are still keeping them in mind for some harmonious future edition. But if responsibility is about blame, there are many ways to skin that cat. And there are other costs competing with the cost of weak or wrongful blame. Since JS has functions, not only methods, we have benefits countervailing the cost of having to blame one of two or more actual parameters when analyzing for responsibility. bar(y, z) is tenable in JS interface design, depending on particulars, sometimes competitive with w.foo(x), occasionally clearly better. What if w doesn't respond to a foo request? Currently our choices are Sorry, but you are assuming your conclusion -- the locality of responsibility and its pleasantness in all cases, over against other costs, is precisely the issue. Experience on the web includes enough cases where third parties have to mate two abstractions in unforeseen ways. We seem to agree on that much. But your 1-3 do not prove themselves superior to 4 (multimethods, cited below) without some argument that addresses all the costs, not just blame-related costs. 2c has come to be regarded as bad practice. I suggest that one of the main reasons why is that it destroys this locality. w shouldn't be held responsible if it can't be responsible. One of the great things about Object.freeze is that w's provider can prevent 2c on w. Many JS users have already given voice (see ajaxian.com) to fear that freeze will be overused, making JS brittle and hard to extend, requiring tedious wrapping and delegating. This is a variation on Java's overused final keyword. Object.freeze, like any tool, can be misused. There are good use-cases for freeze, and I bet a few bad ones, but in any case with freeze in the language, monkey-patching is even harder to pull off over time. That leaves forking or wrapping in practice. Why forking or wrapping should always win based on responsibility considerations, if one could use a Dylan or Cecil multimethod facility, is not at all clear. Forking has obvious maintenance and social costs. Wrapping has runtime and space costs as well as some lesser (but not always trivial) maintenance cost. So why shouldn't we consider, just for the sake of argument, dispatch on the types of arguments to one of several functions bound to a given operator, in spite of the drawback (or I would argue, sometimes, because of the benefit) of the responsibility being spread among arguments -- just as it is for Math.atan2's x and y parameters? The Cecil-style operator overloading argument, extended to this example, would enable a fourth option Complete with multimethods! If not, why would #4 be ok for operators but not method or function calls? The ES4 proposal (http://wiki.ecmascript.org/doku.php?id=proposals:generic_functions) for generic functions/methods indeed subsumed the operators proposal that preceded it. I'm not trying to revive it wholesale, please rest assured. But I am objecting to circular (or at best incomplete) arguments for dyadic operators via double dispatch of single-receiver methods. As I've written before, if double dispatch is the best we can agree on for Harmony, then I'm in. But we should be able to agree on the drawbacks as well as the benefits, and not dismiss the former or oversell the latter. /be _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn 2009-06-30, at 01:26EDT, Brendan Eich wrote:
> http://www.norvig.com/design-patterns/ +∞ The flip side of the "diffusion of responsibility" is the "solipsism of encapsulation", which falls down as soon as there is more than one self (or `this` :P). _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Jun 28, 2009, at 7:05 AM, Christian Plesner Hansen wrote:
under "Design: Symmetric vs. Encapsulated Multimethods", which leads to the [Boyland 1997, Castagna 1995, Bruce 1995] refs. The ES4 generic functions proposal has refs at the bottom too. It's necessary to put on your subclass/subtype-filtering glasses when reading, of course, but then the images resolves into very much what you propose. Also, if you know Python and haven't seen this yet, have a look at: There are differences to debate between symmetric and asymmetric or encapsulated multimethods, and we can defer the CPL until (and it's not for sure) we have some user-extensible notion of subtyping, but I want to stress that the multimethod cat is out of the bag here. I like the gist of your proposal, it is a round-enough wheel. ;-) Whether it should be extended to non-operator methods is something else we can debate, or defer. I claim there's nothing special about operators, and given other numeric types one might very well want atan2 multimethods. /be _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
RE: Operator overloading revisitedPulling back from Mark's and Brendan's programming language metaphysics discussion (which I think is interesting and useful) I just wanted to say that I find Christian's proposal quite interesting. It feels like a natural fit to the language as it exists today and doesn't require any new abstraction concepts such as classes, nominal type, value types, etc. although it could be made compatible with any of them. It has a simple conceptual explanation: an operator works with two object if they both associate the same function with that operator.
It may or may not be the ultimate solution, but I think it deserves serious consideration. I do have a few thoughts and questions on the proposal as outlined: I wouldn't want to describe or specify these per object (or per prototype) operator lists as properties. Too much baggage comes with that concept. It'd just make them another internal characteristic of objects. As already mentioned, if you think of them this way there shouldn't be any interactions with Object.freeze or any other existing semantics other than whatever we might choose to explicitly define. It's too bad that the "this+" and "+this" lists can't be merged but keeping them distinct seems essential to making this scheme work. However, it also complicates the simple explanation. I assume that your example really should be defining the operators on prototype objects. Eg: Function.defineOperator('+', Point.prototype, Number.prototype, pointPlusNumber); In order to emphasize the essential difference between "this+" and "+this", I might order the arguments like: Function.defineOperator(Point.prototype,'+', Number.prototype, pointPlusNumber); There seems like there are probably cases where you would only want to define a "this+" or "+this" binding and not both. defineOperator probably should be a method on Object and there probably needs to be corresponding introspection methods. This sort of usability engineering can be worked out if this proposal gains traction. Allen >-----Original Message----- >From: es-discuss-bounces@... [mailto:es-discuss- >bounces@...] On Behalf Of Christian Plesner Hansen >Sent: Sunday, June 28, 2009 7:05 AM >To: es-discuss@... >Subject: Operator overloading revisited > >Following the decimal discussion where values came up I looked for >information on that. The most substantial I could find was Mark's >operator overloading strawman. > >I think the current strawman has a number of weaknesses. It relies on >the (hypothetical) type system which makes it fundamentally different >from normal method dispatch. It uses double dispatch for all >user-defined operators which makes optimization difficult. It is >asymmetrical: the left hand side always controls dispatch. > >I'd like to propose an alternative approach that avoids these >problems. You could call it "symmetric" operator overloading. When >executing the '+' operator in 'a + b' you do the following (ignoring >inheritance for now): > > 1: Look up the property 'this+' in a, call the result 'oa' > 2: If oa is not a list give an error, no '+' operator > 3: Look up the property '+this' in b, call the result ob > 4: If ob is not a list give an error, no '+' operator > 5: Intersect the lists oa and ob, call the result r > 6: If r is empty give an error, no '+' operator > 7: If r has more than one element give an error, ambiguity > 8: If r[0], call it f, is not a function give an error > 9: Otherwise, evaluate f(a, b) and return the result. > >The way to define a new operator is to add the same function to the >'this+' list of the left-hand side and the '+this' list on the >right-hand side. This would probably be handled best by a simply >utility function, say Function.defineOperator: > > function pointPlusNumber(a, b) { > return new Point(a.x + b, a.y + b); > } > > Function.defineOperator('+', Point, Number, pointPlusNumber); > >Using this approach it's easy to extend symmetrically: > > function numberPlusPoint(a, b) { > return new Point(a + b.x, a + b.y); > } > > Function.defineOperator('+', Number, Point, numberPlusPoint); > >In this case you need two different functions, one for Point+Number >and one for Number+Point, but in many cases the same function can be >used for both: > > function addPoints(a, b) { > return new Point(a.x + b.x, a.y + b.y); > } > > Function.defineOperator('+', Point, Point, addPoints); > >This approach is completely symmetric in the two operands. Operator >dispatch is fairly similar to ordinary method dispatch and doesn't >rely on a type system. Programmers don't have to manually implement >double dispatch. It my be a bit more work to do lookup but on the >other hand it is straightforward to apply inline caching so often >you'll only need to do that work once. It can be extended to deal >with inheritance -- you might consider all operators up through the >scope chain and pick the most specific one based on how far up the >scope chain you had to look. > >It may look a bit foreign but I think it has a lot of nice properties. > Comments? > > >-- Christian >_______________________________________________ >es-discuss mailing list >es-discuss@... >https://mail.mozilla.org/listinfo/es-discuss _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Jun 30, 2009, at 10:35 AM, Allen Wirfs-Brock wrote:
> Pulling back from Mark's and Brendan's programming language > metaphysics discussion (which I think is interesting and useful) I > just wanted to say that I find Christian's proposal quite > interesting. It feels like a natural fit to the language as it > exists today and doesn't require any new abstraction concepts such > as classes, nominal type, value types, etc. although it could be > made compatible with any of them. It has a simple conceptual > explanation: an operator works with two object if they both > associate the same function with that operator. > > It may or may not be the ultimate solution, but I think it deserves > serious consideration. We can entertain strawman UI but I hope to focus on substance not surface. Costs that seem to leap out from Christian's mutual-asymmetric multimethod proposal: 1. Two lookups, "this+" and "+this", required. 2. Intersection. (2) looks like a transformation or simplification of standard multimethod Class Precedence List usage to find the most specific method. Any system allowing multiple methods for a given operator will have something like it. (1) seems avoidable, though -- *assuming* it is not a phantom cost. We have to deal with the symmetric vs. encapsulated issue, though. But notice that Christian's proposal does not use "this" in the method bodies! function pointPlusNumber(a, b) { return new Point(a.x + b, a.y + b); } function numberPlusPoint(a, b) { return new Point(a + b.x, a + b.y); } function addPoints(a, b) { return new Point(a.x + b.x, a.y + b.y); } These are nice generic functions. With type annotations or guards, you could imagine them as adding to generic function "+": generic function "+"(a :Point, b :Point) { return new Point(a.x + b.x, a.y + b.y); } Syntax is not the point, please hold fire and spare the bikeshed -- the unencapsulated (no |this| or |self|) nature of these functions is what I'm stressing. So why require "this+" and "+this" lookups? If the reason is to assign responsibility to the different constructors [*], then how can you tell who is responsible? Function.defineOperator in the proposal binds both "this+" and "+this". I agree these should not be plain old properties. But then why spec them as named internal properties of any kind? Suppose we had a generic function "+" (syntax TBD) to which methods could be added for particular non-conflicting pairs of operand types. Then the lookup would be for two types A and B, but this can be optimized under the hood. See, e.g., http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.35.4799 Seems worth allowing this kind of optimization. Whether and how users add multimethods we can debate at greater length. But can we agree that the "this+" and "+this" properties are not necessary? /be [*] or constructor.prototypes in your reply, but I don't see why we would require noisy ".prototype" suffixing -- instanceof does not! _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn 2009-06-30, at 14:06EDT, Brendan Eich wrote:
> These are nice generic functions. With type annotations or guards, > you could imagine them as adding to generic function "+": > > generic function "+"(a :Point, b :Point) { > return new Point(a.x + b.x, a.y + b.y); > } > > Syntax is not the point, please hold fire and spare the bikeshed -- > the unencapsulated (no |this| or |self|) nature of these functions > is what I'm stressing. +∞ again > If the reason is to assign responsibility to the different > constructors [*] I don't understand the "assign responsibility" argument. Is this about aligning (conflating?) security/access-control with classes/ instances? Isn't a generic function a class that you could assign responsibility to? _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
RE: Operator overloading revisited>>From: Mark S. Miller
. >What if w doesn't respond to a foo request? Currently our choices are >> ... >> 2c) Monkey patch w or w.[[Prototype]] from new module M2 ... >>All the options above except for #2c maintain the locality of reponsibility that makes objects so pleasant to reason about. 2c has come to be regarded as bad practice. I suggest that one of the main reasons why is that it destroys this locality. w shouldn't be held responsible if it can't be responsible. One of the great things about Object.freeze is that w's provider can prevent 2c on w. >From: Brendan Eich ... >Many JS users have already given voice (see ajaxian.com) to fear that freeze will be overused, making JS brittle and hard to extend, requiring tedious wrapping and delegating. This is a variation on Java's overused final keyword. Object.freeze, like any tool, can be misused. There are good use-cases for freeze, and I bet a few bad ones, but in any case with freeze in the language, monkey-patching is even harder to pull off over time. I believe that the original experience base with this style of monkey patching was in Smalltalk where it provide to be both highly useful and also gained a reputation as a bad practice. We need to look at both sides of that equation. Smalltalk programming was largely about reuse and monkey patching was highly useful because when combining (reusing) independently created implementation "modules" into a larger application you often need to pragmatically do some tweaking around the edges of the modules in order to make them fit together. Even if you have access to the source of each module (which you typically did in Smalltalk) it was still better (more maintainable) to package the necessary integration code as part of the consuming application module rather than creating modified versions of the pre-existing modules that incorporates the integration code. The "bad practice" rap came about because in the original Smalltalk environments multiple conflicting occurrences of such monkey patching applied on the same classes would silently stomp on each other and you would get some inconsistent combination of behaviors that were difficult to test for and hence caused downstream errors. Patches that conflict with each other are fundamentally incompatible and require human intervention to resolve. Digitalk's Team/V solved this problem by treating such monkey patching as being declarative in nature and detecting any conflicting monkey patches at integration time (load time, build time, whatever..). Such a conflict were treated as a "hard error" (comparable to a syntax error). With this mechanism developers could monkey patch to their hearts content, "mashing up" all sorts of independently created code but as soon as the patches stated stepping on each other they found out and could fix the problem. I've often remarked that to me, the web feels like one big persistent Smalltalk virtual image. It's being continually extended and modified but it can never be recreated from scratch or redesigned as a whole. In such an environment you have to evolve in place and sometimes that means dynamically patching what is already there in order to make it work with something new or to use it for some new purpose. Web technologies need to be dynamic, flexible, and forgiving in order to accommodate this sort of evolution. We can provide mechanisms to help developers detect and deal with common failure scenarios but if the web doesn't stay flexible enough to allow for the possibility of such failures than it won't be able to evolve and will eventually be replaced by something that can Allen _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Mon, Jun 29, 2009 at 10:26 PM, Brendan Eich <brendan@...> wrote:
A fine counterargument if you can distinguish which cases it does and doesn't apply to. The syntactic distinction "operators" seems silly. I'm glad you seem to include Math.atan2(), as that gets us beyond the silly syntactic distinction.
Contradicted by the next text of mine you quote:
I include the bar(y,z) as an example of case against multimethods. Since you include Math.atan2() in the case for multimethods, we can focus on it, and ignore either operators or method calls.
Good. Hereafter, this example is simply atan2(x,y).
No. This is the crux. atan2 is a variable bound to a value in some lexical scope. In different scopes it may be bound to different values. In the expression atan2(x,y), the responsible party is the atan2 function. It decides the next step in processing this request, and decides in what way it will subcontract parts of the job to its x and y arguments.
I was not arguing that w.foo(x) is always better than bar(y,z). Rather, I was saying that currently neither violate locality of responsibility. In these two expressions, w and bar are the respective responsible parties.
Were we to adopt multimethods, where atan2 somehow gets enhanced by all imported modules defining overloadings of atan2 in that scope, what is the value of the atan2 variable in that scope? If it is a function composed of all lexical contributors, what happens when this function is passed as a value and then used in a different scope? What about a global atan2 (as would seem to be suggested by the Math.atan2 example you started with)? Would modules dynamically loaded into the same global environment be able to dynamically enhance the value already bound to the atan2 variable, changing the behavior of the atan2 value that may have already been passed to other scopes? Presumably this would fail if atan2 is frozen, as it should, since such mutation monkey patches the atan2 value. Or would it bind a new value to the atan2 variable, where that new value has the new behavior? Presumably this would fail if atan2 is const, as it should.
Please reread the quote above. I agree that the argument I present here for Object.freeze is incomplete. Since the purpose of this thread is not to decide whether Object.freeze is or is not a good idea, I did not feel it necessary to enumerate all the arguments on either side of this question. But neither am I assuming my conclusion. I am simply enumerating one of these arguments -- that Object.freeze is good because it enables w's provider be responsible for the behavior for which we'd like to hold it responsible. You may disagree with this argument, but I don't see how it assumes its conclusion.
I couldn't quickly find these arguments. Could you provide some links? Thanks.
Vastly underused IMO, due to syntactic overhead. In the early E language, it was easier to declare a variable let-like than const-like. One of the best changes we made was to reverse this. E variables are now const-like unless stated otherwise. Everyone, whether concerned with security or not, has found this to be a pleasant change to the language. When reading code and wondering about a variable, once you see that it's declared const-like, you can avoid many follow-on questions.
Of course. Good things are good and bad things are bad ;).
As goto and pointer arithmetic became harder in earlier language evolution. I sure hope so. Especially for involuntary monkey patching, which could be seen as an attack from the victim's perspective.
Forking is indeed quite costly and should be avoided when there's a less bad alternative. Wrapping (or decorating) may or may not be a good pattern to use depending on the particulars.
We are considering it. That's what this discussion is about. But we need to separate two kinds of considerations: 1) In a language in which all these abstraction mechanisms are present and used, which one should we use for a given problem? This will often be the kind of tradeoff you are explaining, where sometimes monkey patching or multimethods win. Indeed, the Cajita runtime library as implemented on ES3 internally monkey patches some of the ES3 primordials. Were I programming in Common Lisp and trying to make use of preexisting ibraries, I'm sure I'd use multimethods and packages. OTOH, Common Lisp's bloat and incoherence is one of the reasons I've always avoided it. 2) In a language with an adequate set of good abstraction mechanisms and already burdened by a plethora of bad or broken abstraction mechanisms, when is it worth adding a new abstraction mechanism? The bar on #2 must be much higher than #1. If the language in the absence of the proposed abstraction mechanism already has some pleasant formal properties that the addition of the new mechanism would destroy, then the loss of this property must be added as a cost in assessing #2. But not #1, since that battle is already lost.
Well, Peter's my boss, so I concede the case. Let's add multimethods. Just kidding! Actually, thanks for the link. I hadn't seen it before, and it is a nice presentation.
Huh?
I certainly agree that it's good to have a more complete enumeration of the pros and cons. I think we largely agree on many of these but disagree on the weights. JavaScript today has adequate support for modularity and abstraction, and is mostly understandable. ES5/strict even more so. Common Lisp and PL/1 never were understandable. And Common Lisp is highly immodular. I agree that ES6 should grow some compared to ES5, but I highly value retaining ES5/strict's modularity, simplicity, understandability. -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedSorry about falling behind in a discussion I started. I'll try to catch up.
> Are you referring to > <https://mail.mozilla.org/pipermail/es-discuss/2009-January/008535.html>? It > starts with: > >>> Define a new nominal type named, let's say, "Operable". (I'm not stuck on >>> this name. Perhaps "Numeric"?) We don't yet know what nominal type system >>> Harmony will have, so for concreteness, and to separate issues, let's use >>> "instanceof" as our nominal type test, where only functions whose > > [...suggested restrictions on when this test applies, but all without > presuming any "type"s beyond what ES5 already supports...] Yes that's the one. The main problem I had was with the use of "instanceof". However, as I read the proposal again I've become unsure if I've read something into it that it doesn't say. The way I understood it you would implement the + operator for Point instances something like: Point.prototype['+'] = function (that) { if (that instanceof Point) { return new Point(this.x + that.x, this.y + that.y); } else { return that['reverse+'](this); } }; // Ditto 'reverse+' Is that correct? > ===. Neither do we want > > Point(3, 5) === Point(3, 5) > > to be false. Don't we? I can see that we want Point(3, 5) == Point(3, 5) to be true but ===? > It seems to me that the Operable-test and value-type issues are orthogonal > to the core suggestion of your email: whether operator dispatch is based on > Smalltalk-like double dispatch, in which the left side is asymmetrically in > charge, or on mutual agreement which is indeed symmetric. The first bullet > could be changed to your proposal without affecting the rest of the > value-type issue. That's probably true, though I don't know how Operable solves the '+' problem so I couldn't say for sure. _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisited> Whether it should be extended to non-operator methods is something else we
> can debate, or defer. I claim there's nothing special about operators, and > given other numeric types one might very well want atan2 multimethods. I did briefly consider listing "generalizes to full multimethods" as one of the advantages but decided not to because I wasn't sure if that's a good or a bad thing... _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisited
I withdraw the seeming comparison to Common Lisp. I was trying to make a more general point, but the implied comparison is not fair. Let's take Cecil instead, since it was designed by people with extraordinarily good taste (cc'ed), and since you recommended it as an example of multimethods done right. On your recommendation, I read it. Because of its pedigree, I really wanted to like it. In the end, I was reluctantly repelled by its complexity. If it is representative of the complexity costs of multimethods done right, I consider these costs to far outweight the benefits. Other things being equal, I would chose not to program in Cecil. -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Tue, Jun 30, 2009 at 1:37 PM, Christian Plesner Hansen <christian.plesner.hansen@...> wrote:
Sorry about falling behind in a discussion I started. I'll try to catch up. Almost. It's actually a bit worse than that, since presumaby, Points want to be addable with Numbers: Point.prototype['+'] = function (that) { if (that instanceof Point) { return new Point(this.x + that.x, this.y + that.y); } else if (that instanceof Number) { // again ignoring the difference between numbers and Numbers return new Point(this.x + that, this.y + that); } else { return that['reverse+'](this); } };
Perhaps Complex(3, 5) === Complex(3, 5) is a better example? In any case, I could live with these being false in order to avoid adding value types to the language. On this one, I'm on the fence.
Ignoring Operable, in your proposal, how would you handle the need to preserve the legacy + behavior on legacy kinds of objects? The only other possibility I foresee is duck typing on the presence of the 'this+' and/or '+this' property names. That would work. -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Tue, Jun 30, 2009 at 1:51 PM, Christian Plesner Hansen <christian.plesner.hansen@...> wrote:
Do have a look at Cecil. Multimethods probably can't get much better than that. -- Cheers, --MarkM _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
|
|
Re: Operator overloading revisitedOn Jun 30, 2009, at 1:04 PM, Mark S. Miller wrote:
> On Mon, Jun 29, 2009 at 10:26 PM, Brendan Eich <brendan@...> > wrote: > On Jun 29, 2009, at 11:55 AM, Mark S. Miller wrote: >> >> Let's try a reductio ad absurdum. > > This doesn't settle anything since there is no "for all" claim in > Chambers, et al.'s words cited above. You can't reduce to an > absurdity something that according to its own proponents does not > apply in all cases. > > A fine counterargument if you can distinguish which cases it does > and doesn't apply to. The syntactic distinction "operators" seems > silly. I'm glad you seem to include Math.atan2(), as that gets us > beyond the silly syntactic distinction. Did you check out any of the papers I linked? They talk about non- silly, not-only-about-syntax use-cases (~20% by function population, I recall Chambers writing somewhere) that include (dyadic) operators, math functions, event sourcing/sinking, a few other multi-party situations with symmetric relations and no obvious primary receiver. I don't think there was ever a silly syntactic distinction about operators in anything I've written, recently or in past years, on this list or elsewhere. Value types want operator and literal extensibility. The operator part is one of N use-cases for multimethods IMHO. That's all. >> It seems to me you are the one making a universal claim, for OOP >> methods as the one true way, which could be argued to lead to >> absurd (or just awkward) conclusions such as double dispatch (hard >> for V8, easy for TraceMonkey :-P) and "reverse+", "reverse-", etc. >> operator-method bloat. > > Contradicted by the next text of mine you quote: But not by your double-dispatch proposal for operators. That's one way to add extensible operators. The other is to add multimethods. I don't know of a distinct and canonical third way. Talking about good old functions doesn't help rule out multimethods, indeed it raises them as an option since dyadic operators appear to be functions, not receiver-targeted methods. To be honest I wasn't sure what your point about good old bar(y, z) was, except to say that bar was the locus of responsibility (Tucker noted this callee-is-responsible case too). Again this does not argue against multimethods. Likewise blame systems are great (costly, PLT Scheme users typically add contracts at module boundaries to reduce overhead, intra-module calls can be fast at the cost of losing the blame-carrying wrappers), but I don't see how they require w or bar to bear singular responsibility. Multimethods really are multiple functions inhabiting bar, dispatched by a "most specific argument type" matching algorithm, which can fail but when it succeeds, invokes a single function. That's the responsible function, whatever bar multimethod it is. ;-) > I was not arguing that w.foo(x) is always better than bar(y,z). > Rather, I was saying that currently neither violate locality of > responsibility. In these two expressions, w and bar are the > respective responsible parties. Ok, that's what I thought. Now consider y % z where % is defined as in Christian's proposal, amended by Allen's suggestion that the spec not rely on non-internal, prototype-delegated properties. I'll use obj[[internalId]] to denote internal property access and ^^ for intersection: let lst :List = y[["this%"]] ^^ z[["%this"]]; if (lst.length == 0) throw TypeError("% not defined on operands"); if (lst.length != 1) throw TypeError("ambiguous % operation"); return lst[0](y, z); What party is responsible here, given that the only way any pair of internal properties of the form y[["this%"]] and z[["%this"]] get defined (on constructor prototypes, per Allen's followup) is by Function.defineOperator being called with both y and z's "types" (constructors). So (to repeat a point from my last post) we don't actually need (or want) internal instance properties to keep book -- we could use a separate lexically scoped two-dimensional mapping for the % operator indexed by "types" (see instanceof). If the only observable difference between using internal instance properties and using a separate lexical operator matrix is that you claim Object.freeze(Point.prototype) should cause Function.defineOperator('+', Point, Number, pointPlusNumber); or similar calls passing Point fail, then I side with Allen where he wrote: "I wouldn't want to describe or specify these per object (or per prototype) operator lists as properties. Too much baggage comes with that concept. It'd just make them another internal characteristic of objects. As already mentioned, if you think of them this way there shouldn't be any interactions with Object.freeze or any other existing semantics other than whatever we might choose to explicitly define." But that's no surprise, because I'm in favor of letting Carol define multimethods for types purveyed by Alice and Bob, where Alice and Bob didn't foresee the combination Carol found useful. I don't see how this is "irresponsible". If Alice and Bob freeze prototypes, Carol could still hack wrappers, in the absence of multimethods. If Alice and Bob don't want to provide their types to Carol, they can hide them otherwise. Hiding != Freezing. > Were we to adopt multimethods, where atan2 somehow gets enhanced by > all imported modules defining overloadings of atan2 in that scope, > what is the value of the atan2 variable in that scope? A multimethod. > If it is a function composed of all lexical contributors, what > happens when this function is passed as a value and then used in a > different scope? Multimethods [*] are first-class values, sets of associated type- specific statically scoped functions. No dynamic scope, of course. You define a multimethod, let's say with new syntax or new Function.defineMultimethod or whatever strawman API you like (trade- offs about early error checking left for later discussion). So atan2 is not just a "good old function". It's something new, an overloaded function that "has" multimethods (the ES4 proposal, FWIW, had new syntax like |generic function atan2(x, y);| to get things started). (I don't mean to belabor this point, but I do want to stress that plain old functions can't be confused for multimethods and unintentionally mixed together. JS replaces any previous binding when processing a function declaration. This behavior would of course continue, but if we add multimethods, then we have the choice to make it an error to define a multimethod whose name is already bound to any value, or to a plain old function, or whatever seems best.) Anyway, code with access to the distinguished multimethod's lexical binding then would add functions to it by denoting it in that scope or using a reference to it passed to another function which could add functions to the multimethod. This is observably equivalent as far as I can tell to how Christian's Function.defineOperator would seem to work -- except of course that Function.defineOperator mutates internal "this+" and "+this" properties given a first argument of '+', whereas a Function.addMultimethod(atan2, Complex, Rational, function (c, r) {...}) API would take an explicit reference to the multimethod being augmented. (EIBTI! :-P) Finally, anyone with a reference (found in the lexical scope as the basis case, or passed to other functions to introduce them to the atan2 multimethod) can call atan2 on the several combinations of argument types defined in it. Dispatch works as in Dylan or Cecil, if we want to get fancy with subtypes; or simply as in Christian's proposal (zero inheritance FTW ;-). Short on time, so I should stop here and ask what seems unclear or wrong. /be [*] The "method" name may mislead, but Java static methods don't have a distinguished receiver, so we can cope. I'm using the "multimethod" name instead of "generic method" or "generic function" -- generic methods or generic functions cause confusion with "generics", and with plain old JS functions that make few assumptions about argument types. _______________________________________________ es-discuss mailing list es-discuss@... https://mail.mozilla.org/listinfo/es-discuss |
| < Prev | 1 - 2 - 3 | Next > |
| Free embeddable forum powered by Nabble | Forum Help |