ABSTRACT: In this article, I present the Corrigendum.wl package that introduces many exclusive types and functions inspired from languages such as Rust and Haskell. The purpose of this library is to allow the writing of simpler and more robust functions that can handle errors or uncertainty of values by using enumerable types such as Result<T, E> and Maybe<T>, respectively, together with many checking and chaining functions such as MatchMaybe and AndThen. Throughout this article, I discuss why using such types can have great advantages on writing less and simpler code, and having less problems handling different types. Many native functions such as Part, Take, Drop, Rest, Most, First, and Last can be given a “Safe” version using the IntoMaybe and IntoResult encapsulation functions.
Note: The Wolfram Language already includes multiple ways of handling values and errors, read the "Robustness & Error Handling" documentation page for more information. This package is meant for one more way of handling values and errors.
Why Bothering Handling Errors/Values? TLDR: Simplicity, Robustness, Easiness, and Readability.
Why Bothering Handling Errors/Values? TLDR: Simplicity, Robustness, Easiness, and Readability.
Imagine that we’re writing a function that searches a database from Spikey Bank, there are two possibilities for every name, a corresponding user can exist, and does not exist. This specific function will return both <|“Name” Money|> if the user exists, and False otherwise. A diagram of this process would be something like:
This will soon be a problem, why? This function produces two different types, and checking for these is not trivial. Let’s program everything first for demonstration.
Create an Association called spikeyBank containing names and random monetary values.
In[]:=
spikeyBank=Association[#->RandomInteger[{0,100}]&/@{"Alice","Bob","Charlie","Daniel","Elliot"}]
Out[]=
Alice58,Bob37,Charlie28,Daniel67,Elliot88
Define a function called searchUser that returns an Association if the user exists, False otherwise.
In[]:=
searchUser[name_String]:=Module[{},If[KeyExistsQ[spikeyBank,name],Return[<|name->spikeyBank〚name〛|>],Return[False]]]
Perform a user search on multiple user names.
In[]:=
searchUser/@{"Alice","Bob","Charlie"}
Out[]=
{Alice58,Bob37,Charlie28}
This function works 100% of the time, pretty cool! But.. What if we need to use this function inside another with another level of uncertainty? In this case, the “amount” can hold an Integer, or a Boolean, Let’s try..
Define a function called takeMoney that subtracts an amount from the given user’s bank account and returns an Integer, returns a String in case of an error.
In[]:=
takeMoney[name_String,amount_Integer]:=Module[{user=searchUser[name]},If[user==False,(*clunky!*)Return["No user with this name exists!"];];If[AssociationQ[user],(*clunky!*)(*onlytheselinesofcodematter..*)If[amount<=0,Return["The amount must be greater than zero!"]];Return[user-amount]]]
Take 10 monetary units from the user “Bob”.
In[]:=
takeMoney["Bob",10]
Out[]=
Bob27
Again, this function works! But we need to manually check if the user is False as a Boolean or check if the user is an Association. Which gradually makes the whole process gradually harder and harder.. Since we’re using False to represent that no user with the given name exists, we’re also implicitly using an Association to represent that a value is present, in this case, an Association as a Boolean. I could go on and on with many other implications, specially in a big project..
Checking multiple things at multiple moments can get out of control, and sometimes, be more complex than this simple case, take a look at all possible “falsy” values that a variable could have:
In[]:=
{None,Undefined,Null,False,Indeterminate,Nothing,$Failed,Missing["NotAvailable"]};
Currently, many languages such as Rust and Haskell have different approaches on dealing with values that may or may not exist. Take this Rust snippet for example:
Out[]=
If there’s Some(n), then, it will return Some(n + 2), if there’s None, it will return None. This way of dealing with the presence and absence of a value is widely considered to be robust, and more easy to maintain. Only one type is used, thus, there’s no need to check for multiple cases and types.
Now, let’s create the same functions, but these new functions will return Some values if the user exists or as results, and None otherwise. A diagram of this process would be something like:
Define a function called searchUser that returns a Some<Association> if the user exists, None otherwise.
In[]:=
searchUser[name_String]:=If[KeyExistsQ[spikeyBank,name],MaybeSome[<|name->spikeyBank〚name〛|>],MaybeNone]
Perform a user search on multiple user names and Unwrap the MaybeSome<Integer> values.
In[]:=
Unwrap/@searchUser/@{"Alice","Carlos","Bob","Fernando","Charlie"}
Out[]=
{Alice58,MaybeNone,Bob37,MaybeNone,Charlie28}
Define a function called takeMoney that subtracts an amount from the given user’s bank account and returns an ResultOk<Association>, returns ResultError<String> in case the of an error.
In[]:=
takeMoney[name_String,amount_Integer]:=Module[{},MatchMaybe[searchUser[name],(*onlywhatmatters!*)If[amount>0,ResultOk[#1-amount],(*errorsarehandledwithnoproblematallfromnowon.*)ResultError["The amount must be greater than zero!"]]&,ResultError["No user with this name exists!"]&]]
Take no monetary units from “Charlie”.
In[]:=
takeMoney["Charlie",0]
Out[]=
ResultError[The amount must be greater than zero!]
Take 10 monetary units from “Charlie”.
In[]:=
takeMoney["Charlie",10]
Out[]=
ResultOk[Charlie18]
Take 10 monetary units from an user that doesn’t exist.
In[]:=
takeMoney["Charlotte",10]
Out[]=
ResultError[No user with this name exists!]
Works like a charm! And now, it has support elegant syntax that only pipes MaybeSome<T>, and does not propagate errors or not found users! Let’s try using AndThen.
Search for a user called “Daniel”, and then, subtract 10 from it’s value.
In[]:=
searchUser["Daniel"]~AndThen~(#-10&)
Out[]=
Daniel57
Which to Use? Maybe<T> or Result<T, E>?
Which to Use? Maybe<T> or Result<T, E>?
How to choose between Maybe and Result? The answer is pretty simple!
Maybe<T>
Maybe<T>
Maybe<T> means that there can be Some<T>, or None. These enumerable type can represent if both values exist and not exist, as well as their values, at the same time. Some use cases could be:
1
.A user exists or doesn’t exist.
2
.There’s or there isn’t an element on a certain position of a List.
3
.Some value or no value from a key on a certain Association.
Result<T, E>
Result<T, E>
Result<T, E> means that there can be a successful ResultOk<T> that holds a value T, or an error ResultError<E> that holds an error E. These enumerable types can represent if a computation succeeded or not, and what went right and wrong at the same time.
1
.A connection with a database, or a connection error.
2
.A successful computation, or a computation error.
3
.A successful division, or a “division by zero” error.
Metaprogramming with Maybe<T> and Result<T, E>
Metaprogramming with Maybe<T> and Result<T, E>
One simple way of writing a function that can receive any enumerable type of Maybe and Result, is to actually use these (Maybe, Result) as functions on the arguments of the definition of a function.
Maybe[T_] returns Alternatives containing two possible enumerable types, a pattern T_MaybeSome that can hold a certain type such as an Integer or String, or a pattern T_ that is a MaybeNone.
Result[T_, Err_] returns Alternatives containing two possible enumerable types, a pattern T_ResultOk that can hold a certain type such as an Integer or String, or a pattern Err_ResultErr that can hold a certain type such as an Integer or String.
Let’s use Maybe[T] on a simple function for demonstrating:
Define a function called acceptsMaybeStringOnly that accepts only Some<String> or None.
Now, let’s try with multiple arguments..!
Matching using MatchMaybe and MatchResult
Matching using MatchMaybe and MatchResult
Instead of using If or Switch statements, you can match using MatchMaybe and MatchResult.
Maybe<T>
Maybe<T>
Result<T, E>
Result<T, E>
Piping Values using AndThen
Piping Values using AndThen
You can pipe successful values using AndThen.
Unwrap and UnwrapFlatten
Unwrap and UnwrapFlatten
You can unwrap or flatten multiple nested successful results using Unwrap or Flatten.
SomeQ, NoneQ and OkQ, ErrorQ
SomeQ, NoneQ and OkQ, ErrorQ
For additional checking, you may use many functions exclusive to the enumerable types from Maybe and Result, such as:
Check if a given value is a MaybeSome<T>.
Check if a given value is a MaybeNone<T>.
Check if a given value is a ResultOk<T>.
Check if a given value is a ResultErrorQ<E>.
Encapsulation with IntoMaybe and IntoResult
Encapsulation with IntoMaybe and IntoResult
Many functions on the Wolfram Language will, instead of giving something such as “Null”, return a Message as well as the unevaluated expression, one way of turning these types of functions into “safe” functions, is to use IntoMaybe or IntoResult.
This simple expression, always returns a Message when it cannot return a valid result, which makes it somewhat confusing on how to deal with the error.
Get the third and fourth element of a List of three elements.
Get the third and fourth element of a List of three elements, and wrap them as Maybe<T>.
If you need to know if an error happened, it’s better to use a Result<T, Part::Error>.
Get the third and fourth element of a List of three elements, and wrap them as Result<T, Part::Error>.
Installation
Installation
You can get Corrigendum.wl on the GitHub repository: https://github.com/cabralski/corrigendum.wl and move the corrigendum.wl file into the following folder:
Open the folder for moving the “corrigendum.wl” file into.