
Ask HN: Why Are Conditionals Important? - q_eng_anon
Quantum Computer Engineer here: Studied Computer Engineering in school (undergrad), now I&#x27;m working on quantum computers for an unnamed company. I thought I understood computers (having worked -albeit shallowly- on literally every level from silicon manufacturing to web-development) until I started working on quantum computers...<p>I&#x27;d like to get some varied perspectives (theoretical or otherwise) on why conditionals (i.e. the ability to write &#x27;if -something- do -this- else do -that-&#x27;) are important to classical computer architectures&#x2F;languages. Clearly they&#x27;re important - something to do with the fact that without them your machine isn&#x27;t actually Turing-complete. Clearly there&#x27;s something about them that differentiates them from other operations - see the limits imposed by branch operations in pipelining. Perhaps I just haven&#x27;t found the correct description of why they are important yet - or perhaps I am just stupid (very likely) and am incapable of understanding.<p>Input welcome, references appreciated.
======
tonyg
Conditionals allow a program to _react_ to the data it is presented with. This
can be data from the outside world, or data from some other module internal to
a program composed out of modules (i.e. subroutines, for example). Without
conditionals - or their moral equivalent - programs would not be able to react
to their inputs or to changes in their internal state.

\--

There are Turing-complete languages without conditionals, though - even
without data, as such! An example is the lambda calculus. If you want to learn
more about this, read up on "Church encoding" and "Scott encoding".

The basic idea is to use functions and the visitor pattern to encode a kind of
conditional behaviour that varies with the input supplied to it.

    
    
      True = (lambda (t f) (t))
      False = (lambda (t f) (f))
      If = (lambda (b tr fa) (b tr fa))
    
      (lambda (x)
        (If x
            (lambda () (display "It was true!"))
            (lambda () (display "It was false!"))))
    

Desk check the reductions of applying that function to True and to False. The
"t" and "f" arguments _to the boolean values_ act as a kind of "menu" (the
visitor pattern), from which the datum selects in order to communicate what
variant of data it is. The pattern extends readily to natural numbers, lists,
and so on, giving a straightforward encoding of not only conditionals, but
also (one-layer-deep) pattern matching.

~~~
q_eng_anon
this is interesting - thanks for your response.

I'm having a hard time understanding the program you supplied though, the
subsequent description did not really help (probs more my fault than your
own). I certainly understand the visitor pattern (and have had a sneaky
suspicion it is important) but the lambda calculus, not so much.

What type of syntax is the program you supplied in - is it more of a
pseudocode or an actual formal language?

thanks for the search keywords - definitely helped - if you have a favorite
textbook/arxiv reference/author on this stuff, those would be greatly
appreciated as well.

~~~
cardiffspaceman
The syntax is concise definition of named values where the values are all
anonymous functions.

True = (lambda (t f) (t))

\-- Let True be the function that takes two parameters t and f and returns the
first, t. \--

False = (lambda (t f) (f))

\-- Let False be the function that takes two parameters t and f and returns
the second, f. \--

If = (lambda (b tr fa) (b tr fa))

\-- Let If be the function that takes three parameters, and applies the first
parameter b to the second and third parameters. \--

Notice that if the parameter b is the True value above, 'b tr fa' will return
'tr'. If 'b' is False, 'b tr fa' will return 'fa'

    
    
      (lambda (x)
        (If x
            (lambda () (display "It was true!"))
            (lambda () (display "It was false!"))))
    
    

So it follows that this will pass either "It was true!" or "It was false!" to
'display', waving aside all the stuff you have to define that makes 'display'
and STRINGS work as you'd expect.

This is where it becomes clear that you need a lazy system for this to make
sense. In an eager system all three of the parameters to 'If' are evaluated
and then their values are substituted into the body of 'If'. In a lazy system
the expressions for the values are substituted for the parameters in the body,
so only the parameter 'b' and one of the other parameters will be evaluated.
__

When I play around with functional languages I would just write

    
    
      True t f = t
      False t f = f
      If b tr fa = b tr fa
      ShowIt x = If x (display "It was true!") (display "It was false!")
    

You could also write this in (I think this is) Scheme:

    
    
      (define True (t f) (t))
      (define False(t f) (f))
      (define If (b tr fa) (b tr fa))
      (define ShowIt (x) (If (x (display "It was true!") (display "It was false!"))))
    

Except that Scheme isn't usually lazy.

~~~
tonyg
Yes, this is right except that the language as written is eager Scheme. You
don't need a lazy system. The second and third arguments to If are nullary
procedures. Their effects are delayed until invoked by whichever of True or
False is supplied. Scheme's notation for invoking a nullary procedure --
`(t)`, vs `t` which just refers to the procedure -- doesn't help matters here
:-)

------
tlb
There are styles of programming with no conditionals, especially in purely
functional languages.

A common technique inside compilers is to separate out which code gets run,
and which result gets used. If everything is side-effect-free, then there's no
harm in running the code whose result won't get used. In highly parallel
systems, you can just run both versions and select at the end. So you can
rewrite:

    
    
      if foo < 2:
        a = foo
      else
        a = 2
    

to:

    
    
      a = phi(foo<2, 2, foo)
    

(the phi operator is taken from SSA notation. See
[https://llvm.org/docs/LangRef.html#phi-
instruction](https://llvm.org/docs/LangRef.html#phi-instruction) if you're
unfamiliar)

~~~
2_listerine_pls
I don't need to understand that code to tell you there is a control flow
mechanism somewhere using a condition.

------
lacker
Some operations are basic enough that theoretically you can build any other
operation out of them. “NAND” is commonly used as an example but in practice
“nand” doesn’t really map to how people think. Conditionals are another one.
They are a bit more complicated in a sense (three inputs rather than two) but
they map better to how people think so people actually use them in practice.

So Turing completeness is basically two things. 1: the ability to compose an
arbitrary number of conditional statements. 2: the ability to keep computing
recursively, until some output is true.

------
SamReidHughes
Without conditionals, every program either terminates after a pre-specified
amount of computation or never terminates. That leaves you extremely limited
in what you can accomplish (less powerful than a DFA).

A DFA or maybe a 2DFA (which are equally powerful) is basically what you get
when _all_ you have is conditionals.

Without that, what you have is a box that can read finite input and compute a
value. Equivalent to a finite lookup table.

~~~
tlb
It's not as much of a limitation as people think. One pattern is for the
program to update a state every (fixed-length) iteration. You can build any
arbitrary computation on top of that.

~~~
rowanG077
Can you provide an example? I have a hard time believing any arbitrary
computation can be build without conditionals.

~~~
saltcured
I suspect they are thinking about things like cellular automata.

If you squint just right, you may think there are no conditionals. Just
straight-line data flow to compute cell values based on a fixed neighborhood
of the preceding state. A big fluid dynamics simulation has a similar
characteristic.

However, this squinty interpretation can just as simply show you that the
whole thing is just a DFA, as mentioned a few posts up! The finite mutable
register just encodes the name of each state in the state machine, and each
simulation cycle is just computing the next state transition within that large
but finite state space.

~~~
rowanG077
But cellular automata have conditionals. That is the entire point of having a
state in each cell and then doing something based on that state.

~~~
saltcured
I suspect the confusion in this thread really boils down to a lack of
agreement about what "conditional" means. I think some commenters above think
it means conditionally branched instruction streams. They don't consider
arithmetic or logical operators, lookup tables, and demuxers to be
conditionals.

The earlier description of a cycle of functional updates to state sounds like
the classic basis for latched digital circuit design. But, assuming the
commenters were interested in software examples, I brought up analogous
programs for cellular automata and grid simulations.

Also, since the state transition is purely functional with a finite input and
output, it could theoretically be implemented with any functionally equivalent
method, including flat lookup tables. No branching or conditional execution is
required, even though it might be convenient.

~~~
rowanG077
Yeah true but I wouldn't call storing every possible result of a system in for
instance a lookup table a computation. That is just precomputing all the
results and then running a lookup.

If I have a lookup table of all the values of the sine and then use that
lookup table to get the result of sine for specific value I haven't computed
sine, I just looked it up.

~~~
saltcured
I'm not sure if we're actually debating anything at this point...

A cellular automaton or other grid simulation has modularity which means that
the state space for one cell is relatively small. So, you might admit that the
whole thing is doing computation without caring how the cell transition
function is implemented?

But, lookup tables are at one extreme, and there are plenty of other design
techniques for arbitrary functional computation that avoid conditional
branching. At the digital design level, you can think of a continuum of logic
gates where ROMs, mux/demux, or even ALU operations all have elements that can
be seen as lookup-table or computation. The difference is in the observer's
mental model, more so than in actual gate structures.

At the software level, many instructions can be used for demultiplexing. So,
you can compute two different potential values, e.g. in separate registers,
and select between results without branching. You could use a conditional-
store instruction, or some kind of swizzle/pack instruction, but without any
of that you could compute a binary test result, extend it to a whole register
width, and use bitwise logical operations to combine both registers while
effectively selecting one or the other. The essence of whether it is
computation or conditional selection is in the eye of the beholder.

This is a common way to transform some code paths for vectorization, e.g. to
generate one common SIMD instruction stream that has the effect of performing
different optional computations in each lane, while actually running the same
instructions on all lanes.

------
ktpsns
Conditionals actually destroy information. The way they are written does not
even matter; Consider the evaluation of this functional pseudocode:

    
    
       min(a,b) := if(a < b, a, b)
    

Obviously, the evaluation of this function looses information about either a
or b. This is of course by intention. But this is incompatible to a closed
(quantum) system where information must be preserved.

Interestingly (but I think there is no deeper connection), vectorization uses
masks to avoid branching. Programmatically, one implements then both branches
at the same time, and on a per-data level different operations are performed.
A simple pseudo-numpy example would be

    
    
       B[where(A < B)] = A
       which should actually read component by component, for
       instance, having A and B being matrices,
    
          B_{ij} = A_{ij}  if A_{ij} < B_{ij}
                   B_{ij}  else  ("no-op")
    

One can write long programs following this paradigm which frequently excel at
performance at GPUs and modern day CPUs (which are actually vector machines).

~~~
q_eng_anon
thanks for tying it back into quantum - also for the clarification that the
way you write them doesn't matter - whether that be in terms of if/else,
switch statements or actual CPU-level branch instructions.

------
badrabbit
Are you not approaching it a bit too much from a high level perspective?

From assembly(x86), most conditionals and loops break down to cmp/test and
jmp,jx,jnx type instructions.

Computers need to make decisions based on comparisons of data or program
state. You can abstract decision making however you want (e.g.: switch
statements in place of if/else or usage of goto's) but the computer needs to
test or compare and decide on what instruction to execute based on the result.

Let me invent a new condtional 'alot':

for i in range(0,100): alot i: exit(1)

instead of testing for boolean true/false. My conditional "alot" tests the
result of the evaluation against the value range of the type,if the result is
over half of the value range it executes the conditional statement (exit())

Of course I am being a bit silly but my point is my silly conditional much
like if/else breaks down to comparing and jumping based on evaluation (i.e:
instead of a jz/jnz it will break down to jge)

------
psv1
To simplify, software (generally) does this:

    
    
        input >>> output
    

By definition the output _is conditional_ on the input, at least if the
program works correctly. That's why conditional statements are important -
they express the relationship between input and output.

Isn't this really basic stuff? Or am I missing something?

~~~
gshdg
You can have output that’s conditional on the input without having conditional
statements.

How is the output of the following not conditional upon the input? Are there
any conditional statements in it?

F(x, y) = x + y

~~~
psv1
> You can have output that’s conditional on the input without having
> conditional statements.

Yes, of course you can.

------
raincom
In a trivial way, logical gates (AND, OR, etc) are conditionals. So, we need
these gates to build any logical expressions. And many problems can be reduced
to satisfiability, etc.

------
mcphage
Taking in input, and making decisions based on that input, are what computers
are _for_. And conditionals are the simplest version of "making a decision".

------
AnimalMuppet
Most computer programs are, in practice, _difference amplifiers_. Conditionals
are how they achieve that.

("Most" because you have programs like image classifiers that are trying to be
_similarity amplifiers_. That turns out to be hard to do with the standard
building blocks, which want to be difference amplifiers. Perhaps different
building blocks would be more useful.)

~~~
q_eng_anon
thought you were going to tie it back into transistors for a second there...
difference amplifiers are, after all, one of the simplest circuits out there.
Not so sure about similarity amplifiers

------
jensgk
It is possible to approximate logical expressions without conditionals. This
is an old problem in neural networks. See more here:
[https://towardsdatascience.com/emulating-logical-gates-
with-...](https://towardsdatascience.com/emulating-logical-gates-with-a-
neural-network-75c229ec4cc9)

------
Jtsummers
I apologize, but I really am having a hard time understanding your question at
all. Every non-trivial computation requires some kind of conditional.

The first computers were literally people, they computed. There wasn't really
a lot of "conditional" work to what they did, they did _math_ (principally
numerical work) like generating tables of data and compiling the results. They
still needed some conditionals, like if a value was over a certain threshold,
use a particular formula or change some parameter.

The early mechanical computers were largely tabulators of the same sort, but
machines and not people. But as you want to express more complicated
computations, you need the ability to branch. Conditionals are just a means
for executing such a branch. Even the simplest of computational machines (in
the theory sense) like regular expressions and deterministic finite automata
include a branch or conditional element: Given input C while in state S go to
state S'. A simple way to visualize this decision matrix is with a table:

    
    
      Input | S_0 | S_1
      -----------------
        0   | S_0 | S_0
        1   | S_1 | S_1
      Accepting states = {S_0}
    

Is a simple machine that will recognize any binary string ending in '0'. In
each state, a decision is made on what the next state should be based on the
input. This isn't even a Turing complete system, DFAs can only recognize
regular languages. This cannot be done without some sort of conditional
branching capability. You may be able to encode this in a hardware description
language such that the conditional is obscured or obfuscated, but it's still
there.

Even trying to compute taxes or payroll depended on conditionals (income level
determines which tax brackets you pay into, over a certain amount of income
you've maxed out Social Security). And that was one of the earliest
(commercial) uses for computational devices.

Now, the way we express conditionals may change from one language or system to
another. In Erlang, you can remove a lot of `if` expressions using pattern
matching, consider the simple computation of the Fibonacci numbers as an
example:

    
    
      fib(0) -> 1;
      fib(1) -> 1;
      fib(N) -> fib(N-1) + fib(N-2).
    

A conditional is right there: If N = 0, return 1; if N = 1, return 1; else
return fib(N-1)+fib(N-2). Haskell and other functional languages use similar
constructs, and you can include other guards:

    
    
      fib(0) -> 1;
      fib(1) -> 1;
      fib(N) when N > 1, is_integer(N) -> fib(N-1) + fib(N-2).
    

This removes the potential for infinite recursion by adding an additional
guard, now this function is only valid for non-negative, integral inputs. It
has been transformed now to: If N = 0, return 1; if N = 1, return 1; if N > 1
and N is an integer, return fib(N-1)+fib(N-2); else signal an error.

