Created
December 7, 2022 19:03
-
-
Save sgoguen/0750c528a3eff01f248d2e3855eb3207 to your computer and use it in GitHub Desktop.
F# Reader Monad Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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