Learning-FSharp/Ch18-ResultPipeline/Program.fs
Learning-FSharp/Ch18-ResultPipeline/Comprehensive.fs
Check out all the comments that are placed in the source file.
Limitation of Option Container
In the last article, we discussed how to build a pipeline using the option
container.
There is a major limitation with the option
container. The option
container has two cases; Some
or None
. When in None
state, there is no reason or explanation. The Result
container on the other hand has Ok
of 'T
case and Error
of 'TError
state, which means in Error
case you can hold data. That data may be a string
, an Exception
, or any other piece of information.
The concepts which are used to build pipelines for the Result
containers are the same as we used for option
containers:
Mapping functions: both input and output are real data; used with
Result.map
Binding functions: real input, lifted output; used with
Result.bind
For the examples in this article, we will use Student
record type:
type Student =
{ Name: string
Age: int
RollNo: string }
Validation Pipeline with Result.bind
Check out validateStudent
function where the pipeline is implemented.
ent -> Result<Student, string>
let validateName (student: Student) =
if String.IsNullOrWhiteSpace(student.Name) then
Error "Name can't be empty!"
else
Ok student
// Student -> Result<Student, string>
let validateAge (student: Student) =
if student.Age < 10 || student.Age > 20 then
Error "Age must be between 10-20!"
else
Ok student
// Student -> Result<Student, string>
let validateRollNo (student: Student) =
if String.IsNullOrWhiteSpace(student.RollNo) then
Error "Roll number can't be empty!"
else
Ok student
// Student -> Result<Student, string>
let validateStudent (student: Student) =
Ok student
|> Result.bind validateName
|> Result.bind validateAge
|> Result.bind validateRollNo
// Validation Example 1
let student =
{ Name = "First Last"
Age = 10
RollNo = "Whatever" }
match validateStudent student with
| Ok(_) -> "Valid student record."
| Error(err) -> $"Invalid student record. {err}"
|> printfn "%s" // Valid student record.
// Validation Example 2
let student =
{ Name = ""
Age = 10
RollNo = "Whatever" }
match validateStudent student with
| Ok(_) -> "Valid student record."
| Error(err) -> $"Invalid student record. {err}"
|> printfn "%s" // Invalid student record. Name can't be empty!
// Validation Example 3
let student =
{ Name = "First Last"
Age = 5
RollNo = "Whatever" }
match validateStudent student with
| Ok(_) -> "Valid student record."
| Error(err) -> $"Invalid student record. {err}"
|> printfn "%s" // Invalid student record. Age must be between 10-20!
// Validation Example 4
let student =
{ Name = "First Last"
Age = 10
RollNo = "" }
match validateStudent student with
| Ok(_) -> "Valid student record."
| Error(err) -> $"Invalid student record. {err}"
|> printfn "%s" // Invalid student record. Roll number can't be empty!
Transformation Pipeline with Result.map
Check out transformStudent
function where the pipeline is implemented.
let nameToUpper (student: Student) =
{ student with
Name = student.Name.ToUpper() }
// Student -> Student
let agePlus5 (student: Student) = { student with Age = student.Age + 5 }
// Student -> Student
let rollnoToUpper (student: Student) =
{ student with
RollNo = student.RollNo.ToUpper() }
// Student -> Student
let transformStudent (student: Student) =
Ok student
|> Result.map nameToUpper
|> Result.map agePlus5
|> Result.map rollnoToUpper
let student =
{ Name = "First Last"
Age = 10
RollNo = "Whatever" }
match transformStudent student with
| Ok(student) -> $"{student.Name}, {student.Age}, {student.RollNo}."
| Error(err) -> $"Invalid student record. {err}"
|> printfn "%s" // FIRST LAST, 15, WHATEVER.
Comprehensive Example
In this example we will do the following:
Read a text file,
existing_student.txt
, which should have 3 lines of textName
Age
RollNo
Perform validations (as given in the validation example above)
Perform transformations (as given in the validation example above)
Write a text file,
new_student.txt
, with the same 3 lines as mentioned abovePrint success or failure details
File IO Functions
Here's the implementation of wrappers for System.IO.File.ReadAllLines
and System.IO.File.WriteAllLines
.
For details on tryFunc1
and tryFunc1
, check out Basics 16 - Functions (Part 2).
// Wrapper for System.IO.File.ReadAllLines
// May raise exceptions
// string -> string array
// file -> lines
let readAllLinesImpl file = File.ReadAllLines(file)
// Wrapper for System.IO.File.WriteAllLines
// May raise exceptions
// string -> string array -> unit
// file -> lines -> unit
let writeAllLinesImpl file lines = File.WriteAllLines(file, lines)
// Reads the content of given text file as lines
// string -> Result<string array, exn>
let readAllLines = tryFunc1 readAllLinesImpl
// Writes the content (lines) for the specified text file
// string -> string array -> Result<unit, exn>
// file -> lines -> result
let writeAllLines = tryFunc2 writeAllLinesImpl
Validation and Transformation Functions
Here we have validation and transformation functions. Here we deal with Result<Student, exn>
:
//////////////////////////
// Validation Functions //
//////////////////////////
// Student -> Result<Student, exn>
let validateName (student: Student) =
if String.IsNullOrWhiteSpace(student.Name) then
Error(Exception("Name can't be empty!"))
else
Ok student
// Student -> Result<Student, exn>
let validateAge (student: Student) =
if student.Age < 10 || student.Age > 20 then
Error(Exception("Age must be between 10-20!"))
else
Ok student
// Student -> Result<Student, exn>
let validateRollNo (student: Student) =
if String.IsNullOrWhiteSpace(student.RollNo) then
Error(Exception("Roll number can't be empty!"))
else
Ok student
//////////////////////////////
// Transformation Functions //
//////////////////////////////
// Student -> Student
let nameToUpper (student: Student) =
{ student with
Name = student.Name.ToUpper() }
// Student -> Student
let agePlus5 (student: Student) = { student with Age = student.Age + 5 }
// Student -> Student
let rollnoToUpper (student: Student) =
{ student with
RollNo = student.RollNo.ToUpper() }
Student (to/from) Text Lines
Here we have functions to convert Student
to text lines and text lines to Student
:
// Converts given lines of text to Student
// string array -> Result<Student, exn>
let linesToStudent (lines: string array) =
let mutable age = 0
if lines.Length = 3 && Int32.TryParse(lines[1], &age) then
Ok
{ Name = lines[0]
Age = age
RollNo = lines[2] }
else
Error(Exception("Failed to parse file content."))
// Converts given student to lines of text
// Student -> string array
let studentToLines (student: Student) =
[| student.Name; student.Age.ToString(); student.RollNo |]
The Pipeline
Finally, here's the pipeline:
// Pipeline implementation
// string -> string -> string
let comprehensivePipeline inputFile outputFile =
readAllLines inputFile
|> Result.bind linesToStudent
|> Result.bind validateName
|> Result.bind validateAge
|> Result.bind validateRollNo
|> Result.map nameToUpper
|> Result.map agePlus5
|> Result.map rollnoToUpper
|> Result.map studentToLines
|> Result.bind (writeAllLines outputFile)
|> function
| Ok(_) -> "All Done!"
| Error(ex) -> $"Failed. {ex.Message}"
Once everything is ready, you may call:
comprehensivePipeline "existing_student.txt" "new_student.txt" |> printfn "%s"
Concluding Remarks
The comprehensive example shown above covers a lot:
File IO (which may fail)
Student to/from text lines (which may fail)
Validations (which may fail)
Transformations
Once you have your functions ready, stitching a pipeline isn't difficult
Once the pipeline is written, it is elegant and easy to understand
If you have reached so far, congratulations.
Keep reading!