iif… anonymous expression parameters?

While making the rounds of “compiler magic” functions, I bumped on iif, the ternary operator, which f.i. Prism, VB and other support. Which looks like:

function iif (boolean; expressionIfTrue; expressionIfFalse) : value;

One part of the magic goes to the type-checking, the other part which interests me here, is that in a regular function call, all parameters are evaluated once before the call is made.
For iif, either expressionIfTrue or expressionIfFalse is evaluated, but not both, this means you can have such code as:

v := iif( Assigned(myObject); myObject.VirtualGetMethod; zero );

While with a regular function (without compiler magic), if myObject isn’t assigned, you would get an exception, as myObject.VirtualGetMethod would be invoked regardless. There are obviously countless other uses when the expressions have side-effects.

It occurred to me that in DWS, that “magic” is not only already available to internal “magic functions”, but that it could also be easily offered in the language and made no longer magic at all. It could just be another call convention, in which you wouldn’t pass a value or a reference, but something akin to a “light” anonymous expression parameter.

Would it be worth it?

Such a parameter could be qualified with a uses for instance (to reuse the keyword) rather than a var or const.

function iif( bool : Boolean; uses ifTrue, ifFalse : Variant) : Variant;
begin
   if bool then
      Result := ifTrue
   else Result := ifFalse;
end;

Would declare the iif “magic” function on variants.

Nothing would limit you to invoke a uses expression only once, so f.i.

procedure PrintNFloats(n : Integer; uses needFloat : Float);
begin
   while n>0 do begin
      Print(needFloat);
      Dec(n);
   end;
end;

PrintNFloats(10, Random); // will print 10 different random numbers

And you could use the caller’s capture for side-effects, f.i. by combining a var parameter and a uses expression parameter making use of that variable.

procedure SkipEmpty(var iter: Integer; maxIter: Integer; uses needStr: String);
begin
   while (iterator<=maxIterator) and (needString='') do
      Inc(iterator);
end;
...
SkipEmpty(iter, sl.Count-1, sl[iter]);  // with a TStrings
SkipEmpty(iter, High(tbl), tbl[iter]);  // with an array

Contrary to anonymous functions, the capture is thus explicitly declarative, and also hierarchical only (it’s only valid in functions directly called from your functions). That’s a drastic limitation, so such a syntax isn’t intended for out-of-context tasks (like closures), but for local sub-tasks, which you also guarantee will be local (something that anonymous methods can’t guarantee).

And as a final sample, in the exemple above if you want to equate the ‘hello’ and ‘world’ strings to an empty string for SkipEmpty, you could use:

SkipEmpty(iter, sl.Count-1,
          iif(sl[iter] in ['hello', 'world'], '', sl[iter])
          );

You could thus chain the expression parameters to introduce some non-traditional (for Delphi code) behaviors.

All in all, this could cover a variety of scenarios for default values, conditional alternatives, iterators, with a much restricted capability compared to full-blown anonymous methods, but with hopefully less scope for confusion than anonymous methods offer. But still, it would introduce the possibility of complex side-effects.

Any opinions? Should the possibility be surfaced or be kept only as an internal magic?

Post Scriptum:

As Craig Stuntz & APZ noted in the comments, this is similar to Digital Mars D’s lazy parameters, and both suggested using the “lazy” keyword in place of “uses” to match. However, lazy evokes more delayed evaluation, but evaluated once (as in “lazy binding”, etc.), something D doesn’t seem to support AFAICT with the lazy keyword (every use of a parameter leads to an evaluationif I’m not mistaken). While “uses” was to indicate you could “use” the parmeter’s underlying expression, as many times as you wanted to. More input welcome 🙂

18 thoughts on “iif… anonymous expression parameters?

  1. Ternary operators should be resolved at compile time. That way the compiler can use short-circuit boolean evaluation while changing it to inline code to avoid evaluating the parameters when calling the function. Your way of declaring a parameter type that actually can contain a function of that type that should be evaluated later would make compiler life very hard:

    procedure PrintNFloats(n : Integer; uses needFloat : Double);

    var
    f: Double;

    PrintNFloats(10, Random);
    PrintNFloats(10, 1.0);
    PrintNFloats(10, f);

    How could the compiler use the same code for the procedure PrintNFloats() function? Should it create an “hidden” function returning the constant or variable?

  2. @LDS
    The “uses” call is all about short-circuiting, it’s similar to passing a pointer to a local function evaluating the value of the parameter, with the added twist of properly handling the stack pointer (since the expressions live in the caller’s stack).

    In the case of variables or constants, yes, they have to be wrapped, but in the case of DWS the internals already handle what I describe, it is mostly about exposing the feature (or not).
    From the POV of the expression tree, things are so transparent that the current built-in functions already have that capability (and can actually introduce side-effects bugs if they use one of their parameters multiple times!)

    In a traditional compiler, you would need three standard wrappers, one for constants (trivial), another for variables (trivial too), and a slightly less trivial one for expression (because of the stack manipulations), but basically, if you were to emulate that last one with current Delphi f.i., you would move the expression to a local procedure, and invoke it through a wrapper function to handle the stack pointer changes (you would need basm for that one).
    If done by the compiler, the local procedure & asm wrapper could be merged into one.

  3. No, IfThen in Delphi is a regular function, ie. all its parameters are evaluated all the time.
    iif is different: only the true or false expression is evaluated, but never both.

    You can’t do what I do in my first iff exemple with Delphi’s IfThen, you’ll just get an access violation.

  4. I think that one of Delphi’s IfThen’s greatest limitations is the fact that all the parameters are evaluated at runtime. I like the idea of the extension that allows one to only the parameters being used get evaluated.

  5. I appreciate that you’re trying to not add reserved words/keywords, but “uses” is clear as mud here.

    I would suggest “lazy” instead. Because of the context, it can’t break existing code (i.e., it would be a keyword, not a reserved word).

  6. @APZ
    Lazy would indeed fit D, however as I understand your link, D allows multiple evaluation of a lazy parameter (like my “uses” parameter), however how would you then specify a “true” lazy parameter?
    By that I mean evaluated lazily (when needed), but only once?

    D’s lazy seems to encompass both multiple uses of the expression, and lazy evaluation.

  7. @Craig Stuntz
    Like Digital Mars D’s lazy? APZ suggested it below too, however the lazy seems to encompass both multiple uses of the expression parameter (where my “uses” came), and lazy evaluation (ie. evaluate when needed, but only once per call per parameter).

  8. As IfThen in Math.pas is declared as inline, it does quite well what you describe. The follwing code produces some proper output without any exception raised:

    program Project68;

    {$APPTYPE CONSOLE}

    uses
    SysUtils, Math;

    type
    TMyObject = class
    private
    FValue: Integer;
    public
    constructor Create(AValue: Integer);
    property Value: Integer read FValue write FValue;
    end;

    constructor TMyObject.Create(AValue: Integer);
    begin
    inherited Create;
    FValue := AValue;
    end;

    var
    obj: TMyObject;
    begin
    try
    obj := nil;
    Writeln(IfThen(obj nil, obj.Value, -1));
    obj := TMyObject.Create(42);
    try
    Writeln(IfThen(obj nil, obj.Value, -1));
    finally
    obj.Free;
    end;
    Readln;
    except
    on E: Exception do
    Writeln(E.ClassName, ‘: ‘, E.Message);
    end;
    end.

  9. No, inlining doesn’t protect you from that, try this:

    type
    TMyObject = class function One : Integer; virtual; end;
    function TMyObject.One : Integer; begin Result:=1; end;

    var
    obj : TMyObject;
    begin
    obj:=nil;
    WriteLn(IfThen(obj<>nil, obj.One, 0));
    end;

  10. In your sample, it only works because the compiler recognizes a field direct access (through the property), thus something with no side-effects, and generates special branching code for the inlining. If there are side-effects or functions calls, all arguments of the IfThen will be evaluated.

  11. THis is a neat, clever and interesting idea, which may have it’s uses. But for a ternary operator specifically I would much prefer to see “case” overloaded as a magic function, perhaps using [] rather than () if it is felt necessary/desirable to formally and visually distinguish it from true functions.

    Case is already a reserved word, so no danger of collision with a user defined functions or variables, and the syntax for the case function would be unambiguous with the syntax for the case statement.

    Equally, the purpose is very similar and could be considered (and implemented as) merely a shorthand for the expanded syntax:

    case selector of
    TRUE : value := SomeExpressionA;
    FALSE : value := SomeExpressionB;
    end;
    subject := value;

    vs

    subject := case[selector; SomeExpressionA; SomeExpressionB];

    I think when it’s laid out like that, the similarity and the ease of implementation is pretty obvious. not to mention appealing.

    🙂

  12. I never realized that IfThen isn’t lazy in Delphi.

    Anyway, you can create lazy ifthen easily for input functions without parameters using function pointers:

    TProcPtr = function():string of object;

    function IfThen(const ACondition: Boolean; const AStatement, BStatement: TProcPtr):String;overload;

    function IfThen(const ACondition: Boolean; const AStatement, BStatement: TProcPtr):String;
    begin
    if ACondition then
    begin
    Result := AStatement;
    end
    else
    begin
    Result := BStatement;
    end;
    end;

    Not the perefect solution, but it may be useful in some cases.

  13. @mart
    That however makes the call site for IfThen use far more complex code (execution and readability-wise) than using a regular if-then-else.
    The whole point of the ternary operator is IMHO to make thing simpler than a regular if-then-else.

  14. How exactly does it make calling more complicated? You still call it:
    IfThen(true, FunctionA, FunctionB). You can also add aditional overloaded versions, that use lazy evaluation only if it’s possible:
    function IfThen(const ACondition: Boolean; const AStatement: TProcPtr; BStatement: String):String;overload;
    function IfThen(const ACondition: Boolean; const AStatement: String; BStatement: TProcPtr):String;overload;

    Only downside I see, is that just looking IfThen call doesn’t give you any hint if lazy evaluation is used. So it probably makes sense to create functions with another name like Iif (both sides lazy), IifL (left side lazy), IifR(right side lazy). So you could write:
    IifL( Assigned(myObject); myObject.VirtualGetMethod; zero );

    By the way I don’t say it can replace “compiler magic”, I would really like to see real “ternary operator” and better “case” in Delphi. I actually think that these kind of little things are more important than making new shiny features.

  15. @mart
    I mean complicated because of the FunctionA and FunctionB, which with anonymous methods means at least a “function : Type begin … end”, once you format that, the resulting code is larger and more complex than what a regular if-the-else would have required.
    Or maybe I didn’t understand what you meant?

  16. I agree about anonymous methods. My version is usable only in some cases (if the input functions are regular functions without parameters).

Comments are closed.