/* The Option Monad for Error Handling Of course error handling is an essential component of serious software engineering. Programmers, in both academia and industry, often just write programs that "work" on good input, but they don't check for invalid input, or for boundary conditions. This results in a lot of bugs and "patches". Why don't we write software that correctly handle errors in the first place. For example, the null pointer is used in many object oriented programming languages such as Java and C++. Java probably relies on it more than any other language. When you create an array of objects, you get an array of null pointers initially. If you look up a HashMap for a key that it doesn't contain, it returns null as a value. Grades G = mygrades.get("csc127"); // no such class double average = G.computeAverage(); // NULL POINTER EXCEPTION Code like this is why Tony Hoare called the null pointer his "billion dollar mistake". What we would like to do is to set up a framework where ***error handling is not an option.*** Code that does not address the possibility of errors should not even compile. Many programming languages are adopting a functional programming approach to error handling based on an idea called *Monads* from category theory. The following code is my implementation of the Option (aka "Maybe") monad, using an abstract superclass and two concrete subclasses: "Some", which encapsulates a value, and "None", which does not. Many languages have built-in versions of this monad, including Java and C++23. However, their implementations are not pure. An optional object can either hold a value ("Some") or not ("None"). Typically, a method called "unwrap" or "get" or "getValue" is provided that returns the value if present, and throws an exception otherwise. This gets us back to square one, and programmers will write code that they're more used to: if (optional.hasValue()) x = optional.unwrap(); The problem is that if monads are used this way then "None" isn't much different from "null" and will suffer from many of the same problems. There's no requirement to writing the "if" clause before unwrapping, just as there's not requirement to check if a pointer is null. We should note that the language Kotlin actually requires you to write an if-clause to check for null before using a pointer. But the monad approach can address error-handling in general, not just with respect to the null pointer. My implementation below does not provide an unwrap method. The closest is `unwrap_or`, which requires you to supply a default value before unwrapping. Of course, some programmers will still use type casting, reflection, etc to get the value. One thing you can do in C# is to take advantage of mutable closures: int x; optional opt = ... opt.map(y => {x=y; return 0}); // will assign x to value held by the optional object if present, and leave it unassigned otherwise. But it would be better to just do int x = opt.unwrap_or(default_value); which would assign x to the wrapped value if there is one, or the supplied default if not. You will have to give x some value eventually. */ using System; public abstract class Option { public abstract bool is_some(); public abstract Option map( Func fun); public abstract Option map_mut(Func fun); public abstract U match( Func some, Func none ); // version of match that does not return a value (overloading here), public void match( Action some, Action none) { match(some: v=>{some(v); return 0;}, none: ()=>{none(); return 0;}); } // the map function can also be defined using match // return a Some value or the supplied default value public T unwrap_or(T default_val) { return match(some: v => v, none: () => default_val ); } // this functor is also called "bind" or "flatmap": public Option and_then( Func> flatfun ) => match(some: v => flatfun(v), none: () => Option.Nothing ); public static readonly Option Nothing = new None(); //1 time constant public static Option Make(T x) { // includes null-testing if (x!=null) return new Some(x); else return Nothing; } public override string ToString() => //overrides object.ToString match(v => "Some("+v+")", () => "None"); // pair 2 option args, apply 2-arg function if both options exist public Option pair(Option Secondopt, Func bifun) => this.match(some: x => Secondopt.map(y => bifun(x,y)), none: () => Option.Nothing); }//Option abstract type // concrete, non-public sealed classes (can't be further extended) sealed class Some : Option { private T val; public Some(T v) {val=v;} public override bool is_some() {return true;} public override Option map(Func fun) => Option.Make(fun(val)); public override Option map_mut(Func fun) { val=fun(val); return this; } public override U match( Func somef, Func nonef ) => somef(val); } sealed class None : Option { public override bool is_some() => false; public override Option map(Func fun) => Option.Nothing; public override Option map_mut(Func fun) {return this;} public override U match( Func somef, Func nonef ) => nonef(); } // static extension methods can be added to the abstract class/interface public static class option // includes static extension method "flatten" { // extension method, allows call on nested.flatten() instead of // Option.flatten(nested); public static Option flatten(this Option> nested) => nested.unwrap_or(Option.Nothing); // overload map to allow no return value public static void map(this Option opt, Action act) => opt.map(x => {act(x); return 0;}); }// extension methods /* Demonstration and Testing (in option_test.cs) public class option_test { public static Option some(double x) { // syntactic specialization return Option.Make(x); } public static Option NaN = Option.Nothing; public static Option safediv(double a, double b) { if (b!=0) return some(a/b); else return NaN; }//safediv // find largest value in an array of doubles, array can be empty public static Option smallest(double[] A) { if (A==null || A.Length==0) return NaN; int ai = 0; for(int i=1;i ToDouble(string[] args, int i) { if (args==null || i<0 || i>=args.Length) return Option.Nothing; Option answer; try { answer = Option.Make(Convert.ToDouble(args[i])); } catch (Exception e) { Console.Error.WriteLine("ERROR: "+e.Message+" Returing None Option"); answer = Option.Nothing; } return answer; }//ToDouble, moving from exceptions to monads public static void Main(string[] args) { // get value from command-line args, or set to 2.0 as default: double divisor = ToDouble(args,0).unwrap_or(2.0); var Result = safediv(4,divisor); Console.WriteLine("division result: "+Result); Result .map( v => v + 1 ) //non-destructive construction of new Option .map_mut( v => v * -1) //destructive change to existing Option .and_then(v => { // can also call .map(..).flatten() Console.WriteLine("val is now "+v); return safediv(100,(v+3)) .pair(safediv(200,(v-3)), (x,y)=>x+y); // return sum of two safedivs }) .match( some: v => {Console.WriteLine("final result "+v);}, none: () => {Console.WriteLine("no result");} ); }//main for testing }// testing/demonstrating the Option monad */