We have three boolean operators: NOT, AND, and OR. With these we can build any boolean expression. But 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 they can both read and write. We could write canRead && canWrite every time we need this check. But if we use this logic in ten places and the rule changes, we must find and fix all ten. We want to write the logic once, name it, and use that name everywhere.
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.
Type Signatures in Code
In Chapter 0, we described computations by their shape. We wrote type signatures like not: bool → bool. C# has its own notation for function types. It uses Func<> with angle brackets.
For a function from bool to bool, we write Func<bool, bool>. The types list in order: inputs first, output last. So Func<bool, bool> means one boolean goes in, one boolean comes out.
For a function from two booleans to a boolean, we write Func<bool, bool, bool>. The first two types are inputs, the last is output.
For a function that takes nothing and returns a boolean, we write Func<bool>. With only one type, that type is the output.
You have seen < and > as comparison operators. Here they serve a different purpose: they hold type information. Treat Func<...> as a single unit describing a function’s shape.
Defining a Function
Here is how we define our own NOT operation:
Func<bool, bool> Not = x => !x;This defines a function named Not. Let’s break it down token by token.
Func<bool, bool> is the type. It says this is a function from bool to bool.
Not is the name we are giving this function.
= works just like with variables. We are binding a value to the name Not.
x is the parameter. It names the input. When someone uses Not, the value they provide will be bound to x.
=> is a new symbol. You have not seen it before. It separates the parameter from what the function computes. Read it as “goes to” or “maps to.”
!x is the returned expression. It computes the result by negating x.
The whole line translates as: “Define a function named Not that takes a boolean x and returns 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 function is a named, reusable computation that takes input values and produces an output value.
Definition. A parameter is a variable that receives a value when a function is called.
Practice. Translate the following function definition.
Func<bool, bool> Identity = x => x;Write the translation before checking the answer.
Reveal answer
“Define a function named Identity that takes a boolean x and returns x.”
Compare your answer to the model. Note any differences. Write the corrected version before continuing.
This follows a pattern you already know. Compare:
bool flag = true;
Func<bool, bool> Not = x => !x;Both lines have the same structure: type, then name, then =, then value. In the first line, the value is true. In the second line, the value is a function.
Practice. Write a function definition from this description:
Define a function named Reject that takes a boolean x and returns false (ignoring the input).
Reveal answer
Func<bool, bool> Reject = x => false;Self-correct against the model above.
Calling Functions
When we define a function, we describe its behavior: what it takes, what it computes, and what it returns. To actually use the function, we call it.
Func<bool, bool> Not = x => !x;
bool result = Not(true);The second line calls Not with the value true. We write the function name followed by the argument in parentheses.
Translation: “Call Not with the value true and bind the returned value to a boolean variable named result.”
Definition. An argument is the value passed to a function when calling it.
The parameter is the name that appears in the function 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):
- Bind the value true to x (the parameter)
- Evaluate
!x→!true→ false - Return false
- Bind false to result
The function 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 output emerges on the right.
What if we call the function with a variable instead of a literal?
Func<bool, bool> Not = x => !x;
bool flag = true;
bool flipped = Not(flag);The third line must first evaluate flag to get its value, then pass that value to Not.
Translation: “Evaluate flag, call Not with that value, and bind the returned value to a boolean variable named flipped.”
| after line | flag | flipped |
|---|---|---|
| 2 | true | — |
| 3 | true | false |
When we wrote bool y = x; earlier, we evaluated x first, then bound the result. Function 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, call Not with that value, and bind the returned value to a boolean variable named answer.”
Practice. Write code for this description. Assume Identity is already defined.
Call 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.
Functions with Multiple Parameters
NOT takes one input. AND and OR take two. We can define functions with multiple parameters.
Func<bool, bool, bool> And = (a, b) => a && b;Let’s break this down.
Func<bool, bool, bool> is the type. Three types are listed: the first two are inputs, the last is the output. This function goes from two booleans to a boolean.
And is the name.
(a, b) lists the parameters. With multiple inputs, we wrap them in parentheses. The first argument gets bound to a, the second to b.
a && b is the returned expression. It uses both parameters.
Translation: “Define a function named And that takes two booleans a and b and returns a and b.”
To use this function, we call it with two arguments:
bool result = And(true, false); // result is falseTranslation: “Call And with true and false and bind the returned value to result.”
Visualized:
a ───►┌─────────────┐
(true) │ a && b │───► result
b ───►│ │ (false)
(false) └─────────────┘
And
Practice. Translate the following function definition.
Func<bool, bool, bool> Or = (a, b) => a || b;Reveal answer
“Define a function named Or that takes two booleans a and b and returns a or b.”
Practice. Write a function definition from this description:
Define a function named First that takes two booleans a and b and returns a.
Reveal answer
Func<bool, bool, bool> First = (a, b) => a;Self-correct against the models above.
Functions with No Parameters
Some functions need no input at all.
Func<bool> AlwaysTrue = () => true;The type Func<bool> has only one type inside the brackets. That type is the output. This function goes from nothing to a boolean.
The empty parentheses () on the left of => mean no parameters.
Translation: “Define a function named AlwaysTrue that takes nothing and returns true.”
To use this function, we call it. We still write parentheses even with no arguments:
bool result = AlwaysTrue(); // result is trueThe parentheses tell C# we are calling the function, not just referring to it by name.
Practice. Write a function definition from this description:
Define a function named AlwaysFalse that takes nothing and returns false.
Reveal answer
Func<bool> AlwaysFalse = () => false;Self-correct against the model above.
Functions That Return Nothing
Not every function produces a value. Some functions 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 function returns nothing, we say it returns void. Void means “empty” or “nothing.” The function performs an action but produces no value to bind.
For void-returning functions, C# uses Action instead of Func. The type Action<bool> means: takes a boolean, returns nothing.
Action<bool> PrintBool = x => Console.WriteLine(x);Translation: “Define a function named PrintBool that takes a boolean x and displays it to the console.”
Token by token:
Action<bool> is the type. It says this function takes a boolean and returns nothing.
PrintBool is the name.
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 function, we call it for its effect:
PrintBool(true);This displays “True” to the console. Notice we do not write bool result = PrintBool(true); because there is no result to bind. We call the function for what it does, not for what it produces.
What about Action with no type parameters? That means a function that takes nothing and returns nothing.
Action SayHello = () => Console.WriteLine("Hello!");Translation: “Define a function named SayHello that takes nothing and displays ‘Hello!’ to the console.”
Practice. Translate the following function definition.
Action<bool> Announce = flag => Console.WriteLine("The value is: " + flag);Reveal answer
“Define a function named Announce that takes a boolean flag and displays ‘The value is: ’ followed by the value to the console.”
Practice. Write a function definition from this description:
Define a function named Greet that takes nothing and displays “Welcome!” to the console.
Reveal answer
Action Greet = () => Console.WriteLine("Welcome!");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:
| a | b | Xor(a, b) |
|---|---|---|
| false | false | false |
| false | true | true |
| true | false | true |
| true | true | false |
C# has no ^ operator for booleans that behaves exactly like XOR (the ^ symbol exists but is rarely used for booleans). We must build it ourselves.
Strategy: describe XOR in English, then translate that description into operators.
XOR means “one or the other, but not both.” Let’s break that into pieces:
- “one or the other” →
a || b - “not both” →
!(a && b) - “one or the other, but not both” →
(a || b) && !(a && b)
Func<bool, bool, bool> Xor = (a, b) => (a || b) && !(a && b);Let’s verify this matches the truth table.
When a is false and b is false:
a || b→false || false→ false!(a && b)→!(false && false)→!false→ truefalse && true→ false ✓
When a is false and b is true:
a || b→false || true→ true!(a && b)→!(false && true)→!false→ truetrue && true→ true ✓
When a is true and b is false:
a || b→true || false→ true!(a && b)→!(true && false)→!false→ truetrue && true→ true ✓
When a is true and b is true:
a || b→true || true→ true!(a && b)→!(true && true)→!true→ falsetrue && false→ false ✓
The expression matches every row. Composition works by translating an English description into operators.
NOR Through Branching
NOR returns true only when both inputs are false. Here is the truth table:
| a | b | Nor(a, b) |
|---|---|---|
| false | false | true |
| false | true | false |
| true | false | false |
| true | true | false |
Instead of composing operators, we can match the truth table directly using if-statements. For this, we need a function with multiple statements.
So far, every function we have written fits on one line. The returned expression follows => directly, like x => !x. For simple computations, this works well. The result of the expression is automatically returned.
For complex logic with multiple steps, we use curly braces and the return keyword:
Func<bool, bool, bool> Nor = (a, b) => {
if (!a && !b) return true;
return false;
};The curly braces create a scope, just like with if-statements. The return keyword ends the function and produces the specified value.
This function 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→!false && !false→true && true→ true- We return true. ✓
When a is false and b is true:
!a && !b→!false && !true→true && false→ false- The condition is false. We skip to
return false. ✓
When a is true and b is false:
!a && !b→!true && !false→false && true→ false- The condition is false. We skip to
return false. ✓
When a is true and b is true:
!a && !b→!true && !true→false && false→ false- The condition is false. We skip to
return false. ✓
Branching works by examining cases and returning the appropriate value for each.
Comparing the Strategies
Composition gave us:
Func<bool, bool, bool> Xor = (a, b) => (a || b) && !(a && b);Branching gave us:
Func<bool, bool, bool> Nor = (a, b) => {
if (!a && !b) return true;
return false;
};Composition is often shorter. It works well when you can describe the logic in a phrase: “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 functions. Choose based on which makes the logic clearer.
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.
| a | b | Xor(a, b) |
|---|---|---|
| false | false | false |
| false | true | true |
| true | false | true |
| true | true | false |
Write a function Xor using if-statements and return.
Reveal answer
Func<bool, bool, bool> Xor = (a, b) => {
if (a && b) return false;
if (!a && !b) return false;
return true;
};Or equivalently:
Func<bool, bool, bool> Xor = (a, 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 function 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.
| a | b | Nor(a, b) |
|---|---|---|
| false | false | true |
| false | true | false |
| true | false | false |
| true | true | false |
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.
Func<bool, bool, bool> Nor = (a, b) => !(a || b);Self-correct against the model.
NAND
NAND: true unless both inputs are true.
| a | b | Nand(a, b) |
|---|---|---|
| false | false | true |
| false | true | true |
| true | false | true |
| true | true | false |
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).”
Func<bool, bool, bool> Nand = (a, b) => !(a && b);Branching:
Func<bool, bool, bool> Nand = (a, 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.”
| a | b | Implies(a, b) |
|---|---|---|
| false | false | true |
| false | true | true |
| true | false | false |
| true | true | true |
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.”
Func<bool, bool, bool> Implies = (a, b) => !a || b;Branching:
Func<bool, bool, bool> Implies = (a, 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.
- What is a function?
- What is a parameter?
- What is an argument?
- 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: Type Notation
Write the meaning of each type signature.
Func<bool, bool>Func<bool, bool, bool>Func<bool>Action<bool>Action
Check your answers against the explanations in this section.
Part 3: Translations
Translate each piece of code to English.
Func<bool, bool> Negate = x => !x;Func<bool, bool, bool> Both = (a, b) => a && b;Func<bool> GetTrue = () => true;Action<bool> Show = x => Console.WriteLine(x);bool result = Negate(flag);
Check your translations against the patterns shown in this section.
Part 4: Writing Code
Write C# code for each description.
- Define a function named Always that takes a boolean x and returns true.
- Define a function named Second that takes two booleans a and b and returns b.
- Define a function named Greet that takes nothing and displays “Hello” to the console.
- Call a function named 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.
- XNOR: returns true when both inputs are the same (both true or both false).
| a | b | Xnor(a, b) |
|---|---|---|
| false | false | true |
| false | true | false |
| true | false | false |
| true | true | true |
- Inhibition: returns true only when the first input is true and the second is false.
| a | b | Inhibit(a, b) |
|---|---|---|
| false | false | false |
| false | true | false |
| true | false | true |
| true | true | false |
Verify your implementations against the truth tables.
You now know how to define your own boolean computations. You can package logic into named, reusable functions. 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.
Previous: Section 3 - Control Flow
Next: Section 5 - Custom Types