Basics 16 - Functions (Part 2) - Stitching Pure and Impure Worlds
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.ReadAllText
raises exceptions, which meansreadAllText
raises exceptionsSince
readAllText
raises exceptions, there are two options when callingreadAllText
:Wrap the call to
readAllText
in atry...with
Don't wrap the call in a
try...with
, let the exceptions be propagated upward
Both these options are problematic:
If the functions that call
readAllText
usetry...with
, you will end up with redundant and noisy codeIf
try...with
is 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
/reraise
an exception:raise (ArgumentException("Whatever"))
let ex = ArgumentException("Whatever")
,ex
is 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
readAllText
onlyreadAllText
returnsResult<string, exn>
, no matter what happensIf all goes well, you get
Ok
case with file contentIf there is an exception, you get
Error
case with exception detailsUsing the
Result
container type, you can perform pattern matching,map
,bind
etc.
Isolate Exception Handling
There is one problem with the above-given implementation of readAllText
:
Essentially, the purpose of
readAllText
is callingSystem.IO.File.ReadAllText
Rest 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
readAllTextImpl
callsSystem.IO.File.ReadAllText
Function
readAllTextImpl
may raise exceptionsreadAllText
is partial application oftryFunc1
How do you call
readAllText
?let result = readAllText "some.file.name.txt"
The type of
result
in 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
readAllTextImpl
andwriteAllTextImpl
are low-level implementations/wrappersreadAllText
andwriteAllText
are the partial applications oftryFunc1
andtryFunc2
respectively
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
Result
container 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!