Skip to content

Instantly share code, notes, and snippets.

@sgoguen
Created December 7, 2022 19:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sgoguen/0750c528a3eff01f248d2e3855eb3207 to your computer and use it in GitHub Desktop.
Save sgoguen/0750c528a3eff01f248d2e3855eb3207 to your computer and use it in GitHub Desktop.
F# Reader Monad Example
type Reader<'Env, 'T> = Reader of ('Env -> 'T)
module Reader =
let run (Reader f) env = f env
let ret x = Reader(fun _ -> x)
let ask = Reader(fun env -> env)
let zero() = Reader(fun _ -> ())
let map f (Reader g) = Reader(fun env -> f (g env))
let delay (f: unit -> Reader<'TEnv, 'T>) =
Reader(fun env -> run (f()) env)
let bind (Reader f) g =
Reader(fun env -> run (g (f env)) env)
let retFrom (Reader f) = Reader(fun env -> f env)
let mergeSources (Reader f) (Reader g) =
Reader(fun env -> let x = f(env) in (x, g(env)))
type ReaderBuilder() =
member __.Bind(x, f) = bind x f
member __.Return(x) = ret x
member __.ReturnFrom(x) = retFrom x
member __.Zero() = zero()
let reader = ReaderBuilder()
module ReaderTests =
open Reader
type IConsole =
abstract member WriteLine : string -> unit
abstract member ReadLine : unit -> string
type IRandomNumberGenerator =
abstract member NextInRange : int * int -> int
type INumberDb =
abstract member CheckedNumber : int -> bool
abstract member SetNumber : int -> unit
type IEnv =
inherit IConsole
inherit IRandomNumberGenerator
inherit INumberDb
let playGame =
reader {
let! (env: IEnv) = Reader.ask
let! (console: #IConsole) = Reader.ask
let! (db: #INumberDb) = Reader.ask
let! (rng: #IRandomNumberGenerator) = Reader.ask
let rec loop() =
let secretNumber = rng.NextInRange(1, 10)
console.WriteLine("Guess a number between 1 and 10")
let x = int(console.ReadLine())
if db.CheckedNumber(x) then
console.WriteLine("You already guessed that number")
loop()
else
db.SetNumber(x)
if x = secretNumber then
console.WriteLine("You won!")
else
console.WriteLine("You lost!")
loop()
loop()
}
let fakeConsole (inputs: string list) =
// This console replays the user's guesses
let mutable guesses = inputs
{ new IConsole with
member __.WriteLine(x) = printfn "%s" x
member __.ReadLine() =
let x = List.head guesses
guesses <- List.tail guesses
x }
// This fake number number generator cycles through the numbers inputed
// and starts over when it reaches the end
let fakeRandomNumberGenerator (numbers: int[]) =
let mutable i = 0
{ new IRandomNumberGenerator with
member __.NextInRange(x, y) =
let n = numbers.[i]
i <- i + 1 % numbers.Length
n }
let fakeNumberDb (numbers: int[]) =
let mutable checkedNumbers = Set.empty
{ new INumberDb with
member __.CheckedNumber(x) = Set.contains x checkedNumbers
member __.SetNumber(x) = checkedNumbers <- Set.add x checkedNumbers }
// Create a test environment that combines the above fakes
let createEnvironment (console: IConsole) (rng: IRandomNumberGenerator) (db: INumberDb) =
{ new IEnv
interface IConsole with
member __.WriteLine(x) = console.WriteLine(x)
member __.ReadLine() = console.ReadLine()
interface IRandomNumberGenerator with
member __.NextInRange(x, y) = rng.NextInRange(x, y)
interface INumberDb with
member __.CheckedNumber(x) = db.CheckedNumber(x)
member __.SetNumber(x) = db.SetNumber(x) }
let testEnvironment =
let console = fakeConsole ["5"; "10"; "7"]
let rng = fakeRandomNumberGenerator [|7|]
let db = fakeNumberDb [||]
createEnvironment console rng db
run playGame testEnvironment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment