Author |
Message |
Roger Brown #1 / 25
|
Multiple return values
Quote: > In the Blue language one can look up a symbol table like this: > found, value := symtab.find("aardvark") > ... it's extremely convenient ...
Suppose you need to print the value of "aardvark" if it exists. Here are two ways to do it, with and without multiple return values: find_and_print is local found: BOOLEAN value: ANY do found, value := symtab.find("aardvark") if found then print(value) end end find_and_print is do symtab.find("aardvark") if symtab.found then print(symtab.value) end end Hmm, the version _without_ multiple return values seems "extremely convenient" to me! Quote: > I wish Eiffel had it.
I can live without it. On the other hand, Blue's enumerated types... Regards, Roger -- -- -- Roger Browne, 6 Bambers Walk, Wesham, PR4 3DG, UK | Ph 01772-687525 -- Everything Eiffel: http://www.*-*-*.com/ | +44-1772-687525
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Bob Hutchis #2 / 25
|
Multiple return values
Quote:
>> In the Blue language one can look up a symbol table like this: >> found, value := symtab.find("aardvark") >> ... it's extremely convenient ... >Suppose you need to print the value of "aardvark" if it exists. Here are two >ways to do it, with and without multiple return values: > find_and_print is > local > found: BOOLEAN > value: ANY > do > found, value := symtab.find("aardvark") > if found then > print(value) > end > end > find_and_print is > do > symtab.find("aardvark") > if symtab.found then > print(symtab.value) > end > end >Hmm, the version _without_ multiple return values seems >"extremely convenient" to me!
Until you introduce concurrency. Quote: >> I wish Eiffel had it. >I can live without it. On the other hand, Blue's enumerated types... >Regards, >Roger >--
Cheers, Bob ---
RedRock, Toronto, Canada
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Karel Th?nisse #3 / 25
|
Multiple return values
Quote:
> > In the Blue language one can look up a symbol table like this: > > found, value := symtab.find("aardvark") > > ... it's extremely convenient ... > Suppose you need to print the value of "aardvark" if it exists. Here are two > ways to do it, with and without multiple return values: > find_and_print is > local > found: BOOLEAN > value: ANY > do > found, value := symtab.find("aardvark") > if found then > print(value) > end > end > find_and_print is
local symtab: SYMTAB -- declaration was missing, or were you using a global symtab? -- that is too different from the solution above, I would say Quote: > do > symtab.find("aardvark") > if symtab.found then > print(symtab.value) > end > end > Hmm, the version _without_ multiple return values seems > "extremely convenient" to me!
Oh yes, this one looks nice enough (even with the declaration added). But keep in mind that all the examples here are implanted in a language that was not designed for multiple return values from the beginning. Adapting Eiffel (or any other oo-language) so that it can return multiple return values is not that difficult, what is needed is an extension to the syntax of function declaration. However, for MRV to pay off, one needs more: multiple assignments and named return parameters (needed in e.g. postconditions). Better even, a strict conformance to functional programming practices. The best thing about MRV is that it allows the use of pure functions with only in-parameters and results, without the use of contrived intermediate types. In non-MRV languages there are two work-arounds: the use of a pure function that returns one object of a composite type, or the use of a procedure with both in-parameters and out-parameters. The last thing is particulary {*filter*} because of aliasing problems that can occur. For example: m, d, r := modDivRem(p, q) -- MRV modDivRem(p, q, m, d, r) -- procedure: what is in and what is out? or even worse: x, y, z := f(x, y, z) -- MRV, no aliasing problems whatsoever f(x, y, z, x, y, z) -- procedural style The aliasing seriously affects the readability and the maintainabilty. It may even involve performance issues, because non-aliased routines are much much simpler to optimise. In the MRV examples it is obvious that there are no aliases in the function call. So the fact that things _can_ be done is not to say that it should be done, or that it delivers clear and maintainable code. For completeness, the same example with split functions and a function that returns a composite type: mdr : MDR_COMPOSITE mdr := modDivRem(p, q) -- composite function: verbosity? m := mdr.mod d := mdr.div r := mdr.rem m := mod(p, q) -- split functions: performance? d := div(p, q) r := rem(p, q) The reading quality it fine, but a lot of double work is done here, unless the optimiser is able to factor out common code. Now that is very hard in this case. Just because mod, div and rem are three different functions here, how should the optimiser know that they had lots of common code? Do we expect the optimiser to analyse any pair of functions this way? The nice thing about MRV-functions is that the optimiser knows that there is common code. So that even if we only use one of the results of the MRV, we can fairly expect the optimiser to be able to factor out: m := modDivRem(p, q).mod d := modDivRem(p, q).div r := modDivRem(p, q).rem Quote: > > I wish Eiffel had it. > I can live without it. On the other hand, Blue's enumerated types...
Can anyone provide a pointer to Blue? I have been searching for it some time ago, but could not find anything. Thanks in advance. -- Groeten, Karel Th?nissen -- mijn e-adres is versleuteld om junk-mailers op het net te verwarren -- verwijder confusion om zo mijn echte adres te verkrijgen -- my e-mail address is scrambled to confuse spammers -- remove the confusion to obtain my true address
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Nick Leato #4 / 25
|
Multiple return values
Quote:
> The best thing about MRV is that it allows the use of pure functions > with only in-parameters and results, without the use of contrived > intermediate types. In non-MRV languages there are two work-arounds: the > use of a pure function that returns one object of a composite type, or > the use of a procedure with both in-parameters and out-parameters. The > last thing is particulary {*filter*} because of aliasing problems that can > occur. For example: > m, d, r := modDivRem(p, q) -- MRV > modDivRem(p, q, m, d, r) -- procedure: what is in and what is out? > or even worse: > x, y, z := f(x, y, z) -- MRV, no aliasing problems whatsoever > f(x, y, z, x, y, z) -- procedural style > The aliasing seriously affects the readability and the maintainabilty. > It may even involve performance issues, because non-aliased routines are > much much simpler to optimise. In the MRV examples it is obvious that > there are no aliases in the function call. So the fact that things _can_ > be done is not to say that it should be done, or that it delivers clear > and maintainable code. For completeness, the same example with split > functions and a function that returns a composite type: > mdr : MDR_COMPOSITE > mdr := modDivRem(p, q) -- composite function: verbosity? > m := mdr.mod > d := mdr.div > r := mdr.rem > m := mod(p, q) -- split functions: performance? > d := div(p, q) > r := rem(p, q) > The reading quality it fine, but a lot of double work is done here, > unless the optimiser is able to factor out common code. Now that is very > hard in this case. Just because mod, div and rem are three different > functions here, how should the optimiser know that they had lots of > common code? Do we expect the optimiser to analyse any pair of functions > this way? The nice thing about MRV-functions is that the optimiser knows > that there is common code. So that even if we only use one of the > results of the MRV, we can fairly expect the optimiser to be able to > factor out: > m := modDivRem(p, q).mod > d := modDivRem(p, q).div > r := modDivRem(p, q).rem
But isn't the solution a little more simple. We have a group of functions where calculating an individual function may be expensive but we can compute the two together for a little more than the cost of computing one. In either case cache the result and checking on the subsequent call if the value is that in the the cache. If it is you have the result partially calculated. Or is the sugestion that the compiler with MRV's will optimise when the values are interleaved in some way? -- Nick
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Marc Wachowi #5 / 25
|
Multiple return values
Of course you can package several values into an object, but defining some class merely serving as container for otherwise unrelated objects which only happen to be returned by some (and perhaps only one) function appears to be overkill, and seems just as absurd to me as only allowing functions with one argument, collecting multiple arguments in one object before the call. The alternative with out-parameters is even worse, as it hides a nice purely functional computation. CQS is an excellent idea - but it's even better if you can restrict commands to those cases where also logically something should be modeled as "possibly changing object". There should be no more (explicit) state involved in returning something than in passing something along as argument. However, there's yet another alternative: introduce first-class functions. You only need one mechanism to pass multiple values from one function to another - and that mechanism is calling a function with multiple arguments. Returning one value is simply an abbreviation for passing a function to which the called function passes its "result" as argument at its end, and which then performs the remaining computation (so-called "continuation"); i.e. with g1(x) = p(x) and g2(x, r) = r(p(x)) we have c(g1(x)) = g2(x, c) but the latter does naturally cover multiple "results" being passed to c: h2(x, r) = r(p(x), q(x)) With nested statically-scoped first-class functions, you're already done; preferably, anonymous function-expressions should be allowed, too. It does also buy you a powerful and elegant form of control abstraction. Admittedly, integrating such a style well with a like Eiffel would be a non-trivial design task, but perhaps worth thinking about. Below is a very simple example what you can easily do in a functional language, using the notation of Scheme (a Lisp dialect): (F A1 ... An) calls function F with arguments Ak (define (F P1 ... Pn) X) defines function F with formal parameters Pi as X (lambda (P1 ... Pn) X) is an anonymous function Example: (define (quotient-and-remainder n d receiver) ; assume that quotient and remainder are computed together (receiver (quotient n d) ; call the receiver with the computed values (remainder n d))) (define (test x y) ; ... do something (quotient-and-remainder (+ x 1) (- y 1) (lambda (q r) ; note that all the statically visible variable bindings, like x and y, ; are accessible here ; ... do something with q and r, e.g. return the sum of all this stuff (+ x y q r))))
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Joachim Durchhol #6 / 25
|
Multiple return values
Quote:
> >Hmm, the version _without_ multiple return values seems > >"extremely convenient" to me! > Until you introduce concurrency.
Not a problem. The symtab object will be automatically locked on entry to find_and_print and released on exit. This might be longer than you like (especially if find_and_print is a large routine that runs for hours), but usually routines are small. And if you know there's a problem, you just write smaller routines. Regards, Joachim -- Please don't send unsolicited ads.
|
Fri, 05 May 2000 03:00:00 GMT |
|
|
Karel Th?nisse #7 / 25
|
Multiple return values
Quote:
> >> x, y, z := f(x, y, z) -- MRV, no aliasing problems whatsoever > >> f(x, y, z, x, y, z) -- procedural style > Instead of throwing around x, y, and z values, shouldn't there be a > POINT_3D class instead? This allows you to consolidate common functions, > so instead of writing:
I didn't have 3D geometry in mind when I wrote this example. I am sorry to have confused you. You are right that for 3D a good abstraction is preferable. Quote: > x1 := x2 - x3; -- subtract point3 from point2 > y1 := y2 - y3; > z1 := z2 - z3; > you write things like: > p2 := f(p2); -- above fuction without MRV > p1 := p2 - p3; -- subtract points > The POINT_3D class is not contrived composite. In this case at least, it > is a matter of finding the proper abstration.
Absolutely! -- Groeten, Karel Th?nissen -- mijn e-adres is versleuteld om junk-mailers op het net te verwarren -- verwijder confusion om zo mijn echte adres te verkrijgen -- my e-mail address is scrambled to confuse spammers -- remove the confusion to obtain my true address
|
Sat, 06 May 2000 03:00:00 GMT |
|
|
Bob Hutchis #8 / 25
|
Multiple return values
Quote:
>> >Hmm, the version _without_ multiple return values seems >> >"extremely convenient" to me! >> Until you introduce concurrency. >Not a problem. The symtab object will be automatically locked on entry >to find_and_print and released on exit. >This might be longer than you like (especially if find_and_print is a >large routine that runs for hours), but usually routines are small. And >if you know there's a problem, you just write smaller routines.
OK. Automatic? I admit to not having read SCOOP in a while, so I'll ask how this is done automatically. What are the rules that say when and how symtab are locked and released? I suppose it would be a good thing if I read about SCOOP again, but maybe to keep the thread continuity, you could try to briefly explain. Aside from that, I would think that this kind of locking of symtab might have the effect of ensuring that threads using symtab run to completion before yielding to any other thread using symtab. This might be contrary to what is desired (especially if it is automatic), considering that for all the compiler knows symtab might do some IO (say went to the disk, or worse, asked the user something). It also seems to lead to a many-small-classes kind of thing... I don't know if I like that either. Cheers, Bob Quote: ---
RedRock, Toronto, Canada
|
Sun, 07 May 2000 03:00:00 GMT |
|
|
James Grav #9 / 25
|
Multiple return values
Quote: >Yes, but: >Frequently what one intends to return is a success flag, to pick a silly >example: > flag, value = increment (value, x) >Now perhaps one would only want to change the value (of value) if the >function were called successfully. The two ways of handling this >currently are 1) exceptions or 2) ad-hoc classes. Both can be annoying >(but, I suppose, so could multiple return values).
There is yet another way to handle such a situation. If throwing an exception would not be appropriate, you could have a "last calculation sucess" flag in that class. So your code would look like this: -- Code fragment 1 a.calculate_result(x, y); if a.result_valid then x := a.last_result_x; y := a.last_result_y; else -- calculation failed, do something else end As I recall, the EiffelMath library does this in several places. With multiple return values, the same code would look like this: -- Code fragment 2 old_x := x; old_y := y; flag, x, y := a.calculate_result(x,y); -- not valid Eiffel! if not flag then -- calculation failed, do something else x := old_x; y := old_y; end Besides, code fragment 1 obeys the command/query separation principal, which is also important for uniform access. Uniform access is very nice, and that gives the developer much flexibility, especially when the implementation of a class needs to be changed. If calculate_result() throws an exception, of course it doesn't have to maintain the class invariant, but if it returns, then it must maintain the invariant. James Graves -- _______________________________________________________________________________ http://www.xnet.com/~ansible <-- 100% free of server push and commercials.
|
Sun, 07 May 2000 03:00:00 GMT |
|
|
Charles Hixso #10 / 25
|
Multiple return values
Yes, but: Frequently what one intends to return is a success flag, to pick a sill example: flag, value = increment (value, x) Now perhaps one would only want to change the value (of value) if the function were called successfully. The two ways of handling this currently are 1) exceptions or 2) ad-hoc classes. Both can be annoying (but, I suppose, so could multiple return values). Quote:
> > >> x, y, z := f(x, y, z) -- MRV, no aliasing problems whatsoever > > >> f(x, y, z, x, y, z) -- procedural style > > Instead of throwing around x, y, and z values, shouldn't there be a > > POINT_3D class instead? This allows you to consolidate common functions, > > so instead of writing: > I didn't have 3D geometry in mind when I wrote this example. I am sorry > to have confused you. You are right that for 3D a good abstraction is > preferable. > > x1 := x2 - x3; -- subtract point3 from point2 > > y1 := y2 - y3; > > z1 := z2 - z3; > > you write things like: > > p2 := f(p2); -- above fuction without MRV > > p1 := p2 - p3; -- subtract points > > The POINT_3D class is not contrived composite. In this case at least, it > > is a matter of finding the proper abstration. > Absolutely! > -- > Groeten, Karel Th?nissen > -- mijn e-adres is versleuteld om junk-mailers op het net te verwarren > -- verwijder confusion om zo mijn echte adres te verkrijgen > -- my e-mail address is scrambled to confuse spammers > -- remove the confusion to obtain my true address
--
|
Sun, 07 May 2000 03:00:00 GMT |
|
|
Joachim Durchhol #11 / 25
|
Multiple return values
Quote:
> >The symtab object will be automatically locked on entry > >to find_and_print and released on exit. > OK. Automatic? I admit to not having read SCOOP in a while, so I'll > ask how this is done automatically. What are the rules that say when > and how symtab are locked and released? I suppose it would be a good > thing if I read about SCOOP again, but maybe to keep the thread > continuity, you could try to briefly explain. > find_and_print (symtab: separate SOME_TABLE_TYPE) is > do > symtab.find("aardvark") > if symtab.found then > print(symtab.value) > end > end
I added the parameter declaration to your code to complete it (please forgive me if I got the syntax wrong, I haven't looked into SCOOP for some time). Now, SCOOP in a nutshell: 1) "separate" entities (parameters or attribute) automatically lock on routine entry. Procedures calls to them are queued, function calls (and, hopefully, attribute accesses) are queued up too but your thread won't be able to continue until that function result is available. 2) If a separate entity is part of a precondition, that precondition mutates from a correctness condition to a wait condition. This dual role of preconditions sounds outrageous at first, but correcness and wait preconditions have one thing in common: the routine cannot run if the precondition isn't fulfilled. Now I hope I got it right and overlooked nothing - I can't double-check it right now (and, besides, I'm not sure wether the version presented in SCOOP is already the final word on the mechanism). Quote: > >This might be longer than you like (especially if find_and_print is a > >large routine that runs for hours), but usually routines are small. > >And if you know there's a problem, you just write smaller routines. > Aside from that, I would think that this kind of locking of symtab > might have the effect of ensuring that threads using symtab run to > completion before yielding to any other thread using symtab.
Not the thread. Just the routine. So put your uninterruptible tasks into short routines. Quote: > It also seems to lead to a many-small-classes kind of thing... I don't > know if I like that either.
Many-small-classes is a typical Eiffel style. I'm under the impression that improving a system of classes typically fragments them into lots of tiny classes, each specifically dedicated to one single aspect of something. I'd like to see a more lightweight syntax for such uses though. Anyway, the Eiffel syntax is still much better than C++; and I don't think Eiffel should be restructured just to squeeze out another line of code per class on the average. (I have to write redundant C++ header files every day - not very productive but necessary.) Regards, Joachim -- Please don't send unsolicited ads.
|
Sun, 07 May 2000 03:00:00 GMT |
|
|
Michael Kollin #12 / 25
|
Multiple return values
In this thread, we have so far seem four alternatives to MRV: 1 - composite return objects 2 - in/out parameters 3 - query of object state 4 - first class functions I would argue that none of them are as nice as multiple return values. Karel Th?nissen argued in his post (very convincingly, in my opinion) against 1 and 2, so I'll concentrate on 3 and 4.
[...] Quote: > However, there's yet another alternative: introduce first-class functions. > You only need one mechanism to pass multiple values from one function to > another - and that mechanism is calling a function with multiple arguments. > Returning one value is simply an abbreviation for passing a function to > which the called function passes its "result" as argument at its end, and > which then performs the remaining computation (so-called "continuation"); > i.e. with g1(x) = p(x) > and g2(x, r) = r(p(x)) > we have c(g1(x)) = g2(x, c) > but the latter does naturally cover multiple "results" being passed to c: > h2(x, r) = r(p(x), q(x))
I would argue that first-order functions do not fit into a statically typed object-oriented language like Eiffel. A function is an intrinsic part of a class. Passing pieces of code around, thus separating code from the data it operates on, breaks the whole concept of a class based system where all code executed are operations performed on objects of that class. To number 3, Bob Hutchison gave the example: Quote: > find_and_print is > do > symtab.find("aardvark") > if symtab.found then > print(symtab.value) > end > end
James Graves gave the example: Quote: > a.calculate_result(x, y); > if a.result_valid then > x := a.last_result_x; > y := a.last_result_y; > else > -- calculation failed, do something else > end
While these examples seem nice at first glance, they have serious problems. Concurrency has already been mentioned. There is another, more fundamental one. This solution depends on side effects caused in another object (symtab in the first, a in the second example). Relying on side effects introduces an extremely strong coupling which is undesirable from a design point of view. From a viewpoint of OOD, this solution is worse than number 1 or 2. An example where problems with this coupling become apparent is this modification of the first example above: find_and_print is do symtab.find("aardvark") if symtab.found then doSomethingElse print(symtab.value) end end "doSomethingElse" is a procedure call. What now, if "doSomethingElse" contains a lookup in symtab? Maybe it is even recursive? (Not unlikely - we are dealing with a symbol table here - maybe this is code from a recursive decent compiler?) In this case, doSomethingElse would change the state of symtab. By the time the print statement is reached, value has changed. Aa wrong value gets printed, all is syntactically correct, all code looks (viewed individually) fine. The problem comes from a larger scale interaction - a debugging nightmare. This nightmare is caused by bad system design. This is why I prefer multiple return values. Michael -- Michael Kolling phone: + 61 3 9903 1033 Dept. of Software Development fax: + 61 3 9903 1077 Monash University
|
Tue, 09 May 2000 03:00:00 GMT |
|
|
Michael Kollin #13 / 25
|
Multiple return values
[...] Quote: > Can anyone provide a pointer to Blue? I have been searching for it some > time ago, but could not find anything. Thanks in advance. > -- > Groeten, Karel Th?nissen
Web sites with information about Blue are at http://www.cs.su.oz.au/~blue/ and http://www.sd.monash.edu.au/blue/ (Both sites are identical - no need to look at both of them.) -- Michael Kolling phone: + 61 3 9903 1033 Dept. of Software Development fax: + 61 3 9903 1077 Monash University
|
Tue, 09 May 2000 03:00:00 GMT |
|
|
Roger Brown #14 / 25
|
Multiple return values
Quote:
> find_and_print is > do > symtab.find("aardvark") > if symtab.found then > doSomethingElse > print(symtab.value) > end > end > "doSomethingElse" is a procedure call. What now, if "doSomethingElse" > contains a lookup in symtab? Maybe it is even recursive? (Not unlikely - > we are dealing with a symbol table here - maybe this is code from a > recursive decent compiler?) > In this case, doSomethingElse would change the state of symtab. By the > time the print statement is reached, value has changed.
In the world of object-oriented programming, objects have state and can be aliased. You do need to be aware of this potential pitfall, both with and without multiple return values. Quote: > ... This is why I prefer multiple return values.
Hmm. Consider this MRV code: find_and_print is local found: BOOLEAN value: ANY do found, value := symtab.find("aardvark") if found then do_something_else print(value) end end What now, if 'do_something_else' holds a reference to the "aardvark" entry? It can change the "aardvark" entry, and your code now holds a reference to the changed value. As you said in your post: Quote: > A wrong value gets printed, all is syntactically correct, > all code looks (viewed individually) fine. The problem comes from > a larger scale interaction - a debugging nightmare.
MRV does not make it possible to code without considering aliasing. So, here's the alias-safe version, first with and then without MRV: find_and_print is local found: BOOLEAN value: ANY do found, value := symtab.find("aardvark") value := clone(value) if found then do_something_else print(value) end end find_and_print is local value: ANY do symtab.find("aardvark") if symtab.found then value := clone(symtab.value) do_something_else print(value) end end The version _without_ MRV is just as safe and one line shorter. Regards, Roger -- -- -- Roger Browne, 6 Bambers Walk, Wesham, PR4 3DG, UK | Ph 01772-687525 -- Everything Eiffel: http://www.eiffel.demon.co.uk/ | +44-1772-687525
|
Tue, 09 May 2000 03:00:00 GMT |
|
|
Page 1 of 2
|
[ 25 post ] |
|
Go to page:
[1]
[2] |
|