I'm trying to write a code in F# that allows logging to custom source and using generating random number sing swappable implementations.
Instead of passing logging function/random generator to every function in the application, I'm trying to pass these functions as a context of computation expression.
I've noticed it is very similar requirement to state monad implementations, so I've tried to write something similar.
I have proof of concept working, and generating numbers is working very good, but I cannot make it to work nicely with printf style parameters for my logging function:
module Test
type Simulator = { random : int * int -> int; logger : string -> unit }
module Simulator =
let create = let random = new System.Random() in
{
random = fun (min, max) -> random.Next(min, max)
logger = fun str -> (printfn "%s" str |> ignore)
}
type Simulation<'T> = Simulation of (Simulator -> 'T)
module Simulation =
/// Runs a simulation given a simulator
let inline run state simulation = let (Simulation(play)) = simulation in play state
/// Returns a random number
let random min max = Simulation (fun simulator -> simulator.random (min, max))
/// Writes to simulation log
let log = Simulation (fun simulation -> Printf.ksprintf simulation.logger)
type SimulationBuilder() =
member this.Bind (x, f) = let (Simulation(simulation)) = x in Simulation (fun simulator -> f (simulation simulator))
member this.Return (x) = x
let simulate = new SimulationBuilder()
let simpleSimulation =
simulate
{
//very nice, working
let! x = Simulation.random 2 12
//this is working, but verbose
let! logger = Simulation.log
do logger "Value: %d" x
//I want to write
//do! Simulation.log "Value: %d" x
// or something similar
return x;
}
Simulation.run Simulator.create simpleSimulation |> ignore
Can someone help me? I'm pretty new to writing custom computation expressions.
EDIT
Note that log function could have signature
let log str = Simulation (fun simulation -> simulation.logger str)
and it would be easy to call:
simulate
{
...
do! Simulation.log "Hello world!"
...
}
but here I am losing ability to pass format parameters without using sprintf
EDIT
There is an error in bind implementation, should be:
member this.Bind (x, f) = let (Simulation(simulation)) = x in Simulation (fun simulator -> Simulation.run simulator (f (simulation simulator)))
The problem here is that your log
function returns the printing function wrapped in Simulation
, so that you have to use an extra let!
to unwrap it before you can use it.
But nothing prevents you from wrapping the function's result in Simulation
rather than the function itself:
let log = Printf.ksprintf (fun str -> Simulation (fun simulation -> simulation.logger str))
This will let your code compile:
do! Simulation.log "Value: %d" x
Because now the expression Simulation.log "Value: %d"
returns a function int -> Simulation<unit>
rather than Simulation<int -> unit>
as it did before.
However, this will also monomorphise the log
function: the compiler will see that it's used with a single int
parameter, and will fix its type to accepting int
s, and then if you try something different:
do! Simulation.log "Value: %s" "foo"
it will not compile again, complaining that it was expecting an int
, but given a string
.
To fix this new problem, you'll have to help the compiler out a bit by providing an explicit generic type annotation and specifying exactly how it should be translated to ksprintf
:
let log (format: Printf.StringFormat<'T, _>) =
Printf.ksprintf (fun str -> Simulation (fun simulation -> simulation.logger str)) format
Here, 'T
represents the string of parameters, like int ->
or string -> bool ->
or whatever else your format string specifies.
With this, any format will compile:
do! Simulation.log "Value: %d" x
do! Simulation.log "Value: %s" "foo"
do! Simulation.log "I have %d apples which are %s" 42 "rotten"