The IO Monad for F#
If you have any experience in Haskell, you know the IO monad. Some people think it just makes things ugly – but I think it is good, since we are functional programming and we should know when we are using an impure function.
What we will do is to define an IO<’T> type and an io monad (which returns an IO<’T>). let!, use!, do! and return! will assist us, the rest will be just as usual.
Since most impure functions do not return IO<’T>, we will use the iofy function which “IOfies” functions (“overloaded” for different number of arguments: iofy, iofy2, iofy3, iofy4, iofy5) and hand-IOfying.
This isn’t a hard task at all. Here is IOBuilder.fs:
namespace IOMonad
type IO<'a> = IO of 'a
type IOBuilder() =
member this.Return value =
IO value
member this.Bind ((IO(value)), cexpr) : IO<_> =
cexpr value
member this.Using ((value : #System.IDisposable), cexpr) =
let result = cexpr value
value.Dispose()
result
member this.Zero() = IO ()
member this.Combine ((_ : IO<_>), cexpr) : IO<_> = cexpr()
member this.Delay (cexpr : unit -> IO<_>) = cexpr
member this.For ((values : #seq<_>), (cexpr : (_ -> IO<unit>))) =
for i in values do
cexpr i |> ignore
IO ()
member this.While (condition, cexpr) =
while condition() do
cexpr() |> ignore
IO ()
member this.TryWith (cexpr, handler) =
try
cexpr()
with e -> handler e
member this.TryFinally (cexpr, final) =
try
cexpr()
finally
final()
module IOUtils =
let io = IOBuilder()
let iofy (f : 'a -> 'b) =
(fun a0 -> IO (f a0))
let iofy2 (f : 'a -> 'b -> 'c) a0 a1 =
IO (f a0 a1)
let iofy3 (f : 'a -> 'b -> 'c -> 'd) a0 a1 a2 =
IO (f a0 a1 a2)
let iofy4 (f : 'a -> 'b -> 'c -> 'd -> 'e) a0 a1 a2 a3 =
IO (f a0 a1 a2 a3)
let iofy5 (f : 'a -> 'b -> 'c -> 'd -> 'e -> 'f) a0 a1 a2 a3 a4 =
IO (f a0 a1 a2 a3 a4)
This code is quite simple, almost a disgrace for the monad system.
I think this covers every, here is my test script:
#load "IOBuilder.fs"
open IOMonad.IOUtils
let console_readline = iofy (System.Console.ReadLine)
let fake_readline = io { return "4" }
let fake_wrong_readline = io { return "2.0" }
let console_writeline = iofy (fun (s : string) -> System.Console.WriteLine s)
let main =
io {
let! input = fake_readline()
let int_input = int input
use! str = io { return new System.IO.StreamReader(@"C:\TheArtofUnitTesting.pdf") } <| ()
while false do
do! console_writeline "test is not a number"
for x in 0 .. int_input do
do! console_writeline (string x)
try
int "test" |> ignore
with
| _ -> do! console_writeline "test is not a number"
try
try
let! input2 = fake_wrong_readline()
let int_input2 = int input2
do! console_writeline "2.0 is an integer"
finally
console_writeline "Did it work?" |> ignore
with _ -> do! console_writeline "2.0 is not an integer"
try
let! input2 = fake_wrong_readline()
let i = int input2
do! console_writeline "2.0 is an integer"
with :? System.FormatException -> do! console_writeline "2.0 is not an integer"
return 2
}
let main2 arg =
io {
let! main = main()
do! console_writeline (main.ToString())
do! console_writeline arg
} <| ()
main2 "Done here!"
This allows us to mark everything we want as “IO”.
Every function which interacts with the user or affects the environment should return an IO.
But what about impure functions which are not IO, such as random number generators, GET web requests and reading of files? These are neither interacting with the user nor affecting the environment, but they are affected by the environment. I like to mark them with a special ImpureFunction attribute:
[<AttributeUsage(AttributeTargets.Method)>]
type ImpureFunctionAttribute() =
inherit System.Attribute()
So we’d do this:
let random() =
4
[<ImpureFunction>]
let random2() =
System.Random().Next(1, 7)
You can also say that “impure functions are functions that might return different values for the same parameters”.