This article contains a variety of code samples demonstrating common usages for various parts of the Rascal library. All the samples source code can be found here.
Creating results
Results in Rascal can be created in a variety of ways, the two most common of which are through the Ok
and Err
methods defined in the prelude, or through implicitly converting ok values or errors into results.
// You can create a result either through explicit Ok/Error functions...
var explicitOk = Ok(new Person("Melody", 27));
var explicitError = Err<Person>("Could not find person");
// ... or through implicit conversions...
Result<Person> implicitOk = new Person("Edwin", 32);
Result<Person> implicitError = new StringError("Failed to find person");
Mapping
"Mapping" refers to taking a result containing some value some type (T
) and mapping said value to a new value of some other type (TNew
). The principal method of mapping is the aptly named Map
.
var name = "Raymond";
// Read console input and try parse it into an int.
// If the input cannot be parsed, the result will be an error.
var age = ParseR<int>(Console.ReadLine()!);
// Map the age to a new person.
// If the age is an error, the person will also be an error.
var person = age.Map(x => new Person(name, x));
Another operation, commonly referred to as "bind" or "chaining", exists, which looks quite similar to mapping, the only difference being that the lambda you supply to the method returns a new result rather than a plain value. The principal method of chaining is Then
, which can be read as "a, then b, then c".
// Read console input and assert that it isn't null.
// If the input is null, the value will be an error.
var name = Console.ReadLine().NotNull();
// Chain an operation on the name which will only execute if the name is ok.
var person = name.Then(n =>
{
// Read console input, assert that it isn't null, then try parse it into an int.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str));
// Map the age into a new person.
return age.Map(a => new Person(n, a));
});
Map
and Then
together make up the core of the Result<T>
type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes Result<T>
a functor and monad respectively (although not an applicative).
Tip
The aliases Select
and SelectMany
are available for Map
and Then
respectively. These exist to supply support for query expressions as an alternative to method chaining. Query syntax can in specific situations be more readable than the method chaining alternative, although in most scenarios, method chaning is better. Select
and SelectMany
should not be used outside query syntax.
Combine
Combine
is an addition to Map
and Then
which streamlines the specific case where you have two results and want to combine them into a single result only if both results are ok.
// Read console input and assert that it isn't null.
var name = Console.ReadLine().NotNull();
// Read console input, assert that it isn't null, then try parse it into an int.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str));
// Combine the name and age results together, then map them into a person.
var person = name.Combine(age)
.Map(v => new Person(v.first, v.second));
Validation
Rascal supports a simple way of validating the value of a result, returning an error in case the validation fails.
// Read console input, assert that it isn't null, and validate that it matches the regex.
var name = Console.ReadLine().NotNull()
.Validate(
str => Regex.IsMatch(str, "[A-Z][a-z]*"),
_ => "Name can only contain characters a-z and has to start with a capital letter.");
var person = name.Then(n =>
{
// Read console input, assert that it isn't null, try parse it into an int, then validate that it is greater than 0.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str))
.Validate(
x => x > 0,
_ => "Age has to be greater than 0.");
return age.Map(a => new Person(n, a));
});
Exception handling
One of the major kinks of adapting C# into a more functional style (such as using results) is the already existing standard of using exceptions for error-handling. Exceptions have many flaws, and result types explicitly exist to provide a better alternative to exceptions, but Rascal nontheless provides a way to interoperate with traditional exception-based error handling.
The Try
method in the prelude is the premiere exception-handling method, which runs another function inside a try
-catch
block, and returns an ExceptionError
in case an exception is thrown.
// Try read console input and use the input to read the specified file.
// If an exception is thrown, the exception will be returned as an error.
var text = Try(() =>
{
var path = Console.ReadLine()!;
return File.ReadAllText(path);
Try
variants also exist for Map
and Then
, namely TryMap
and ThenTry
.
// Read console input and assert that it isn't null.
var path = Console.ReadLine().NotNull();
// Try to map the input by reading a file specified by the input.
// If ReadAllText throws an exception, the exception will be returned as an error.
var text = path.TryMap(p => File.ReadAllText(p));