Basics 16 - Functions (Part 2) - Stitching Pure and Impure Worlds
Love and hate relationship with programming. Always learning programming and Jazz.
This article is a continuation of Basics 04 - Functions (Part 1).
Learning-FSharp/Ch16-FunctionStitchingPureAndImpure/Program.fs
Check out all the comments that are placed in the source file.
Recap
Breaking an application down into the pure and impure worlds
The pure world is where we have pure functions, the business logic
The impure world is one where we have uncertainty, filesystem, network, HTTP, APIs, exceptions etc.
The Impure World and Result Container
Let's take an example. You need to create a function to read text file content:
// string -> string
let readAllText file = System.IO.File.ReadAllText(file)
Some problems you'll face when using readAllText:
System.IO.File.ReadAllTextraises exceptions, which meansreadAllTextraises exceptionsSince
readAllTextraises exceptions, there are two options when callingreadAllText:Wrap the call to
readAllTextin atry...withDon't wrap the call in a
try...with, let the exceptions be propagated upward
Both these options are problematic:
If the functions that call
readAllTextusetry...with, you will end up with redundant and noisy codeIf
try...withis not used by the functions that callreadAllText, whose problem exception handling is then?
Another major problem is how to integrate such a function with the pure world. Remember there are no exceptions in the pure world.
Exceptions: Flow Control vs Data
There are two ways to look at exceptions in F#/.NET:
When you
raise/reraisean exception:raise (ArgumentException("Whatever"))let ex = ArgumentException("Whatever"),exis just another piece of data
try...with and raise etc. are flow control tactics. My learning of two decades with flow control is to avoid it as much as possible.
Let's implement an alternative version of readAllText, this time, for the callers there is no flow control involved:
// string -> Result<string, exn>
let readAllText file =
try
Ok(System.IO.File.ReadAllText(file))
with ex ->
Error ex
Benefits of this approach:
The flow control, exception handling etc. is isolated; it is the problem of
readAllTextonlyreadAllTextreturnsResult<string, exn>, no matter what happensIf all goes well, you get
Okcase with file contentIf there is an exception, you get
Errorcase with exception detailsUsing the
Resultcontainer type, you can perform pattern matching,map,bindetc.
Isolate Exception Handling
There is one problem with the above-given implementation of readAllText:
Essentially, the purpose of
readAllTextis callingSystem.IO.File.ReadAllTextRest everything is boilerplate, repetitive, control flow code
In reality, one creates many such functions... reading text, writing text, reading binary, writing binary, dealing with streams, sockets, HTTP calls and so on
If you follow the approach given above, you will end up with tons of functions having the same template
Let's try to fix this by creating functions that isolate exception handling:
// Helper function for 1 param function
// Helper function for trying f when f is 'a -> 'b
// f: ('a -> 'b) -> p: 'a -> Result<'b, exn>
let tryFunc1 f p =
try
Ok(f p)
with ex ->
Error ex
// Helper function for 2 param function
// Helper function for trying f when f is 'a -> 'b -> 'c
// f: ('a -> 'b -> 'c) -> p1: 'a -> p2: 'b -> Result<'c, exn>
let tryFunc2 f p1 p2 =
try
Ok(f p1 p2)
with ex ->
Error ex
// Helper function for 3 param function
// Helper function for trying f when f is 'a -> 'b -> 'c -> 'd
// f: ('a -> 'b -> 'c -> 'd) -> p1: 'a -> p2: 'b -> p3: 'c -> Result<'d, exn>
let tryFunc3 f p1 p2 p3 =
try
Ok(f p1 p2 p3)
with ex ->
Error ex
Let discuss the tryFunc1 function. The other functions are its cousins only.
tryFunc1 takes two parameters, a function f, and the parameter value for f. tryFunc1 calls f in the try block, and wraps the output of f in Ok case. In case of an exception, the with block wraps the exception in Error case.
Let's rewrite readAllText again:
// Helper function for 1 param function
// Helper function for trying f when f is 'a -> 'b
// f: ('a -> 'b) -> p: 'a -> Result<'b, exn>
let tryFunc1 f p =
try
Ok(f p)
with ex ->
Error ex
// Wrapper for System.IO.File.ReadAllText
// May raise exceptions
// string -> string
// file -> content
let readAllTextImpl file = System.IO.File.ReadAllText(file)
// Reads the content of given text file
// string -> Result<string, exn>
let readAllText = tryFunc1 readAllTextImpl
Function
readAllTextImplcallsSystem.IO.File.ReadAllTextFunction
readAllTextImplmay raise exceptionsreadAllTextis partial application oftryFunc1How do you call
readAllText?let result = readAllText "some.file.name.txt"The type of
resultin the above given-line isResult<string, exn>
Let's implement writeAllText, which is for writing text file content.
// Helper function for 1 param function
// Helper function for trying f when f is 'a -> 'b
// f: ('a -> 'b) -> p: 'a -> Result<'b, exn>
let tryFunc1 f p =
try
Ok(f p)
with ex ->
Error ex
// Helper function for 2 param function
// Helper function for trying f when f is 'a -> 'b -> 'c
// f: ('a -> 'b -> 'c) -> p1: 'a -> p2: 'b -> Result<'c, exn>
let tryFunc2 f p1 p2 =
try
Ok(f p1 p2)
with ex ->
Error ex
// Wrapper for System.IO.File.ReadAllText
// May raise exceptions
// string -> string
// file -> content
let readAllTextImpl file = System.IO.File.ReadAllText(file)
// Wrapper for System.IO.File.WriteAllText
// May raise exceptions
// string -> string -> unit
// file -> content -> unit
let writeAllTextImpl file content =
System.IO.File.WriteAllText(file, content)
// Reads the content of given text file
// string -> Result<string, exn>
let readAllText = tryFunc1 readAllTextImpl
// Writes the content for the specified text file
// string -> string -> Result<unit, exn>
// file -> content -> result
let writeAllText = tryFunc2 writeAllTextImpl
readAllTextImplandwriteAllTextImplare low-level implementations/wrappersreadAllTextandwriteAllTextare the partial applications oftryFunc1andtryFunc2respectively
Concluding Remarks
Exceptions are not the only uncertainties when stitching pure and impure worlds
For instance, when using asynchronous constructs, the task is an uncertainty
Not only can a task fail, but you also have to wait for its completion
Using
Resultcontainer type is not the only option, there are other mechanisms (which we will discuss later)Key points:
Avoid flow control as much as possible
Deal with data, not flow control
Where flow control is not avoidable, isolate it, and create wrappers
If you have reached so far, congratulations.
Keep reading!