Antti Sykäri
| In D, Pavel Minayev <evilone@omen.ru> wrote:
> But out parameters are a bit more than that - don't forget that you can overload functions based on types of their parameters, and not by return type! For example, in stream.d, I wrote:
>
>void read(out byte x);
>void read(out short x);
>void read(out int x);
That looks nice, and might be the right way to solve the problem (which problem, that I will address shortly).
But, as declaring
void read(out byte x);
is practically the same as declaring
byte read();
- the difference is merely syntactical - then why not extend the idea of overloading functions to also cover the case of overloading them by the return type? The intention of the above-mention code would not change if it would be written:
byte read();
short read();
int read();
void f()
{
int x;
x = read();
}
Only it would mean that the type of return value of "read()", and therefore the function to be called, would have to be determined by analyzing its context - which might have unexpected consequences. Think about that.
Also, "out" parameters make it impossible to ignore a return value, since we need a typed lvalue parameter as an argument to determine which overloaded function to call. That might, or might not, be a good idea.
The core of the problem at hand is ultimately that we want to avoid redundancy in the code. Consider (C example):
byte read_byte();
short read_short();
void redundancy_problem()
{ byte b;
b = read_byte();
}
Now assume you want to change the byte to short. You have to change the function call into a short, too. That's not fun, especially if the function is a long one and you can't see all references. Besides i
So, in a modern language, we have to get rid of this kind of behavior.
Well, in C++ we can do that:
void read(byte& b);
void read(short& b);
void redundancy_solution()
{
byte b; short s;
read(b); // calls read(byte&)
read(s); // calls read(short&)
}
So far, so good. But we can do that also if we had overloading by return value types, or overloading by "out" types. (Which, the syntax set aside, are practically the same. Except you can ignore the return value(s).)
void solution_by_overloading_return_values()
{ byte b;
b = read(); // byte read() is called
}
void solution_by_overloading_out_value()
{ byte b;
read(b); // read(out byte b) is called
}
Finally let's see two interesting alternatives to achieve the non-redundancy:
1. Type inference (notice how I'm trying to smuggle features commonly seen in functional languages into D? No, I'm really not. This is just an example. :-):
void type_inference_example()
{ b; // type of b will be inferred soon enough... but it is not
// too visible from the source code and seem like trickery
b = read_byte();
b = read_short(); // error: cannot infer common supertype for b
// .. or actually, if short _is_ considered
// a supertype for b, we actually can!
}
2. Or, we could have the "C++ with typeof" solution:
void wicked_cplusplus_like_example()
{ byte b;
b = read<typeof(b)>(); // yechh
}
No comments on that one.
Actually, type inference might be nice; consider
class Base { }
class Derived : Base { }
class AnotherDerived : Base { }
class Foobar { }
void type_inference_example_on_steroids(char[][2] obj_type)
{
// no type presented, leave it to the compiler to decide
object0;
object1;
// these two cause object0 to have type Base
switch (object_to_make[0])
{
case "derived":
object0 = new Derived(); break;
case "anotherderived":
object0 = new anotherDerived(); break;
}
// and these two cause object1 to be of type Object
switch (object_to_make[1])
{
case "derived":
object1 = new Derived;
case "foobar":
object1 = new Foobar; // uh-oh... not much in common
}
}
Actually, type inference might not be nice. At least not for me, since I want to know the type of the declared variable, and I want it to read right there in the code. I don't mind if, for example, a syntax-directed editor does the work for me (although it would have to me more like a semantics-directed editor): when I for example change the Foobar there to be Derived, it could inform me that "Your Object object1 now doesn't need to be Object any more, it should be Derived, so I made the change for you" or something like that. But anyway, this kind of work wouldn't be suitable to be done behind the curtains.
>=3E Now, if you want to return nothing, just use:
>=3E
>=3E f();
>
> This makes it harder to parse. Also, when I look at it, my mind sees a function call, and not a declaration.
Was this ("harder to parse") the original reason why the "void" asymmetry (requiring void in return values but not arguments) wasn't dumped in C++? Can't remember, don't have a copy of "The Design and Evolution of C++" at hand, so can't check either. Stroustrup is good at defending the choices he made when designing C++. (There actually are a bunch of principles buried under it. *g*)
Still:
In my opinion, too many language features are crippled by the seemingly well-meaning pursuit of trying to make the language easy to parse.
Consider the template syntax of C++: you have to say, for example, set<vector<char> > instead of the _far_ more natural set<vector<char>>, because someone decided that we must build lexical analyzers upon the glorious principle of always returning the longest possible match. Rat's ass I think. I think that lot of C++ code would simply look a lot better if, in the design phase, it had been decided to recognize ">>" as two ">" tokens if we're inside <> brackets.
And I think that nobody's code would be broken even if we started to adhere to the new rule right now. How often do you use ">>" inside a template argument list? Theoretically possible that you'd want to do something like "const int n; set< vec < float, 32>>n > > funny;". But, hey, dream on. Besides you could circumvent that by defining a new constant before the set<float<>> usage, and it'd be much clearer. So, Including a rather simple special case in the lexer would've made the language look and feel better. Imagine!
(It would've been simple -- as simple as making the /* */ comments nested, actually. A language, to be successful, must be cool.)
And, in my opinion, look and feel of the language matter much more than the ease of parsing. Sometimes the two goals do not conflict. In fact, most of the time they don't - which deceitfully makes people associate a "easily readable language" with "easily parsable language". But when they do conflict, I'd opt for the better look and feel since the language has far more users than it has implementers.
(And of course, function calls don't happen in the same places as
function declarations. (At least as long as you cannot declare functions
inside functions.) So it might not be that big an issue.)
Finally, well, look and feel are a matter of opinion. But they are also always a fresh topic for a debate, since successful opinions are usually those that change :-)
>"void" can be treated as "procedure" in other languages, so I don't
> really see a problem here.
By the way, something I really don't understand is that some languages
actually have different keywords for procedures (which just execute
code) and functions (which return a value). It's just silly.
Maybe what I'm looking for here is a uniform interface - something that could be (ab)used in metaprograms. Something along these lines:
First, a function f must have has the following parameters:
f.returnTuple - a tuple type of values it returns
f.argTuple - a tuple type of values it gets as an argument
// please ignore the invented ad-hoc syntax and concentrate on the idea:
template<function F>
F.returnTuple
printingFunctionWrapper(
F fun,
F.argTuple arguments) // this could be replaced with the "rest"
{ // keyword proposed earlier
print("I'm calling function ", F.name, " with arguments ");
printTuple(arguments);
// do the actual function call, store return values
F.returnValues retVals = fun(arguments);
print(F.name, " returned: ");
printTuple(retVals);
return retVals;
}
As it might be obvious, this function is a wrapper that emulates normal functions but logs their arguments and return values. printTuple is a meta function which generates code which prints its arguments, their names and their types. For example:
int squared_x square(int x) = { return x*x; }
main()
{
printingFunctionWrapper(square, 5);
}
says:
I'm calling function square with arguments int x = 5
square returned: int squared_x = 25
Now, I admit that implementing this and refining the idea might not be an easy task...
>=3E private int inline max(int[] array, int maxSoFar, uint idx)
>=3E {
>=3E if (idx == array.length)
>=3E maxSoFar;
>=3E else
>=3E max(array, max(array[i], maxSoFar), idx + 1);
>
> Yes, and then D will become a functional language...
> No, thanks! =)
Well, what I'm proposing here might not look like the traditional C syntax - but on the semantic level it would be exactly the same as the equivalent C code which 1) creates a nameless temporary value; 2) generates code which assigns to the temporary value; 3) returns (or yields) the temporary value at the end.
(I think that the functional form might be easier to optimize. I can't say - haven't written an optimizing compiler yet.)
Another nice-to-have functional-like thing would be
- lambda functions (there is a need for it in some form in any language,
even if would be implemented only as syntactical sugar for introducing
a nameless global function and yielding its address)
Let expressions or similar, on the other hand, wouldn't be needed.
Already C has a form of let expressions; the following block structure
is equivalent to (let ((x 5) (y 6)) (do_something)):
{
int x = 5;
int y = 6;
do_something();
}
Antti.
(By the way, Pavel, I found it necessary to do some search & replace on the quoted text, since my newsreader (slrn) shows your articles like this, apparently not recognizing the character set and escaping each punctuation mark:
>=09void read=28out short x=29=3B
>=09void read=28out int x=29=3B
>=09=2E=2E=2E
Not nice :( If anyone knows a cure for this, I'd appreciate that.)
|