Skip to content

Instantly share code, notes, and snippets.

@julian-a-avar-c
Last active October 5, 2021 18:55
Show Gist options
  • Save julian-a-avar-c/65af877504e7976a1919f08c49fca425 to your computer and use it in GitHub Desktop.
Save julian-a-avar-c/65af877504e7976a1919f08c49fca425 to your computer and use it in GitHub Desktop.
I've been learning some FP, so I wanted to teach my friends some of the cool stuff I've been learning :)
using System;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace FunctionalProgrammingForCSharp {
class Program {
static void title(string title_name, string separator = "\n----\n") {
Console.Write(separator + $" {title_name}" + separator);
}
static void block(string text, int indentation = 4) {
Console.WriteLine("\n" + string.Join("\n", text
.Split("\n")
.Select(line => line.Length >= indentation ? line[indentation..] : line)
.Select(line => "| " + line)
));
}
static void label(string text) {
Console.WriteLine($"\n- {text}");
}
static void log(string text) { Console.WriteLine($"Log> {text}"); }
static void log<T>(T t) { log(to_s<T>(t)); }
static void log<T>(List<T> t) { log(to_s<T>(t)); }
static string to_s<T>(T t) { return t.ToString(); }
static string to_s<T>(List<T> l) {
return $"[{string.Join(", ", l.Select(e => to_s<T>(e)))}]";
}
static void end() {
Console.Write("\n[END OF CHAPTER]");
}
static T clone<T>(T thing) =>
JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(thing));
static void newline() { Console.WriteLine(); }
static void Main(string[] args) {
title("Functional Programming for C#", "\n========\n");
// Feel free to un/comment out the lines chapter by chapter :D
// You may see some of the chapters being shuffled around or renamed,
// don't worry, I'm trying to improve the reading experience :P
Chapter_1("Chapter 1 - Pure Functions");
// Chapter_2("Chapter 2 - Functions as First Class Citizens");
// Chapter_3("Chapter 3 - Imperative vs Declarative");
// Chapter_4("Chapter 4 - Immutability");
// Chapter_5("Chapter 5 - Currying");
// Chapter_6("Chapter 6 - Function Composition");
// Chapter_7("Chapter 7 - Avoid Primitive Obsession");
// Chapter_8("Chapter 8 - Option Type");
// Epilogue();
}
static void Chapter_1(string title_name = "Chapter 1 - Pure Functions") {
title(title_name);
block(@"
Much like how OOP is all about objects, functional programming
is all about functions. OOP tries to encapsulate things so that
each part does not need to know about each other which ends
looking very elegant to the end user : car1.drive() as opposed
to drive(car1), and then making sure there's an overload for
every kind of function.
However, FP is based on different principles than OOP.
Let's start with pure functions. We'll say that we have a list
of numbers. We first need to double them, then we need to sum
them.
An impure way of doing so might be:
-- ex1
");
List<float> nums = new List<float>() {1f, 2f, 3f, 4f, 5f};
void double_nums_v1() {
for (int i = 0; i < nums.Count; i ++) {
float oldNum = nums[i];
float newNum = oldNum * 2;
nums[i] = newNum;
}
}
float sum_v1() {
float total = 0;
for (int i = 0; i < nums.Count; i++) {
float num = nums[i];
total += num;
}
return total;
}
double_nums_v1();
float total_v1 = sum_v1();
label("nums");
log(nums);
label("total_v1");
log(total_v1);
block(@"
Let's annalyse this for a bit. First we doubled the numbers,
great, then we got the sum and the count of how many items at
the same time, double great! Now let's get the original array,
but we can't :( because it doesn't exist anymore, we merged it
the double numbers.
But when I called `double_nums()` nothing told me I was going
to change the variable that I needed later. So now I have a bug
where a value that I needed to use later is changed and I have
hundreds of lines and I'm gonna have to sit through them until
I find the line that changed the variable.
Instead we (when permissible) we should try to never change a
value that is not owned by the function. We must not have side
effects, because then we have to trust that whoever is naming
the functions is doing so correctly all the time (this of
course includes our past selves).
In order for a function to be pure, however, it also need to be
completely independent from outside sources. This is so that
they always return the same value. `sum()` for example does not
modify a value outside of itself, however it is based on a
value outside of it's scope, and therefore it doesn't have
referential transparency.
This means that if you call the same function twice, you will
get different values.
For instance:
-- ex2
");
int num = 0;
int add_v1(int x) {
num += x;
return num;
}
label("add_v1(3)");
log(add_v1(3));
label("add_v1(3)");
log(add_v1(3));
label("add_v1(3)");
log(add_v1(3));
block(@"
As seen, even though we called the same function 3 times with
the same value, we got different answers, which is ridiculous!
Sometimes, it's needed such as a random number generator, or
when you need to track the state of something.
So we can see that pure functions are not *necessary* all the
time, but they are really useful because they are not changing
hidden variables which we don't know anything about, and they
always return the same result.
Therefore, by convention, any function that returns should be
pure, and any void function should contain *side* effects and
mutations. Again, cirscumstances which require otherwise will
appear, and you don't have to refactor all your functions to be
pure, but it's nice to separate them.
Finally let's rewrite the above code in a *purer* way.
-- ex3
");
// redefine, since it's still in scope
nums = new List<float>() {1f, 2f, 3f, 4f, 5f};
List<float> double_nums_v2(List<float> original_ns) {
List<float> new_ns = new List<float>();
for (int i = 0; i < original_ns.Count; i ++) {
float oldNum = original_ns[i];
float newNum = oldNum * 2;
new_ns.Add(newNum);
}
return new_ns;
}
float sum_v2(List<float> original_ns) {
float total = 0;
for (int i = 0; i < original_ns.Count; i++) {
float num = original_ns[i];
total += num;
}
return total;
}
List<float> doubled_nums = double_nums_v2(nums);
float total_v2 = sum_v2(doubled_nums);
label("nums");
log(nums);
label("doubled_nums");
log(doubled_nums);
label("total_v2");
log(total_v2);
end();
}
static void Chapter_2(string title_name = "Chapter 2 - Functions as First Class Citizens") {
title(title_name);
block(@"
Here's something which you might have not have thought of much
if you started with C#. Functions can be values. That's one of
the main concepts behind FP. Luckily for us, that functionality
was added in C#3 in the form of delegates and lambda functions.
Let's say that we have a 2 dimensional array, and we need to
loop through it and add 42 to every value, and multiply by
11 every value, and store both in it's own variable.
We'd write something like this:
-- ex1
");
List<List<int>> important2DList = new List<List<int>>() {
new List<int>() { 1, 2, 3 },
new List<int>() { 4, 5, 6 },
new List<int>() { 7, 8, 9 }
};
List<List<int>> plus42_val = clone(important2DList);
for (int i = 0; i < plus42_val.Count; i++) {
for (int j = 0; j < plus42_val[i].Count; j++) {
plus42_val[i][j] += 42;
}
}
List<List<int>> multiplyBy11_val = clone(important2DList);
for (int i = 0; i < multiplyBy11_val.Count; i++) {
for (int j = 0; j < multiplyBy11_val[i].Count; j++) {
multiplyBy11_val[i][j] *= 11;
}
}
label("important2DList");
log($"[{string.Join(", ", important2DList.Select(e => to_s(e)))}]");
label("plus42_val");
log($"[{string.Join(", ", plus42_val.Select(e => to_s(e)))}]");
label("multiplyBy11_val");
log($"[{string.Join(", ", multiplyBy11_val.Select(e => to_s(e)))}]");
block(@"
You might notice that we are repeating a lot of code. In fact,
I didn't wanna type it again, so I just copy pasted. Only one
line changed, yet we have only changed one line. It feels very
dumb, and I don't know if you've ever done something like that,
but I definitely have, and it's so frustrating because I didn't
know how to fix it so that I didn't repeat so much code.
Sure, you can put it inside of a function and then add an if
else statement, but that's so much complexity to do something
so simple.
So let's take a look at how we may fix that. As explained
before we need to use functions as values. So let's take a look
at how to do that. I mean, there is no `function` type right?
Well, actually there is XD. It's called delegate in C#, but
sadly it requires class scope, so I'm not going to be doing
that. Instead we can use `System.Func` and `System.Action`.
They both return a delegates, however, an `Action` returns
void, while a `Func` returns something. Since we are trying to
write pure functions as stated in Chapter 1, we will be mainly
using `Func`, but if you need/want side effects, use `Action`.
Let's start on how to use it:
-- ex2
");
Func<int, int, int> add_two_nums = delegate(int a, int b) {
return a + b;
};
label("add_two_nums");
log(add_two_nums(1, 2));
block(@"
Let's first observe the signature, or the type of
`add_two_nums`, it being: `Func<int, int, int>`.
As you might have guessed, the last generic argument is the
return type (by generic argument, I mean the things between the
angle brackets <,> which are also called type arguments) while
the rest are the argument types. Of course, in an `Action` you
don't have a return type, so the same argument signature would
mean that you have 3 arguments.
Either way, you have 2 inputs 1 output, and then you set it to
a delegate, or a function.
You might think, so what? Well since it's a value (just like
any other value) you can change it, let's do so:
-- ex3
");
// we can't change the type since it's already defined
add_two_nums = delegate(int a, int b) {
return a * b + 42;
};
label("add_two_nums");
log(add_two_nums(1, 2));
block(@"
And now the behavior is completely changed. You can use this if
for example you have a bunch of entities and you want to change
their behavior mid-way, you simply change the function and
that's it!
Another thing you might have noticed is that since it's now a
value, that means you can pass it to another function just like
any other value, and then pass those to other functions:
-- ex4
");
// let's go back to the previous definition
add_two_nums = delegate(int a, int b) { return a + b; };
Func<Func<int, int, int>, int, int, int> multiply_two_nums_with_add_definition = delegate(
Func<int, int, int> how_to_add, int a, int b
) {
int total = 0;
for (int i = 1; i <= a; i++) { total = how_to_add(total, b); }
return total;
};
label("multiply_two_nums_with_add_definition(add_two_nums, 3, 4)");
log(multiply_two_nums_with_add_definition(add_two_nums, 3, 4));
block(@"
If by this point your mind is not blow, you have no soul.
`multiply_two_nums_with_add_definition` has no idea how to sum,
so we pass that function so it can do it's job.
You should now try to solve ex1.
The solution is pretty simple:
-- ex5
");
Func<List<List<int>>, Func<int, int>, List<List<int>>> loop2D_v1 = delegate(
List<List<int>> list,
Func<int, int> operation
) {
for (int i = 0; i < list.Count; i++) {
for (int j = 0; j < list[i].Count; j++) {
list[i][j] = operation(list[i][j]);
}
}
return list;
};
Func<int, int> plus42 = delegate(int a) { return a + 42; };
Func<int, int> multiplyBy11 = delegate(int a) { return a * 11; };
List<List<int>> plus42_v1 = loop2D_v1(important2DList, plus42);
List<List<int>> multiplyBy11_v1 = loop2D_v1(important2DList, multiplyBy11);
block(@"
As you can see above, we've got rid of the repetitiveness and
if the function is not complex enough we can even put it on the same line:
-- ex6
");
List<List<int>> plus42_v2 = loop2D_v1(important2DList, delegate(int a) { return a + 42; });
List<List<int>> multiplyBy11_v2 = loop2D_v1(important2DList, delegate(int a) { return a * 11; });
block(@"
Finally, we can use lambda notation, which is simpler:
-- ex7
");
Func<List<List<int>>, Func<int, int>, List<List<int>>> loop2D_v2 = (list, operation) => {
for (int i = 0; i < list.Count; i++) {
for (int j = 0; j < list[i].Count; j++) {
list[i][j] = operation(list[i][j]);
}
}
return list;
};
List<List<int>> plus42_v3 = loop2D_v2(important2DList, a => a + 42);
List<List<int>> multiplyBy11_v3 = loop2D_v2(important2DList, a => a * 11);
block(@"
And as you can see we now have one function to loop through all
the stuff that we had to, and two expression to dictate what
we're actually doing, instead of repeating that double for loop
twice. 13 vs 11 lines. Sure it's not that much shorter, but
it's arguably easier to understand and you are not repeating
code.
Of course, much like with pure functions, this technique should
be used with discretion, but it's a very powerful technique if
used correctly.
");
}
static void Chapter_3(string title_name = "Chapter 3 - Imperative vs Declarative") {
title(title_name);
block(@"
Let's use the last example used
-- ex1
");
List<List<int>> important2DList = new List<List<int>>() {
new List<int>() { 1, 2, 3 },
new List<int>() { 4, 5, 6 },
new List<int>() { 7, 8, 9 }
};
Func<List<List<int>>, Func<int, int>, List<List<int>>> loop2D_v1 = (list, operation) => {
for (int i = 0; i < list.Count; i++) {
for (int j = 0; j < list[i].Count; j++) {
list[i][j] = operation(list[i][j]);
}
}
return list;
};
List<List<int>> plus42_v1 = loop2D_v1(important2DList, a => a + 42);
List<List<int>> multiplyBy11_v1 = loop2D_v1(important2DList, a => a * 11);
block(@"
As you can see from
```
for (int i = 0; i < list.Count; i++) {
for (int j = 0; j < list[i].Count; j++) {
```
that monstrosity, in order to accomplish our task of adding 42
and then multiplying by 11, we can't do any of that before we
tell the computer how to go through the list.
We must teach the computer how to start it, how to stop, how to
loop, how to vary it's speed. It's a pain in the neck,
sometimes you might even forget what you were doing while
writing all of the boilerplate code (I know I have).
From that, it'd be super nice if there was a method that would
just loop through everything in a relatively fast way, and make
*it* figure out how to loop instead of doing so ourselves.
Luckily for us, we know how to use functions as first class
citizens. Let's call our function `ForEach()`
-- ex2
");
newline();
void ForEach(List<int> list, Action<int> task) {
for (int i = 0; i < list.Count; i++) { task(list[i]); }
}
List<int> nums = new List<int>() {1, 2, 3, 4, 5};
ForEach(nums, num => Console.WriteLine(num + 1));
block(@"
Sadly, we can't modify the original list, there are ways of
course, like passing a reference to the item in our `ForEach`
instead of passing the value, but that's outside of our scope.
For now, know that you can't modify the original.
BUT, you can return a new `List` and assign it to the same one
once it returns. Of course we try not to do that since with
purity comes immutability, but as said before, this, much like
OOP, are only guidelines, you can make your code as horrible as
you want in practice.
So let's make a function that return a modified list instead of
doing side effects only and call it Map:
-- ex3
");
List<int> Map_intonly(List<int> list, Func<int, int> function) {
list = clone(list);
for (int i = 0; i < list.Count; i++) {
list[i] = function(list[i]);
}
return list;
}
List<int> result1 = Map_intonly(nums, num => num + 1);
label("result1");
log(result1);
label("nums");
log(nums);
block(@"
Don't worry about the `clone()` function, it'll be irrelevant
by the end of the chapter ;)
We loop through the list, then we set the value of each element
essentially applying a _transform_ and then we return it so
that it can be used by someone.
And we could just hide the implementation behind the curtains
and no one would care about it, you can just use the function
which is excellent because it means you won't ever need to
touch a for loop unless you really care about performance and
need to control every aspect of how you loop.
In that sense this is like C# vs Assembler (google it if
you've never seen assembler code, it's disgusting). On one hand
you have a one line representation of a for loop which although
may not be the fastest it's good enough for most tasks, on the
other you have this for loop that you have to type like 40
characters to get going, and sometimes you misspell something
and you end up an hour hung up with something you really
shouldn't even be worrying about. You are sacrificing a little
bit of speed with readability and ease of use.
Let's now go all the way around and implement our `loop2D` with
our map function. But first we need to make a generic version
(meaning that it can use any type, not just int like before)
-- ex4
");
List<T> Map<T>(List<T> list, Func<T, T> operation) {
list = clone(list);
for (int i = 0; i < list.Count; i++) {
list[i] = operation(list[i]);
}
return list;
}
block(@"
Now we can pass any kind of list and as long as the operation
return the same type everything is fine :D
Let's create our loop2D now:
-- ex5
");
Func<List<List<int>>, Func<int, int>, List<List<int>>> loop2D = (list, operation) =>
Map(list, inner_list => Map(inner_list, item => operation(item)));
List<List<int>> plus42_v2 = loop2D(important2DList, a => a + 42);
List<List<int>> multiplyBy11_v2 = loop2D(important2DList, a => a * 11);
block(@"
Would you look at this, we started with 13 lines, and now we
have 4, and only because `loop2D` doesn't fit in one line,
otherwise, it'd fit in 3.
But luckily for us, we don't need to define Map, because it's
already defined only with a differnet name: `Select`. However,
in order to use it, you need to be `using System.Linq`.
Once you're ready you can type code like this without defining
any helper functions:
-- ex6
");
Func<List<List<int>>, Func<int, int>, List<List<int>>> select2D = (list, operation) =>
list.Select(inner_list => inner_list.Select(item => operation(item)).ToList()).ToList();
block(@"
There are 3 big functions which are used on a day to day basis,
`Select`, `Aggregate`, and `Where`, which is equivalent to
`map`, `reduce`, and `filter`.
`Select`/`map` maps every item by applying a transformation.
`Aggregate`/`reduce` reduces the items so that there's only one
left. It uses an accumulator, and the current item.
`Where`/`filter` filters the list.
");
}
class Point_v1 {
public int x { get; private set; }
public int y { get; private set; }
public Point_v1(int x, int y) {
this.x = x;
this.y = y;
}
public void MoveBy(int x, int y) {
this.x += x;
this.y += y;
}
}
class Point_v2 {
private int x { get; }
private int y { get; }
public Point_v2(int x, int y) {
this.x = x;
this.y = y;
}
public Point_v2 MoveBy(int x, int y) {
return new Point_v2(this.x + x, this.y + y);
}
}
class Point_v3 {
private int x { get; }
private int y { get; }
public Point_v3(int x, int y) {
this.x = x;
this.y = y;
}
public (int x, int y) MoveBy(int x, int y) {
return (this.x + x, this.y + y);
}
}
static void Chapter_4(string title_name = "Chapter 4 - Immutability") {
title(title_name);
block(@"
Immutability means that FP prefers to not mutate the values of
a variable, instead creating a new value.
This means, instead of using your class to modify itself, try
to return a new object.
-- ex1
");
// Point_v1
Point_v1 p_v1 = new Point_v1(3, 4);
p_v1.MoveBy(5, 6);
block(@"
In our fist example, we are encapsulating `x` and `y`, cannot
access them directly and need to use `MoveBy` in order to set
new positions. This is the OOP way of doing things.
FP however dictates that you should not use hidden states,
functions must be pure, and therefore you shouldn't be causing
side effects like `Point_v1.MoveBy` does.
-- ex2
");
// Point_v2
Point_v2 p_v2 = new Point_v2(3, 4);
Point_v2 new_p_v2 = p_v2.MoveBy(5, 6);
block(@"
In our second example `MoveBy` is now returning a new object,
which is great for FP as that means more immutability and more
pure functions. But it also means more processing, since
objects can become quite memory intensive.
We could also create another `MoveBy` which returns the
parameters necessary to create the new object.
-- ex3
");
// Point_v3
Point_v3 p_v3 = new Point_v3(3, 4);
var new_p_v3_args = p_v3.MoveBy(5, 6);
Point_v3 new_p_v3 = new Point_v3(new_p_v3_args.x, new_p_v3_args.y);
block(@"
And so we can keep our immutability, and also not spam out ram
until needed.
");
}
static void Chapter_5(string title_name = "Chapter 5 - Currying") {
title(title_name);
block(@"
In the previous chapters we saw how function are data, just
like strings, or ints. But just like we can pass functions as
arguments, we can also return functions. That is what we call
currying.
Let's say that we have a complex function, and we need to call
it with multiple values, multiple times.
-- ex1
");
Func<int, string, bool, short, long, float, int> complexFunc = (
vA, vB, vC, vD, vE, vF
) => {
// assume complex calculation
return 0;
};
int vA = 0;
string vB = "second value";
bool vC = false;
short vD = 6;
long vE0 = 2500000;
long vE1 = 2522222;
float vF1 = 4.20f;
float vF2 = 69.69f;
float vF3 = 21.00f;
complexFunc(vA, vB, vC, vD, vE0, vF1);
complexFunc(vA, vB, vC, vD, vE0, vF2);
complexFunc(vA, vB, vC, vD, vE0, vF3);
complexFunc(vA, vB, vC, vD, vE1, vF1);
complexFunc(vA, vB, vC, vD, vE1, vF2);
complexFunc(vA, vB, vC, vD, vE1, vF3);
block(@"
We are repeating a lot of values. Also, what if `vB` is
calculated before, then we would have to pass around vB around
as parameter a bunch of times until needed, or store it as
global scope with a bunch of stuff which is not related.
On top of that we are repeating `vA`-`vD`, which is just
annoying.
So, evidently we are gonna have to do something better. Let's
learn how to do currying in C#.
-- ex2
");
Func<int, int, int> add_v1 = delegate(int x, int y) {
return x + y;
};
int r1 = add_v1(2, 5);
int r2 = add_v1(2, 6);
int r3 = add_v1(3, 5);
int r4 = add_v1(3, 6);
// or
Func<int, Func<int, int>> add_v2 = delegate(int x) {
return delegate(int y) {
return x + y;
};
};
Func<int, int> add2_v1 = add_v2(2);
Func<int, int> add3_v1 = add_v2(3);
r1 = add2_v1(5);
r2 = add2_v1(6);
r3 = add3_v1(5);
r4 = add3_v1(6);
block(@"
As you can see currying requires a bit more setup, luckily,
lambdas are here to solve our problem, in addition, we can use
the `var` type, which essentially makes C# do the hard job of
guessing what the type will be so that we don't have to worry
about it.
-- ex3
");
Func<int, Func<int, int>> add_v3 = x => y => x + y;
var add2_v2 = add_v2(2);
var add3_v2 = add_v2(3);
r1 = add_v3(3)(5);
r2 = add_v3(3)(6);
r3 = add_v3(3)(5);
r4 = add_v3(3)(6);
// or
r1 = add2_v2(5);
r2 = add2_v2(6);
r3 = add3_v2(5);
r4 = add3_v2(6);
block(@"
First, just because we have the option of currying, doesn't
mean that we have to set up the function. The function
definition can separate the parameters with arrows (=>) instead
of commas (,), and you just gained a lot of flexibility.
But we don't always have the time, or the wants to create
currying functions, but it'd still be nice if we could get
that.
Luckily for us, it's not that hard to do
-- ex4
");
int multiply(int x, int y) => x * y;
int multBy2(int y) => multiply(2, y);
int multBy3(int y) => multiply(3, y);
r1 = multiply(2, 5);
r2 = multiply(2, 6);
r3 = multiply(3, 5);
r4 = multiply(3, 6);
// or
r1 = multBy2(5);
r2 = multBy2(6);
r3 = multBy3(5);
r4 = multBy3(6);
block(@"
Let's use currying for our first example then:
-- ex5
");
// option 1
Func<int, Func<string, Func<bool, Func<short, Func<long, Func<float, int>>>>>> complexFunc1 =
vA => vB => vC => vD => vE => vF => {
// assume complex calculation
return 0;
};
var func1_vABCD = complexFunc1(vA)(vB)(vC)(vD);
var func1_vE0 = func1_vABCD(vE0);
var func1_vE1 = func1_vABCD(vE1);
// option 2
Func<int, string, bool, short, long, float, int> complexFunc2 = (
vA, vB, vC, vD, vE, vF
) => {
// assume complex calculation
return 0;
};
Func<long, float, int> func2_vABCD = (E, F) => complexFunc2(vA, vB, vC, vD, E, F);
Func<float, int> func2_vE0 = (F) => func2_vABCD(vE0, F);
Func<float, int> func2_vE1 = (F) => func2_vABCD(vE1, F);
// either:
func2_vE0(vF1);
func2_vE0(vF2);
func2_vE0(vF3);
func2_vE1(vF1);
func2_vE1(vF2);
func2_vE1(vF3);
block(@"
As you can see there are two way of doing this, you could also
`using` a library with a partial function, which will transform
any non currying function into a currying function. But from
where we stand that's as much as you need to know.
It is not necessary to use currying functions all the time, but
they can be very powerful when used properly.
");
}
static void Chapter_6(string title_name = "Chapter 6 - Function Composition") {
title(title_name);
block(@"
I'm sure that in math class you might have seen something like:
`g(f(x)) = (g . f)(x)` [img]
This is called function composition, and it can be super
helpful.
img: [http://3.bp.blogspot.com/-kgATvEPETIQ/UKwME42wckI/AAAAAAAAHdM/QK8rA1bKb1U/s1600/def02.PNG]
Let's take a look at some code:
-- ex1
");
Func<string, char[]> reverse = x => x.Select(
(c, i) => (i < x.Count())
? x[x.Count() - 1 - i]
: c
).ToArray();
Func<char[], string> capitalize = x => new string(x.Select(
(c, i) => (i == 0)
? char.ToUpper(c)
: c
).ToArray());
Func<string, int> count = x => x.Aggregate(0, (t, _) => t + 1);
label("new string(reverse(\"1 2 3 4 5 \"))");
log(new string(reverse("1 2 3 4 5 ")));
label("capitalize(\"Robert\".ToArray())");
log(capitalize("Robert".ToArray()));
label("count(\"abcdef\")");
log(count("abcdef"));
var val1 = reverse("Radar");
var val2 = capitalize(val1);
var val3 = count(val2);
// or
var composite = count(capitalize(reverse("Radar")));
label("new string(val1)");
log(new string(val1));
label("val2");
log(val2);
label("val3");
log(val3);
label("composite");
log(composite);
block(@"
As you might have guessed, all functions above have a name, so
luckily you won't have to make them up all the time, I'm just
putting them there as example. You might also have noticed that
I'm logging `reverse` around a `new string()`, cool note,
strings are actually char arrays. But enough of that for now.
As you might have seen, we either have to make 3 confusing
variables, or you can make a ton of nested variables, neither
of them is pretty, so knowing that functions can be treated as
values, can we make a function that does that for us?
-- ex2
");
Func<X, Z> compose2<X, Y, Z>(Func<Y, Z> g, Func<X, Y> f) => x => g(f(x));
var reverse_then_capitalize = compose2(capitalize, reverse);
var reverse_then_capitalize_then_count = compose2(count, reverse_then_capitalize);
var composite1 = reverse_then_capitalize_then_count("john");
Func<W, Z> compose3<W, X, Y, Z>(
Func<Y, Z> h,
Func<X, Y> g,
Func<W, X> f) => x => h(g(f(x)));
var reverse_capitalize_count = compose3(count, capitalize, reverse);
var composite2 = reverse_capitalize_count("john");
label("composite1");
log(composite1);
label("composite2");
log(composite2);
block(@"
As you can see, `compose2` takes 2 functions, and `compose3`
takes 3 functions. Due to limitations in C# however, you'll
have to do a bunch of compose definition for different amount
of functions. Since these are local I can't overload them, but
you can overload as methods. Just make up to 8 and you should
be fine in most scenarios XD.
But even though we made a special function for this, it still
is a bit weird how we are writing the arguments backwards, this
is simply how compose behaves. If we want something that works
more intuitively we might want to use `pipe`.
BTW, pipe is written as `|` in most Linux bash command lines.
-- ex3
");
Func<T1, T3> pipe2<T1, T2, T3>(
Func<T1, T2> f1,
Func<T2, T3> f2) => x => f2(f1(x));
var pipe_reverse_capitalize = pipe2(reverse, capitalize);
var pipe_reverse_capitalize_count = pipe2(reverse_then_capitalize, count);
var piped1 = pipe_reverse_capitalize_count("john");
Func<T1, T4> pipe3<T1, T2, T3, T4>(
Func<T1, T2> f1,
Func<T2, T3> f2,
Func<T3, T4> f3) => x => f3(f2(f1(x)));
var pipe3_reverse_capitalize_count = pipe3(reverse, capitalize, count);
var piped2 = pipe3_reverse_capitalize_count("john");
label("piped1");
log(piped1);
label("piped2");
log(piped2);
block(@"
And as you can see, pipe makes a LOT easier to read :P, but it
also lies on what it's doing a bit (in terms of definition).
That said, please don't use `compose` it's a nightmare that no
one wants to follow. Also, please use T1, T2... and f1, f2...
instead of W, X, Y, Z. Believe me, Alt-Clicking a function and
then finding THAT, would cause anyone to want to pull their
hair out.
Anyways, here's a cool article on that: https://www.codeproject.com/Articles/375166/Functional-programming-in-Csharp
");
}
class Person1 {
string email;
public Person1(string email) { this.email = email; }
public string shareEmail() => this.email;
}
class Person2 {
string? email;
public Person2(string email) {
if (Regex.IsMatch(email, @"([A-Za-z]+[\w.]*)@([A-Za-z]+[\w.\-]*)\.[A-Za-z]")) {
this.email = email;
} else {
this.email = null;
}
}
public string shareEmail() => this.email;
}
class Person3 {
string email;
public static Person3 Factory(string email) {
bool valid_email = Regex.IsMatch(email, @"([A-Za-z]+[\w.]*)@([A-Za-z]+[\w.\-]*)\.[A-Za-z]");
bool valid_arg1 = true;
bool valid_arg2 = false;
return (valid_arg1 && valid_arg1 && valid_arg2)
? new Person3(email)
: null;
}
public Person3(string email) {
this.email = email;
}
public string shareEmail() => this.email;
}
static void Chapter_7(string title_name = "Chapter 7 - Avoid Primitive Obsession") {
title(title_name);
block(@"
It is common amongst starting and/or lazy devs to try to use
primitive types all of the time since either they don't know
how to or don't want to make a factory method.
Let's go over why you would need that first :D
-- ex1
");
// Person1
Person1 p1 = new Person1("[email protected]");
string p1_email(Person1 p) { return p.shareEmail(); }
var res1 = p1_email(p1);
label("res1");
log(res1);
block(@"
The example shown above might seem great, but it has a huge
flaw. What if the programmer doesn't understand what format the
`email` parameter has to be in? Well, we could always use some
regex to detect if it's right, then have the person's email be
null if the programmer doesn't know what he's doing:
-- ex2
");
// Person2
Person2 p2 = new Person2("name@email@com");
string p2_email(Person2 p) { return p.shareEmail(); }
var res2 = p2_email(p2);
label("res2");
log((res2 != null) ? res2 : "null");
block(@"
As you might have guessed now the email being incorrectly
formatted is no longer a problem. Now we might want to have
a person without an email be fine. But it happens at times when
the email must be valid in order to have a valid Person, yet
when we call the constructor, we have no way of returning
`null` for that. As it is now we would have to check for email
every time we want to use Person, or make a function to do that
for us, none of which sounds particularly intelligent. Why
can't we just tell C# what we want, return null if any of the
parameters are invalid? One way to do that is with a factory
-- ex3
");
// Person3
Person3 p3 = Person3.Factory("name@email@com");
string p3_email(Person3 p) { return p?.shareEmail() ?? "null"; }
var res3 = p3_email(p3);
label("res3");
log(res3);
block(@"
As you can see, now we don't have to worry about calling
p3_email on p3 because p3 can be null (since it's a class
instance) and we just have to check it with p3_email.
At this point you might be thinking, doesn't that mean we have
to check for nullness all the time, verifying at every step of
the way when an instance can be null, when a primitive can be
null providing checks upon checks upon checks of null checks?
Yup. Because that's how C# was built.
The biggest problem is that any value can be null at any point,
if you're forced to use a plugin for Unity or an API, you are
forced to either read the entire source code for the plugin/API
to make sure there are no possible nulls, trust whoever made it
that they are perfect human beings who can't ever get anything
wrong, or check for null at every point of contact with that
plugin/API, much like how `p3_email()` does it.
The worst part, is that Person3.Factory never EVER tells us
that there could be a null as a return type. So How the fuck
are we supposed to know???
One way is to use an Option<> type. Take a look at Chapter 8.
");
}
public abstract class Option<T> {
public static implicit operator Option<T>(T value) => new Some<T>(value);
public static implicit operator Option<T>(None none) => new None<T>();
public abstract T getOrElse(T elseValue);
public abstract T getOrElse(Func<T> elseFunc);
public abstract void getOrElse(Action elseAction);
public abstract TOut Match<TOut>(Func<T, TOut> someFunc, Func<TOut> noneFunc);
}
public sealed class Some<T> : Option<T> {
public T value { get; }
public Some(T value) { this.value = value; }
public static implicit operator T(Some<T> some) => some.value;
public override T getOrElse(T elseValue) => this.value;
public override T getOrElse(Func<T> elseValue) => this.value;
public override void getOrElse(Action elseAction) { }
public override TOut Match<TOut>(Func<T, TOut> someFunc, Func<TOut> noneFunc) => someFunc(value);
}
public sealed class None<T> : Option<T> {
public override T getOrElse(T elseValue) => elseValue;
public override T getOrElse(Func<T> elseFunc) => elseFunc();
public override void getOrElse(Action elseAction) { elseAction(); }
public override TOut Match<TOut>(Func<T, TOut> someFunc, Func<TOut> noneFunc) => noneFunc();
}
public sealed class None {
public static None value { get => new None(); }
public override string ToString() => "None";
private None() { }
}
class Person4 {
string email;
public static Option<Person4> Factory(string email) {
bool valid_email = Regex.IsMatch(email, @"([A-Za-z]+[\w.]*)@([A-Za-z]+[\w.\-]*)\.[A-Za-z]");
bool valid_arg1 = true;
bool valid_arg2 = false;
if (valid_arg1 && valid_arg1 && valid_arg2) {
return new Person4(email);
} else {
return None.value;
}
}
public Person4(string email) {
this.email = email;
}
public string shareEmail() => this.email;
}
static void Chapter_8(string title_name = "Chapter 8 - Option Type") {
title(title_name);
block(@"
Our final chapter, mainly cuz I don't want to write more.
Also, this is an advanced subject, you need to know, operator
definitions, implicit type conversions, generics, and abstract
overriding :D.
To understand the title let's give an example:
-- ex1
");
int div(int a, int b) {
return a / b;
}
// comment the try/catch if you wanna see the error
// log(div(12, 0));
try { log(div(12, 0)); } catch { log("Error"); }
block(@"
If we try the above we are going to get a
`System.DivideByZeroException`
Because we divided by 0. That makes sense to us because we know
that we can't divide by 0, we understand what dividing means,
and so the error is expected.
However, what if we have a
`int BlackBoxFunction(int, string)`
And we call it and now we get an error? That would be totally
unexpected and now we would have to read the implementation of
how `BlackBoxFunction` works, which is annoying. Where's the
problem?
The problem is that `BlackBoxFunction` and `div` are both lying
to us, they say they return an `int` but they can sometimes
return an error.
Classes are the same since they are nullable, if the function
has the following signature:
`Person3 Factory(string email)`
This could mean that either it returns a `Person3` or `null`.
Which is infuriating, because that means that now when we use a
it we need to check if it's `null` every time, which ends up
being very annoying. And if you don't do that you'll end up
with an error, at the worst possible time.
`int` and `bool` for example are not nullable, and if you want
them to be nullable, you need to type `int?` or `bool?` which
will make it inherit the `Nullable` interface, and it has two
methods `HasValue` and `Value`, which are very handy, so that
you don't need to be comparing it to null all the time.
-- ex2
");
bool? a = null;
bool b = a.HasValue ? a.Value : false;
block(@"
So, it'd be really nice if we had the same functionality for
any type that we make.
I have defined some classes, feel free to take a look at them.
-- ex3
");
// Option<T>
// Some<T>
// None<T>
// None
void LaunchRocket() { /*...*/ }
Option<int> div1(int x, int y) {
if (y == 0) return None.value;
return x / y;
}
int val1 = div1(12, 0).getOrElse(0);
Option<int> val2 = div1(13, 0);
bool hadToElse = false;
int val3 = val2.getOrElse(() => {
hadToElse = true;
LaunchRocket();
return -1;
});
bool error = false;
val2.getOrElse(() => { error = true; });
label("val1");
log(val1);
label("val2");
log(val2);
label("val3");
log(val3);
label("hadToElse");
log(hadToElse);
label("error");
log(error);
block(@"
Finally, let's take a look at the same code from Chapter 7, to
see just how nice it can be :D
");
Option<Person4> p4 = Person4.Factory("name@email@com");
string p4_email(Option<Person4> p) => p.Match(
p_isValid => p_isValid.shareEmail(),
() => "Person does not exist"
);
var res4 = p4_email(p4);
label("res4");
log(res4);
block(@"
As you may see, has a bit more boiler plate, but now we know
that p4_email may or may not be passed an actual Person, and
therefore we are no longer lying about whether it could be
there or not.
My implementation is extremely simple and not very robust, so I
don't actually recommend using it, instead use something like:
- https://github.com/louthy/language-ext#unity
It has all the function written about until now. Including
compose and pipe, and filter, map and reduce, which are super
nice.
There are also the Either and the Try type. Either tries to
pass around errors instead of possible None values, and Try is
kinda like try catch, but nicer.
And that should sum everything up;
More interesting reads on Monads over here (ah, yeah, the
Option is called a monad):
http://codinghelmet.com/articles/custom-implementation-of-the-option-maybe-type-in-cs
https://davemateer.com/2019/03/12/Functional-Programming-in-C-Sharp-Expressions-Options-Either
https://kwangyulseo.com/2015/01/19/step-by-step-implementation-of-option-type-in-c/
https://github.com/tejacques/Option/
:D
");
}
static void Epilogue() {
block(@"
For more info, here's an excellent video that uses JS:
- https://www.youtube.com/watch?v=FYXpOjwYzcs
Another one using TypeScript:
- https://www.youtube.com/watch?v=tmVk_4oRL-Y
C# with application:
- https://www.youtube.com/watch?v=UM-3ZsHhogA
F#:
- https://www.youtube.com/watch?v=srQt1NAHYC0
Production ready code:
- https://www.youtube.com/watch?v=v7WLC5As6g4
Kinda slow, but shows inner-outer refactoring pattern:
- https://www.youtube.com/watch?v=s8ru33IIQzc
More abstract:
- https://www.youtube.com/watch?v=0if71HOyVjY
Scala:
- https://www.youtube.com/watch?v=m40YOZr1nxQ
Category theory:
- https://www.youtube.com/watch?v=JH_Ou17_zyU
Anyways this is not even the beggining, but it should be enough
for you to get started with FP. Remember, you don't have to use
it all the time, but it can make your life so much easier at
times.
");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment