4

For quite a long time now, I have been using a calling convention from C++ google style guide, which boils down to the following: "[for a function] arguments are values or const references while output arguments are pointers"

The reason I like it so much is that at the point of call you can clearly see whether an argument is going to be modified or not, for example:

// Defined in a.cpp:
void transmogrify(const & Foo foo, Bar * bar) { /* do something */ }

// Used in b.cpp:
Foo foo;
Bar bar;
transmogrify(foo, &bar);

Seeing an address-of operator in the call of transmogrify tells me that bar is going to be modified. This is A Good Thing: the flow of data transformation is now more obvious -- foo is an input argument and bar is an output. Moreover I do not need to go to the definition of transmogify to understand it. However, the bar can be an in-out argument, which is not clear from the call spot. Another problem is that it is a convention and the check is not enforced by compiler -- consider some refactoring that changes transmogrify in a way that bar is no longer modified -- the C++ compiler won't tell you that passing by pointer is not needed any more, despite this information is available at compile time.

I would like to know if there is any (non functional) language that demands specifying the argument passing convention at the point of call (not the point of definition like ).

3
  • 3
    C# requires ref or out when you pass by reference. (But you can still pass references by-value without annotations, the benefit is rather limited) Mar 21, 2016 at 11:42
  • As an aside, many C++ programmers view the google C++ coding style with respect to pass by reference as the "Antithesis of a Good Thing." Mar 21, 2016 at 16:41
  • Swift requires that in/out arguments use the & operator. So the calls to f (x: Int) and f (inout x: Int) would look different.
    – gnasher729
    Jun 1, 2022 at 14:38

1 Answer 1

6

In C♯, pass-by-value is the default, but pass-by-reference is also available, and has to be explicitly annotated at both the declaration site and the call-site:

void transmogrify(Foo foo, ref Bar bar) { /* do something */ }

Foo foo;
Bar bar = new Bar();
transmogrify(foo, ref bar);

C♯ even distinguishes between two different kinds of pass-by-reference: ref and out. out parameters, as the name implies, are intended specifically for returning values: the caller is not required to initialize them first, and the callee must initialize it in all possible paths through the method (and it is a compile error if it doesn't). The callee is also not allowed to dereference it. refs OTOH must be initialized by the caller before calling the method.

Note that this only applies to the variable binding: normal pass-by-value parameters cannot modify variable bindings in the caller's scope, ref and out parameters can. However, C♯ also distinguishes between value types and reference types. Value types are copied when passed, but for reference types only the pointer is copied. So, you can have an "input" parameter of a reference type, and while you cannot change the variable binding in the caller's scope, you can mutate the object the variable binding points to.

This is still pass-by-value (sometimes called call-by-object-sharing, call-by-sharing, or call-by-object).

So, you have:

  • no annotation: pass-by-value
  • ref: pass-by-reference, both directions
  • out: pass-by-reference, only output

and

  • value types: a copy of the value is passed directly
  • reference types: a copy of the pointer to the shared value is passed

And all 6 combinations are legal.

The closest thing you are looking for, is to restrict yourself to using only value types, and only pass-by-value or out. That way, you can be sure that your inputs will never be modified (because they are copied), and that your by-reference parameters will only be returned (because you don't even have to initialize them, there is nothing to change anyway).

Note that in modern languages, there are often alternatives to returning multiple values via pass-by-reference.

One often used pattern for returning multiple values is that one value denotes success or failure and the other value is the actual return value, which is only set when the method succeeds. In a language which supports parametric polymorphism aka generics or templates, this can be much better expressed with an Option<T> type. An Option<T> is a type that represents a value of type T that may or may not be present. It is kind-of like a collection that can only have 0 or 1 element. It is also a monad. If you have Option<T> implement your language's standard collection and monad APIs, it can be very nicely and naturally used. Typical usage looks like this:

var maybeInt = int.TryParse("23");

var myInt = maybeInt.Get // unsafe, may throw an exception!

if (maybeInt.isPresent) maybeInt.Get // safe

var myInt = maybeInt.GetOrElse(0) // safe, provides default for missing value

foreach (var i in maybeInt) // using the collection interface
{
    // do something with i
    // will only be executed if value is present
}

// using the monad interface
from i in maybeInt select /* do something with i */

Another common pattern for returning multiple values is that one value denotes an error code and the other value is the actual return value, which is only set when the method succeeds. This allows to pass more information than a simple Option<T> which can only encode presence or absence. In a language which supports parametric polymorphism aka generics or templates, this can be much better expressed with an Error<T> type (also sometimes called Try<T>). An Error<T> is a type that represents either a value of type T that may or may not be present or an error code value. It is kind-of like a pair of collections that each can only have 0 or 1 element and exactly one of the two collections has 1 element, the other has 0. You can also a make it a monad, by biasing it to consider only the "value" collection. If you have Error<T> implement your language's standard collection and monad APIs, it can be very nicely and naturally used. Normally, if you don't care about the exact error, you can use it exactly like Option<T>. It will execute foreach or Select if the computation was a success and ignore it if it wasn't. There is an unsafe Get and a safe GetOrElse, there is isSuccess for checking.

var intOrError = int.TryParse("23");

var myInt = intOrError.Get // unsafe, may throw an exception!

if (intOrError.isSuccess) intOrError.Get // safe

var myInt = intOrError.GetOrElse(0) // safe, provides default in case of failure

foreach (var i in intOrError) // using the collection interface
{
    // do something with i
    // will only be executed if computation succeeded
}

// using the monad interface
from i in intOrError select /* do something with i */

if (intOrError.isFailure) intOrError.GetError // inspect the error

If the language has exceptions, it is also possible to model an error code return value with an exception instead. It depends on whether failure is a normal occurrence (in that case use Option or Error) or is a truly exceptional situation that should not happen, then throw an exception.

For cases where none of the above applies, and you really truly need to return multiple unrelated values, you can always wrap those values into a tuple, record, struct, collection, or object. Many languages have lightweight syntax for that. In future versions of C♯, we will very likely have lightweight syntax for tuples and records, and a limited form of pattern matching for variable assignments, so the need to use pass-by-reference to return more than one value will be greatly reduced. E.g., the current TryParse method which currently looks like this:

int i;
bool success;

success = int.TryParse("42", i);

if (success) { /* do something with i */ }

could be replaced by

var (success, i) = int.TryParse("42");

and within the implementation of TryParse, there would be a statement like

return (success, i);
5
  • The problem with this suggestion is that it makes the usage of the "improved" TryParse more difficult. Right now, you can say if (int.TryParse(myString, intValue)) all in one line. With the new version, it would take two lines, one to do the assignment and unpacking, and one to do the if statement. Mar 21, 2016 at 12:19
  • 1
    TryParse is a bad example, but it's the only example of a BCL method with out params I could think of. It should be Option<int> TryParse(string str) and then you'd have something like int.TryParse("23").Select(i => { /* do something with i */ }), provided that Option implements the standard collection / monad methods. Mar 21, 2016 at 13:00
  • Why would it? Option isn't a collection. Select is for sequences, whereas Option is a single nullable value. Mar 21, 2016 at 13:40
  • An Option is (or can be interpreted as) a collection with a maximum length of 1. Select/SelectMany is not for sequences, it is for monads (of which sequences are just one of many examples, DB queries and observers are some other examples of monads that are shipped in the BCL). Option is a monad. You can name the method different if you want (e.g. performIfPresent), but naming it Select is a) mathematically and conceptually correct, b) is intuitively familiar to anyone familiar with LINQ, c) allows Option to be used with .NET's monad comprehensions aka LINQ query syntax, which … Mar 21, 2016 at 13:51
  • … gets really nice if you start chaining and nesting operations that might fail. Then you can just say something like var deeplyNestedComputation = from i in computation1() select from j in computation2(i) from k in computation3(j) select k; or something like that. (My C♯ is rusty.) Mar 21, 2016 at 13:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.