.NET Boolean type usually makes if
else
pair all over the code.
Functional Boolean should be more like Either
type. Ideally represented as Either<Unit, Unit>
.
However, my issues with Either type are the following:
Either<Unit, Unit>
verbose, ideally it should not be generic type- It is not communicating clearly that
EitherLeft
isfalse
andEitherRight
istrue
There is a library that solves this, here are some examples
TrueOrFalse IsWeekend()
{
var dow = DateTimeOffset.Now.DayOfWeek;
if (dow == DayOfWeek.Saturday || dow == DayOfWeek.Sunday)
{
return new TrueOrFalse.True();
}
return new TrueOrFalse.False();
}
YesNoOrMaybe CanPurchaseAlcohol(int age)
{
if (age < 16)
{
return new YesNoOrMaybe.No();
}
if (age >= 21)
{
return new YesNoOrMaybe.Yes();
}
return new YesNoOrMaybe.Maybe();
}
However, I have issues with this one as well
- I do not want to have 2 similar libraries in my projects solving similar problems.
new TrueOrFalse.True()
seems to me as wasteful, allocating objects every time which in essenceUnit.Default
. I would Prefer something likeTrueOrFalse.True.Default
- language-ext mainly uses structs, where OneOf uses classes. That is why I would like to stick with language-ext
I was thinking to create a specialized type, let's call it Binate (still working on the name) which will be similar to Either<Negative, Positive>
, where Positive
and Negative
are specialized Unit
types.
However, that will mean copying and renaming types from language-ext
My question is, how have you solved this problem in your projects? Which approach would you recommend?
P.S. originally posted here
2 Answers 2
The "functional version" of booleans is the Church Encoding of Booleans in the λ-calculus.
In λ-calculus, the only abstraction mechanism that exists, are (anonymous) functions. So, naturally, every data type and data structure has to be implemented with functions, and this includes booleans. In the Church Encoding, booleans are implemented as a pair of functions of two parameters, where one function evaluates the first argument and throws away the second and the other function evaluates the second argument and throws away the first:
[Note: I am going to use Scala for the sample code, since some of the concepts easily get lost between the syntactic noise of C#. Also, all of the following is runnable code, I have put in links to the online Scala evaluator for every code snippet.]
trait Buul:
def apply[T](a: T, b: T): T
object Tru extends Buul:
override def apply[T](a: T, b: T) = a
object Fls extends Buul:
override def apply[T](a: T, b: T) = b
So, basically Tru
is a function (the apply
method means it can be called like a function) which takes two arguments, a
and b
, returns a
and ignores b
, and Fls
is the reverse.
Now, how the heck is this a boolean? Well, let's play around with it. I'll add a an implicit conversion which will automatically convert Scala's built-in booleans to my newly-defined booleans:
object BuulExtension:
import scala.language.implicitConversions
implicit def boolean2Buul(b: => Boolean): Buul = if b then Tru else Fls
Now, I can call something like:
(2 < 3)("2 is less than 3", "3 is greater than 2")
//=> "2 is less than 3"
(4 < 3)("4 is less than 3", "4 is greater than 3")
//=> "4 is greater than 3"
So, if the conditional (2 < 3)
is Tru
, then the first argument will be returned, if it is Fls
, then the second argument will be returned. In other words: the two boolean functions Tru
and Fls
act like a conditional expression!
By the way, do you notice something else? Our two "data values" also behave like functions. Does that remind you of something? They are objects (in the "OOP" sense of the word). And in fact, this is also exactly how booleans are implemented in object-oriented languages like Smalltalk. It might be surprising to some people that an encoding for λ-calculus from the 1920s turns out to be object-oriented, but it is actually really not surprising at all: the only construct in λ-calculus is the function, so the only abstraction mechanism is using functions. Since OOP is all about behavioral abstraction, it should not be surprising that λ-calculus is kind-of the first OOP language. But I digress.
There is, however, a small problem with our implementation:
(2 < 3)(println("2 is less than 3"), println("3 is greater than 2"))
// 2 is less than 3
// 3 is greater than 2
(4 < 3)(println("4 is less than 3"), println("4 is greater than 3"))
// 4 is less than 3
// 4 is greater than 3
λ-calculus is lazy, meaning, it only evaluates function arguments that are actually used. Scala (and C#, like most mainstream languages) is eager, meaning function arguments are always evaluated before being passed into the function. So, our "if
" always evaluates both the "then"-branch and the "else"-branch – not good! Note: it still only returns the one result, but it evaluates both, so if the branches have side-effects (such as printing to the terminal), then the side-effects of both branches will be executed.
However, there is a simple trick to delay the evaluation of something: wrap it in a function. If we make the two parameters functions, then the side-effects will only be executed when we call the function:
trait Buul:
def apply[T](a: => T)(b: => T): T
object Tru extends Buul:
override def apply[T](a: => T)(b: => T) = a
object Fls extends Buul:
override def apply[T](a: => T)(b: => T) = b
(2 < 3) { println("2 is less than 3") } { println("3 is greater than 2") }
// 2 is less than 3
(4 < 3) { println("4 is less than 3") } { println("4 is greater than 3") }
// 4 is greater than 3
[Here we are using some Scala niceties: in Scala, a method can have more than one parameter list, which is nice because if you have a parameter list with exactly one parameter which is a function, you can then use curly braces to call the method. This makes it look indistinguishable from built-in control structures.]
Nice! We have a working if
!
What about the operators, though? Let's think about how we can implement those:
trait Buul:
def apply[T](a: => T)(b: => T): T
def ∧(other: => Buul): Buul
def ∨(other: => Buul): Buul
def unary_! : Buul
object Tru extends Buul:
override def apply[T](a: => T)(b: => T) = a
override def ∧(other: => Buul) = other
override def ∨(other: => Buul) = this
override def unary_! = Fls
object Fls extends Buul:
override def apply[T](a: => T)(b: => T) = b
override def ∧(other: => Buul) = this
override def ∨(other: => Buul) = other
override def unary_! = Tru
(Tru ∧ Tru) { println("t") } { println("f") } // t
(Tru ∧ Fls) { println("t") } { println("f") } // f
(Fls ∧ Tru) { println("t") } { println("f") } // f
(Fls ∧ Fls) { println("t") } { println("f") } // f
(Tru ∨ Tru) { println("t") } { println("f") } // t
(Tru ∨ Fls) { println("t") } { println("f") } // t
(Fls ∨ Tru) { println("t") } { println("f") } // t
(Fls ∨ Fls) { println("t") } { println("f") } // f
(!Tru) { println("t") } { println("f") } // f
(!Fls) { println("t") } { println("f") } // t
I hope the logic is clear. For example, for Tru
, the AND-operator is: Tru &&& Tru == Tru
, Tru &&& Fls == Fls
, so really, Tru &&& something
is just something
. So, we can define the result of the &&&
operator for Tru
just to be "the other operand". And the same or similar logic applies to the other 3 binary operators, as well as for the unary NOT-operator.
Here is the full implementation, with some more general types and more type-safety, again fully runnable example:
sealed abstract trait Buul:
def apply[T, U <: T, V <: T](thn: => U)(els: => V): T
infix def ∧(other: => Buul): Buul
infix def ∨(other: => Buul): Buul
def unary_! : Buul
case object Tru extends Buul:
override def apply[T, U <: T, V <: T](thn: => U)(els: => V): U = thn
override infix def ∧(other: => Buul) = other
override infix def ∨(other: => Buul): this.type = this
override def unary_! = Fls
case object Fls extends Buul:
override def apply[T, U <: T, V <: T](thn: => U)(els: => V): V = els
override infix def ∧(other: => Buul): this.type = this
override infix def ∨(other: => Buul) = other
override def unary_! = Tru
In C#, the whole thing would be a bit more verbose, and look a little bit like this:
interface Buul {
public T Apply<T>(Func<T> thn, Func<T> els);
public void Apply(Action thn, Action els);
public Buul And(Buul other);
public Buul Or(Buul other);
public Buul Not { get; }
public static Tru T = new Tru();
public static Fls F = new Fls();
public static Buul FromBool(bool b) => b ? T : F;
public record class Tru: Buul {
internal Tru() {}
public T Apply<T>(Func<T> thn, Func<T> els) => thn();
public void Apply(Action thn, Action els) => thn();
public Buul And(Buul other) => other;
public Buul Or(Buul other) => this;
public Buul Not { get => F; }
}
public record class Fls: Buul {
internal Fls() {}
public T Apply<T>(Func<T> thn, Func<T> els) => els();
public void Apply(Action thn, Action els) => els();
public Buul And(Buul other) => this;
public Buul Or(Buul other) => other;
public Buul Not { get => T; }
}
}
This is how you would use it:
Buul.FromBool(2 < 3).Apply(() => Console.WriteLine("2 is less than 3"), () => Console.WriteLine("2 is less than 3"));
// 2 is less than 3
Buul.FromBool(4 < 3).Apply(() => Console.WriteLine("4 is less than 3"), () => Console.WriteLine("4 is greater than 3"));
// 4 is greater than 3
Buul.T.And(Buul.T).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // t
Buul.T.And(Buul.F).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // f
Buul.F.And(Buul.T).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // f
Buul.F.And(Buul.F).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // f
Buul.T.Or(Buul.T).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // t
Buul.T.Or(Buul.F).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // t
Buul.F.Or(Buul.T).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // t
Buul.F.Or(Buul.F).Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // f
Buul.T.Not.Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // f
Buul.F.Not.Apply(() => Console.WriteLine("t"), () => Console.WriteLine("f")); // t
So, there you have it: a functional / object-oriented implementation of booleans in C#.
But, here's the problem: it is absolutely useless. Why? Because there are tens of thousands of existing methods which return bool
s and there are tens of thousands of existing methods which take bool
s as arguments.
You can add implicit conversions from bool
to Buul
and vice-versa, you can add your own helpers, wrappers, extension methods, etc., but there will always be an impedance mismatch.
My question is, how have you solved this problem in your projects?
I don't consider it a problem.
Which approach would you recommend?
None. There is no problem to solve.
I see this sort of question a lot more than I expected, where someone feels a boolean isn't "functional" enough, but really the boolean type itself is fine. As Jörg pointed out, the standard operations on booleans are basically syntactic sugar for much more fundamental operations.
From a more practical point of view, it seems what you're missing isn't a more "functional" boolean type, but more operations on the existing type that more clearly express your intent. Functional programmers create higher order functions all the time for similar purposes.
One example that comes to mind is Either.cond. It lets you write something like Either.cond(boolean, rightValue, leftValue)
instead of if boolean then Right(rightValue) else Left(leftValue)
. It clarifies your intent that you are creating an Either
based on a boolean, ensures you handle both conditions, and reduces some boilerplate.
So I would encourage you to think more about the functions you want and create those (perhaps with extension methods), rather than trying to invent a new type.
-
1What is wrong with the ternary
(boolean ? rightValue : leftValue)
, which is the standard idiomatic way of a functional conditional expression all kind of languages rooted on the C syntax?Doc Brown– Doc Brown01/25/2023 07:20:46Commented Jan 25, 2023 at 7:20 -
Nothing, if that's all you want to do, but if you are doing more with each part, in a common pattern, you might want to factor that out into a function. The main benefit of an
Either
isn't the representation, it's all the useful functions that are defined for it.Karl Bielefeldt– Karl Bielefeldt01/25/2023 17:23:31Commented Jan 25, 2023 at 17:23
Explore related questions
See similar questions with these tags.
if
/else
constructs with the ternary operator. It is not a coincidence that the ternary operator in functional languages is speltif ... else
.TrueOrFalse
andreturn new TrueOrFalse.True()
all over your code base instead ofbool
andreturn true
?ShowAdvancedStatisticsFeature.Match(t => ShowAdvancedStatistics(), f => ShowSimpleStatistics())
Just to make it more explicit that both cases should be regression tested for example.