12
$\begingroup$

One problem that is hard to make not at least somewhat repetitive is if there is code that needs to be executed in a certain order, and the intermediate code is unconditional but first and last code share a condition.

This results in either:

if (foo) {
    bar();
    baz();
    qux();
} else {
    baz();
}

Or:

if (foo) {
    bar();
}
baz();
if (foo) {
    qux();
}

In the first case the intermediate code is repeated and the latter case the condition expression and if is repeated. The condition is checked twice so the branching cost might be payed twice.

What ways could a language include a DRY method to do this and how could this be optimized in assembly?

$\endgroup$
6
  • 1
    $\begingroup$ Assuming the conditon's value doesn't change? Or should value-change also be handled? $\endgroup$ Commented Jul 14, 2023 at 17:48
  • $\begingroup$ @PabloH Condition's value doesn't change. $\endgroup$ Commented Jul 14, 2023 at 17:49
  • $\begingroup$ "…and how could this be optimized in assembly?" sounds like a completely separate question $\endgroup$ Commented Jul 15, 2023 at 3:18
  • $\begingroup$ if foo: bar(); baz(); qux(); else: baz(). Two baz() seem redundant? Maybe from a human perspective. (In that case you could fool the eye by making a function to call bar, baz, qux and call that instead.) From a computation perspective this is already refactored just fine. $\endgroup$ Commented Jul 17, 2023 at 5:52
  • $\begingroup$ @LukeSawczak Sure, refactoring this to avoid a second function call might be unnecessary, but what if the code in question is not just function calls? $\endgroup$ Commented Jul 17, 2023 at 6:08

7 Answers 7

11
$\begingroup$

It is debatable whether you really need a short grammar for everything. Sometimes the logic is just complex enough that you'll need to write it in the complex way.

But in your case, there is a good reason to have a special grammar. That is, sometimes bar() creates an object and qux() uses the object, and it might be in a type that does work in the constructor, in a language like C++, where using new and delete everywhere in short functions doesn't look good, so you are not supposed to use null-like default values.

In this case, I think you could just invent a new grammar, without considering whether it is short.

The basic logic might be:

some_keyword name = if (foo);
// Or: some_keyword name; if(foo) name;
name {bar();}
baz();
name {qux();}

In the background, of course it is just implemented in the traditional ways. And it doesn't look significantly different. But it could be managed automatically to avoid the possibility of an invalid state.

Or we could change it to make name optional, so you could define something without a name to call its destructor later without referring to it again:

if(foo)
    some_keyword optional_name {bar();}
// Or:
some_keyword optional_name if(foo) {
    bar();
    yield;
}

In the former case, it is in a situation similar to break, that you may want to allow specifying the scope level it binds into. In the later case, it is supposed to be a coroutine, that doesn't exit the scope of if when it is defined. You may also make it support labels to match labels outside. In either case we are making the scope containing bar() live longer than it appears.

Then we could begin adding syntactic sugar:

try {
    if(foo) {
        bar();
        finally {
            qux();
        }
    }
    baz();
}

You could also use it to replace the traditional try. With some improvements, it might work better than the traditional try in a constructor, where you undo what you have already done and ignore what you have not when there is an exception.

Note that it has slightly different meaning from your code. It changes the behavior if baz() throws an exception. You should think of what you actually want in concrete examples. It might be not bad to also have destructors marked as ignored on exceptions.

In case you are not using it in a serious programming language, I think I have seen a language, without remembering its name, where then and else are relatively separate from if and just take the result of the last if. (The Shakespeare Programming Language also works like this, but you won't want to use it as an example.) It might be possible to write code like this, which I don't remember:

if(foo)
then bar();
baz();
then qux();

The down side is it doesn't work well for nested ifs. So programmers often write like this:

if(a) then {
    if(b) then
        c;
    if(true);
}
else
    d;

But I think that could be solved by automatically resetting the if result after then and else blocks, and function calls.

It may not work well if you add more complexity to it, for example stacking more levels of this construct.

And if it is for golfing, you could also just invent an operator for this semantic and don't think too much about the internal logic.

$\endgroup$
10
$\begingroup$

Use the language's existing resource-handling mechanism

Python-style with blocks get you most of the way there. Presumably bar and qux are tied together in some way, so write it as a context manager (or whatever your language has for safely opening and closing resources).

class Resource:
  def __enter__(self):
    bar()
  def __exit__(self, type, value, traceback):
    qux()

Now we can encapsulate the foo part in a conditional context manager

class IfTrue:
  def __init__(self, manager, condition):
    self.manager = manager if condition else None
  def __enter__(self):
    if self.manager:
      self.manager.__enter__()
  def __exit__(self, type, value, traceback):
    if self.manager:
      self.manager.__exit__(type, value, traceback)

Then, when you want to run this setup-teardown code,

with IfTrue(Resource(), foo):
  baz()

This couples the bar and qux parts, which, again, presumably are part of a setup-teardown procedure. The IfTrue context manager is also independent of everything else and can be used with another condition or resource type freely. If you find that this is something your language needs often, IfTrue is a good candidate for standard library inclusion.

You can use whatever tool your language uses for handling resources like files. In Python, that's with. In Ruby, you could just write a method that takes a block (like File.open does). In Java, your resource-allocation object can be AutoCloseable (hence compatible with the try-with-resources statement). In C++, you can use RAII to make the teardown run during your destructor.

Example in C++:

class Resource {
public:
  Resource() {
    bar()
  }
  ~Resource() {
    qux();
  }
};

void example() {
  // Note: std::optional is basically IfTrue already; it
  // runs the deallocation if the thing on the inside exists.
  std::optional<Resource> resource;
  if (foo) {
    resource = Resource();
  }
  baz();
  // resource implicitly goes out of scope and calls qux()...
}

The point is:

  • Decouple the problem into two parts. We have a setup-teardown, and a condition.
  • The setup-teardown pattern is something most languages already have, for dealing with file I/O and other resources. Leverage that.
  • The condition can be added to the resource-management code, making a new resource that's implemented as just "resource plus condition".
$\endgroup$
2
  • $\begingroup$ Or try-with-resource in Java $\endgroup$ Commented Jul 14, 2023 at 19:07
  • 2
    $\begingroup$ Instead of building the conditional logic into a separate context manager, it might make more sense to use something like Python's contextlib.nullcontext: with Resource() if foo else nullcontext():. This avoids constructing a Resource when it's not needed, which is important for some context managers. It also generalizes easily to choosing between more contexts, and means you don't have to remember the argument order for the IfTrue thing. $\endgroup$ Commented Jul 15, 2023 at 19:07
7
$\begingroup$

Clearly, inside our hearts (or at least inside mine), what we all want is a table of code, that more explicitly expresses the relationships among parts of code:

(assuming what you want/need is as-if the condition is evaluated only once)

if (foo)

true false
bar();
baz(); (*)
qux();

(*) imagine that this row has only 1 cell spanning both columns (i.e., rowspan = 2, but cannot do that with stackexchange's markdown, apparently)

Of course for a switch or a match you just add more columns. :-)

$\endgroup$
3
  • $\begingroup$ Welcome to PLDI! Out of interest, what do you think this sort of "table of code" might look like in practice? $\endgroup$ Commented Jul 14, 2023 at 18:16
  • $\begingroup$ @RydwolfPrograms Thanks! Well, options are: horrendous text syntax, IDE support for tables, or programming directly in a spreadsheet... Excel et. al. have "extend cell" to make it span more rows or columns. Or for more precise control, formulas, e.g. "copy contents of relref(0, -1)". Many things you could do with tables... (maintainability unknown...) :-) $\endgroup$ Commented Jul 14, 2023 at 18:24
  • $\begingroup$ @RydwolfPrograms By the way, JetBrains' MPS can do tables inside code, for example. $\endgroup$ Commented Dec 13, 2023 at 13:13
5
$\begingroup$

This is giving me goto flashbacks but here goes:

foo_case: if (foo) {
  bar()
}

baz()

continue foo_case {
  qux()
}

The use of labels and continue allows nesting.

$\endgroup$
5
$\begingroup$

Separate the testing of a condition from the blocks that use the outcome of the test. This is a form of pattern-matching, for matches that should proceed sequentially, and need not be exhaustive.

given (foo) {

    when (true) {
        bar();
    }

    baz();

    when (true) {
        qux();
    }

}

Internally this can be lowered to basic blocks that only test the condition once. For example, in SSA (where parameters are equivalent to phi nodes):

start() {
    t = test(foo);
    if (t) true1(); else always(end);
}

true1() {
    bar();
    always(true2);
}

always(return) {
    baz();
    return();
}

true2() {
    qux();
    end();
}

end() {
}
$\endgroup$
4
$\begingroup$

Similar to the suggestion of Python's with syntax, some functional programming is all you need:

withBarQux = \run -> {
    bar();
    run();
    qux();
};
withNothing = \run -> run();

(foo ? withBarQux : withNothing)(baz);

A programming language can help with this by providing withNothing as a builtin - it is the application operator, or if the pattern is expressed a bit differently, the identity function comes handy as well. Also allowing to pass code blocks to functions without explicit lambda expressions could make the syntax a bit nicer. With Scala's by-name parameters, this could look like

(foo ? withBarQux : withNothing) {
  baz();
}

(which is helpful when the baz() code is not already a function that you can pass).

$\endgroup$
4
$\begingroup$

What ways could a language include a DRY method to do this and how could this be optimized in assembly?

One way is to prefix each statement with something that decides whether to execute it or not. For example, in languages with short-circuiting operators, you could write something like:

foo && bar();
       baz();
foo && quux();

If foo is a long expression and/or has side effects, you could first store its result in a temporary variable:

bool f = foo;
f && bar();
     baz();
f && quux();

In assembly languages there is a direct way to optimize this on some CPU architectures. For example, a lot of 32-bit Arm instructions have some bits to tell whether to execute that instruction or ignore it based on the state of certain processor flags. On x86 you have the conditional move instructions. And in almost all CPU architectures, jump instructions can be conditional. As long as bar() and baz() in your example don't modify processor flags, then only a single compare instruction is necessary to evaluate foo to update the processor flags, and that can then be reused to conditionally execute the rest.

$\endgroup$
1
  • $\begingroup$ I was thinking along this line myself. However even assuming short-circuiting, it might be useful to have some way of mandating that foo() only be evaluated once. $\endgroup$ Commented Jul 16, 2023 at 10:50

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.