Search code examples
f#monadscomputation-expression

Printf-like function in F# computation expression


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)))

Solution

  • 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 ints, 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"