We have three boolean operators: NOT, AND, and OR. With these we can build any boolean expression. Sometimes we want to give a name to a computation so we can reuse it.

Consider checking whether a user has full access to a system. Full access means the user can both read and write. We could write canRead && canWrite every time we need this check. If the rule appears in ten places and then changes, we must find and fix all ten. It is better to write the rule once, name it, and compute it through that name.

Or consider NAND, a logic operation used in circuit design. NAND returns true unless both inputs are true. There is no !&& operator in C#. We cannot write a !&& b. If we want NAND, we must build it ourselves.

We need a way to define our own computations and give them names. In C#, the ordinary syntax for this is a method.

Type Signatures in Code

In Chapter 0, we described computations by their shape. We wrote type signatures like not: bool → bool.

C# method syntax puts the returned type first, then the method name, then the parameters:

bool Not(bool x)

Read this header as: “Not computes a boolean. It receives a boolean called x.”

The first bool is the returned type. It tells us what kind of value the method produces.

Not is the method name.

(bool x) is the parameter list. It says the method receives one boolean value and binds that value to the parameter x.

A method with two boolean inputs lists two parameters:

bool And(bool a, bool b)

Read this header as: “And computes a boolean. It receives a boolean called a and a boolean called b.”

Defining a Method

Here is how we define our own NOT operation:

bool Not(bool x)
{
    return !x;
}

This defines a method named Not. Read it piece by piece.

bool before the name is the returned type. This method computes a boolean.

Not is the name we give the computation.

bool x is the parameter. It names the input. When someone uses Not, the value they provide will be bound to x.

{ and } make the method body. The body contains the steps the method runs.

return !x; evaluates !x and returns that value to the caller.

The whole definition translates as: “Define a method named Not that computes a boolean. It receives a boolean called x. The returned value is the result of evaluating not x.”

Why recreate NOT when ! already exists? We are learning the syntax with a familiar operation. Once you see the pattern, you can build computations that do not exist as operators.

Definition. A method is a named, reusable computation that receives input values and may return an output value.

Definition. A parameter is a variable that receives a value when a method is called.

Practice. Translate the following method definition.

bool Identity(bool x)
{
    return x;
}

Write the translation before checking the answer.

Reveal answer

“Define a method named Identity that computes a boolean. It receives a boolean called x. The returned value is the result of evaluating x.”

Compare your answer to the model. Note any differences. Write the corrected version before continuing.

Practice. Write a method definition from this description:

Define a method named Reject that computes a boolean. It receives a boolean called x and returns false, ignoring the input.

Reveal answer
bool Reject(bool x)
{
    return false;
}

Self-correct against the model above.

Calling Methods

When we define a method, we describe its behavior: what it receives, what it computes, and what it returns. To actually use the method, we call it.

bool result = Not(true);

This line calls Not with the value true. We write the method name followed by the argument in parentheses.

Translation: “Create a boolean variable named result and bind the result of computing Not with the value true.”

Definition. An argument is the value passed to a method when calling it.

The parameter is the name that appears in the method definition. The argument is the actual value you supply when calling. When you call Not(true), the argument true gets bound to the parameter x.

Here is what happens when we call Not(true):

  1. Bind the value true to x (the parameter)
  2. Evaluate !x as !true, which produces false
  3. Return false
  4. Bind false to result

The method receives the argument, binds it to the parameter, evaluates the returned expression, and returns the result. Then we bind that result to our variable.

We can visualize this as a box:

        ┌─────────────┐
  x ───►│     !x      │───► result
(true)  │             │    (false)
        └─────────────┘
             Not

The input enters on the left. The computation happens inside. The returned value emerges on the right.

What if we call the method with a variable instead of a literal?

bool flag = true;
bool flipped = Not(flag);

The second line must first evaluate flag to get its value, then pass that value to Not.

Translation: “Evaluate flag. Compute Not with that value. Bind the returned boolean to a variable named flipped.”

after lineflagflipped
1true-
2truefalse

When we wrote bool y = x; earlier, we evaluated x first, then bound the result. Method calls work the same way: evaluate the arguments, then call.

Practice. Translate the following line. Assume Not is already defined.

bool answer = Not(done);
Reveal answer

“Evaluate done. Compute Not with that value. Bind the returned boolean to a variable named answer.”

Practice. Write code for this description. Assume Identity is already defined.

Compute Identity with the value false and bind the returned value to a boolean variable named same.

Reveal answer
bool same = Identity(false);

Self-correct against the models above.

Methods with Multiple Parameters

NOT takes one input. AND and OR take two. We can define methods with multiple parameters.

bool And(bool a, bool b)
{
    return a && b;
}

Read the header first.

bool before the name is the returned type. This method computes a boolean.

And is the name.

(bool a, bool b) lists the parameters. The first argument gets bound to a, and the second gets bound to b.

return a && b; evaluates a && b and returns the result.

Translation: “Define a method named And that computes a boolean. It receives a boolean called a and a boolean called b. The returned value is the result of evaluating a and b.”

To use this method, we call it with two arguments:

bool result = And(true, false);  // result is false

Translation: “Create a boolean variable named result and bind the result of computing And with true and false.”

Visualized:

  a ───►┌─────────────┐
(true)  │   a && b    │───► result
  b ───►│             │    (false)
(false) └─────────────┘
              And

Practice. Translate the following method definition.

bool Or(bool a, bool b)
{
    return a || b;
}
Reveal answer

“Define a method named Or that computes a boolean. It receives a boolean called a and a boolean called b. The returned value is the result of evaluating a or b.”

Practice. Write a method definition from this description:

Define a method named First that computes a boolean. It receives booleans called a and b and returns a.

Reveal answer
bool First(bool a, bool b)
{
    return a;
}

Self-correct against the models above.

Methods That Return Nothing

Not every method produces a value. Some methods perform an action instead.

You have already seen this. Console.WriteLine displays output but returns nothing. We call it for its effect, not for a result.

When a method returns nothing, we say it returns void. Void means “empty” or “nothing.” The method performs an action but produces no value to bind.

void PrintBool(bool x)
{
    Console.WriteLine(x);
}

Translation: “Define a method named PrintBool that returns nothing. It receives a boolean called x and displays it to the console.”

Token by token:

void is the returned type. It says this method returns nothing.

PrintBool is the name.

bool x is the parameter.

Console.WriteLine(x); is the action performed. There is no return because nothing is returned.

We can visualize this as a box with no output:

        ┌─────────────────────┐
  x ───►│ Console.WriteLine(x)│───► (nothing)
(true)  │                     │
        └─────────────────────┘
              PrintBool

To use this method, we call it for its effect:

PrintBool(true);

This displays True. Notice we do not write bool result = PrintBool(true); because there is no result to bind. We call the method for what it does, not for what it produces.

Practice. Translate the following method definition.

void Announce(bool flag)
{
    Console.WriteLine("The value is: " + flag);
}
Reveal answer

“Define a method named Announce that returns nothing. It receives a boolean called flag and displays ‘The value is: ’ followed by the value to the console.”

Practice. Write a method definition from this description:

Define a method named ShowTrue that returns nothing and displays “True” to the console.

Reveal answer
void ShowTrue()
{
    Console.WriteLine("True");
}

Self-correct against the models above.

Building New Operations

Now we can build boolean operations that do not exist as built-in operators. We will use two strategies: composition and branching.

XOR Through Composition

XOR (exclusive or) returns true when exactly one input is true. Here is the truth table:

abXor(a, b)
falsefalsefalse
falsetruetrue
truefalsetrue
truetruefalse

C# does have a ^ operator for booleans, but we will build XOR from NOT, AND, and OR so the method follows the operators we have practiced.

Strategy: describe XOR in English, then translate that description into operators.

XOR means “one or the other, but not both.” That gives us two pieces:

  • “one or the other” means a || b
  • “not both” means !(a && b)
  • “one or the other, but not both” means (a || b) && !(a && b)
bool Xor(bool a, bool b)
{
    return (a || b) && !(a && b);
}

Let’s verify this matches the truth table.

When a is false and b is false:

  • a || b evaluates as false || false, which produces false
  • !(a && b) evaluates as !(false && false), which produces true
  • false && true produces false

When a is false and b is true:

  • a || b evaluates as false || true, which produces true
  • !(a && b) evaluates as !(false && true), which produces true
  • true && true produces true

When a is true and b is false:

  • a || b evaluates as true || false, which produces true
  • !(a && b) evaluates as !(true && false), which produces true
  • true && true produces true

When a is true and b is true:

  • a || b evaluates as true || true, which produces true
  • !(a && b) evaluates as !(true && true), which produces false
  • true && false produces false

The expression matches every row. Composition works by translating a description into operators.

NOR Through Branching

NOR returns true only when both inputs are false. Here is the truth table:

abNor(a, b)
falsefalsetrue
falsetruefalse
truefalsefalse
truetruefalse

Instead of composing operators, we can match the truth table directly using if-statements. For this, we need a method with multiple statements.

bool Nor(bool a, bool b)
{
    if (!a && !b)
    {
        return true;
    }
 
    return false;
}

The curly braces create scopes, just like with if-statements earlier in this chapter. The return keyword ends the method and produces the specified value.

This method checks: are both inputs false? If so, return true. Otherwise, return false.

Let’s verify against the truth table.

When a is false and b is false:

  • !a && !b evaluates as !false && !false, which produces true
  • The method returns true

When a is false and b is true:

  • !a && !b evaluates as !false && !true, which produces false
  • The condition is false, so the method skips to return false

When a is true and b is false:

  • !a && !b evaluates as !true && !false, which produces false
  • The condition is false, so the method skips to return false

When a is true and b is true:

  • !a && !b evaluates as !true && !true, which produces false
  • The condition is false, so the method skips to return false

Branching works by examining cases and returning the appropriate value for each.

Comparing the Strategies

Composition gave us:

bool Xor(bool a, bool b)
{
    return (a || b) && !(a && b);
}

Branching gave us:

bool Nor(bool a, bool b)
{
    if (!a && !b)
    {
        return true;
    }
 
    return false;
}

Composition is often shorter. It works well when you can describe the logic in a phrase, such as “one or the other, but not both.”

Branching mirrors the truth table directly. It works well when the logic is easier to see case by case.

Both strategies produce correct methods. Choose based on which makes the logic clearer.

Later Function Values

This section uses ordinary methods as the first C# syntax for reusable computation. Later, collection methods such as Filter, Map, Fold, and ForEach need a compact way to receive behavior as an argument. At that point, we will introduce C# function-value notation, including Func, Action, and lambda expressions. Until then, define named computations with methods.

Practice: Implement Boolean Operations

Now it’s your turn. For each operation below, implement it using whichever strategy you prefer. Then try the other strategy.

XOR with Branching

You saw XOR implemented with composition. Now implement it with branching.

XOR: true when exactly one input is true.

abXor(a, b)
falsefalsefalse
falsetruetrue
truefalsetrue
truetruefalse

Write a method Xor using if-statements and return.

Reveal answer
bool Xor(bool a, bool b)
{
    if (a && b)
    {
        return false;
    }
 
    if (!a && !b)
    {
        return false;
    }
 
    return true;
}

Or equivalently:

bool Xor(bool a, bool b)
{
    if (a && !b)
    {
        return true;
    }
 
    if (!a && b)
    {
        return true;
    }
 
    return false;
}

Both versions match the truth table.

Self-correct against the model. The exact structure may differ. Verify that your method returns the correct value for all four input combinations.

NOR with Composition

You saw NOR implemented with branching. Now implement it with composition.

NOR: true only when both inputs are false.

abNor(a, b)
falsefalsetrue
falsetruefalse
truefalsefalse
truetruefalse

Think about how to describe NOR in a phrase, then translate that phrase into operators.

Reveal answer

NOR can be described as “not (a or b).” If either input is true, OR returns true, so NOR returns false. Only when both are false does NOR return true.

bool Nor(bool a, bool b)
{
    return !(a || b);
}

Self-correct against the model.

NAND

NAND: true unless both inputs are true.

abNand(a, b)
falsefalsetrue
falsetruetrue
truefalsetrue
truetruefalse

Implement NAND using whichever strategy you prefer. Then implement it using the other strategy.

Reveal answer

Composition: NAND can be described as “not (a and b).”

bool Nand(bool a, bool b)
{
    return !(a && b);
}

Branching:

bool Nand(bool a, bool b)
{
    if (a && b)
    {
        return false;
    }
 
    return true;
}

Self-correct against the models.

Implication

Implication: false only when the first input is true and the second is false. This operation is often read as “a implies b.”

abImplies(a, b)
falsefalsetrue
falsetruetrue
truefalsefalse
truetruetrue

Implement Implies using whichever strategy you prefer. Then implement it using the other strategy.

Reveal answer

Composition: Implication can be described as “if a is true, then b must be true” or equivalently “not a, or b.”

bool Implies(bool a, bool b)
{
    return !a || b;
}

Branching:

bool Implies(bool a, bool b)
{
    if (a && !b)
    {
        return false;
    }
 
    return true;
}

Self-correct against the models.

Review

Before continuing, test yourself on what you’ve learned. Use the protocol from Chapter 0: attempt each exercise from memory, then search this section to check your answers, then note what you missed.

Part 1: Definitions

Write the definitions from memory, then find them in this section to check.

  1. What is a method?
  2. What is a parameter?
  3. What is an argument?
  4. What does void mean?

If any of your answers differed from the definitions in this section, note what you missed and write the corrected version.

Part 2: Method Headers

Write the meaning of each method header.

  1. bool Not(bool x)
  2. bool And(bool a, bool b)
  3. void PrintBool(bool x)
  4. void ShowTrue()

Check your answers against the explanations in this section.

Part 3: Translations

Translate each piece of code to English.

  1. bool Negate(bool x) { return !x; }
  2. bool Both(bool a, bool b) { return a && b; }
  3. void Show(bool x) { Console.WriteLine(x); }
  4. bool result = Negate(flag);

Check your translations against the patterns shown in this section.

Part 4: Writing Code

Write C# code for each description.

  1. Define a method named Always that computes a boolean. It receives a boolean called x and returns true.
  2. Define a method named Second that computes a boolean. It receives booleans called a and b and returns b.
  3. Define a method named Greet that returns nothing and displays “Hello” to the console.
  4. Compute Check with the value false and bind the returned value to a boolean variable named outcome.

Check your code against the examples in this section.

Part 5: Implement Operations

Implement each boolean operation. Write both a composition version and a branching version.

  1. XNOR: returns true when both inputs are the same (both true or both false).
abXnor(a, b)
falsefalsetrue
falsetruefalse
truefalsefalse
truetruetrue
  1. Inhibition: returns true only when the first input is true and the second is false.
abInhibit(a, b)
falsefalsefalse
falsetruefalse
truefalsetrue
truetruefalse

Verify your implementations against the truth tables.


You now know how to define your own boolean computations. You can package logic into named, reusable methods. You have two strategies for building new operations: compose operators into expressions, or branch through cases with if-statements.

Next, we define our own data.