Multiple return values 
Author Message
 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  
 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  
 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  
 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  
 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  
 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  
 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  
 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:

>Regards,
>Joachim

---

RedRock, Toronto, Canada


Sun, 07 May 2000 03:00:00 GMT  
 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  
 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  
 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  
 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  
 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  
 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  
 
 [ 25 post ]  Go to page: [1] [2]

 Relevant Pages 

1. ???Eiffel idiom for multiple return values???

2. Tuples, iterators, and multiple return values in Eiffel?

3. Multiple return values

4. multiple return values

5. Multiple return values

6. Newbie on multiple return values

7. multiple return values for functions

8. 1st-class method closures (was Re: Multiple return values)

9. C-interface and multiple return values

10. multiple Return values

11. Destructuring / pattern-matching (was: Multiple return values)

12. Destructuring / pattern-matching (was: Multiple return values)

 

 
Powered by phpBB® Forum Software