Friday, March 07, 2008

Odd corner of Delphi: procedural expressions are not expressions

Here's an odd corner of Delphi: expressions of a callable type (e.g. a function pointer or method pointer) are not fully-fledged expressions; or rather, the expression grammar for function application doesn't acknowledge function pointer types as proper first-class parts of the type system. For example:

type
  TFoo = procedure(x: Integer);
  TBar = function: TFoo;

var
  bar: TBar;
begin
  bar(42); // doesn't compile; too many args for TBar
  (bar)(42); // doesn't compile; bar's value doesn't flow through
  bar()(42); // still no good; bar()'s value doesn't flow through
  (bar())(42); // again, no good, for same reasons as previous two
end.

The reasons are probably directly related to two things: the historical lack of general function pointers in Pascal and the rule that no-argument procedures can omit the parentheses.

The following are valid expressions that call the function pointer, but the resulting value can't be invoked in turn:

  bar; // ok: return value dropped
  bar(); // ok: return value dropped

The above results in some serious drawbacks if one wants to program Delphi in a functional style. Currying can't work, for example: there's no way you could turn foo(1, 2, 3) into foo(1)(2)(3), since the latter syntax isn't valid.

I personally consider it a bug, but this behaviour, at the heart of expression parsing, is tricky to fix while guaranteeing not to break anything that already works. For example, there are other odd semantics around these Delphi function pointers:

type
  TFrob = function: Integer;

function MyFrob: Integer; begin Result := 42; end;

procedure Baz1(x: TFrob); begin end;
procedure Baz2(x: Integer); begin end;

procedure BazO(x: TFrob); overload; begin end;
procedure BazO(x: Integer); overload; begin end;

begin
  Baz1(MyFrob); // passing MyFrob
  Baz2(MyFrob); // calling MyFrob and passing result
  BazO(MyFrob); // guess which?
end.

The above call to Baz1 is currently handled, believe it or not, by parsing as a function call and then later, in the middle of function application, throwing away the call bit and turning it back into taking the address of the function. You can see this by hiding the function call just a tiny bit, so the function application logic can't see it so easily:

  Baz1((MyFrob)); // no longer passing MyFrob

The lesson to me in the above case is pretty clear. If you want to have function pointers in your language, you shouldn't make function application look like a function value - that way madness lies. This is why the '@' operator was invented:

  Baz1((@MyFrob)); // works again; passing MyFrob

Unfortunately, its use in Delphi for these cases is optional.

The two issues discussed in the above post are distinct. However, I may need to fix the first one in order to get certain features into the product...

7 comments:

Anonymous said...

Hi Barry,

please fix it. This is just one of the things the Delphi compiler could be improved.

While it is easier to program omitting things like @ and the occasional ^-dereference-symbol, I dearly believe this is exactly what makes it harder to understand what the compiler does beneath the hood.

If this yet improves your possibilities to bring new features to the compiler, honest to god, go ahead and fix it.

Thanks so much!
Daniel

Anonymous said...

The problem here is that Delphi doesn't force you to specify whether or not you are referring to the function address or return value. For example, the following statements are distinct in C/C++:

x = myfunc;
x = myfunc();

The first instance assigns to x the value of the function address, and the second executes the function and assigns its result. The @-operator was invented to get by this problem, but relaxed to make for prettier syntax when assigning event handlers.

No real way past this AFAIK. If you want to use the constructs you showed, you will have to assign the intermediate value to a function variable.

Barry Kelly said...

cobus,

Yes, that's the gist of what I said. Technically, both of the problems I mentioned can be fixed in the compiler (so it's not quite as bad as "no real way past this"), but the first has a higher priority (and to be frank, a greater utility, IMHO). I will be fixing it for Tiburon, barring something unforeseen in compatibility issues.

Anonymous said...

Barry, mind if bring up another syntactical gripe? When assigning handlers to multicast events, I think it's truly heinous that this should be done via Include() and Exclude(). For some reason I find it offensive that an object needs "help" from outside itself (if you know what I mean) to get the job done.

RemObjects' Chrome uses "+=" or somesuch C-ism which isn't particularly attractive. Why not overload "add" and "remove" so they can be used outside of the class declaration? Then we could do something like

MyObj.OnChange add MyHandler ;
MyObj.OnChange remove MyHandler ;

which I find more aggreeable.

I'll be very pleased if you tell me to RTFM because such a thing already exists.

Cheers,
Mark.

Anonymous said...

Barry,

I like that you say it is fixable (and believe you, of course), but how will you get by the syntactic drawback of not being able to tell whether the code is looking for the function result or address? Or will you just impose forced use of () in function calls for such cases?

For example, suppose I have a function calling a function calling a function and I want the last function's address, we can do this (as you suggested before):

p := f()();

Except that for all other functions, the () syntax is only supported, not required, which means that this should be just as valid:

p := f();

So now we need some rule to say when () is required.

This could give the address of the second function:
p := @(f);

But I don't see how to get to the third from there in a fairly intuitive way using @.

Or perhaps my thinking is squarely within the box and I'm missing something?

Anonymous said...

Cobus,

It seems like quite the minefield doesn't it? It's not obvious to me how the situation can be definitively rectified while maintaining backward compatibility.

I read your statement

p := f()();

as calling the second function rather than obtaining the address of the third function :-)

Enforced use of @ and () helps, but breaks old code.

Anonymous said...

>MyFrob
procedural type has a more high priority than simple types (Integer, ...)
--
Vadim