F# 计算表达式中类似 Printf 的函数

Printf-like function in F# computation expression

我正在尝试用 F# 编写代码,允许记录到自定义源并使用生成随机数的可交换实现。

我尝试将这些函数作为计算表达式的上下文传递,而不是将日志记录 function/random 生成器传递给应用程序中的每个函数。

我注意到声明 monad 实现的要求非常相似,所以我尝试编写类似的东西。

我有概念证明工作,生成数字工作得很好,但我不能让它与我的日志记录功能的 printf 样式参数很好地工作:

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

有人可以帮助我吗?我对编写自定义计算表达式还很陌生。

编辑

注意日志函数可以有签名

let log str = Simulation (fun simulation -> simulation.logger str)

而且很容易调用:

simulate
   {
   ...
   do! Simulation.log "Hello world!"
   ...
   }

但在这里我失去了不使用 sprintf 传递格式参数的能力

编辑

绑定实现有错误,应该是:

member this.Bind (x, f) = let (Simulation(simulation)) = x in Simulation (fun simulator -> Simulation.run simulator (f (simulation simulator)))

这里的问题是你的 log 函数 returns 包装在 Simulation 中的打印函数,所以你必须使用额外的 let! 来打开它你可以使用它。

但是没有什么能阻止您将 函数的结果 包装在 Simulation 而不是函数本身:

let log = Printf.ksprintf (fun str -> Simulation (fun simulation -> simulation.logger str))

这将使您的代码编译:

do! Simulation.log "Value: %d" x

因为现在表达式 Simulation.log "Value: %d" returns 是一个函数 int -> Simulation<unit> 而不是像以前那样 Simulation<int -> unit>


但是,这也会使 log 函数单态化:编译器会看到它与单个 int 参数一起使用,并将其类型固定为接受 ints,然后如果你尝试不同的东西:

do! Simulation.log "Value: %s" "foo"

它不会再次编译,抱怨它期待一个 int,但给了一个 string

要解决这个新问题,您必须通过提供显式通用类型注释并准确指定应如何将其转换为 ksprintf:

来帮助编译器解决问题
let log (format: Printf.StringFormat<'T, _>) = 
  Printf.ksprintf (fun str -> Simulation (fun simulation -> simulation.logger str)) format

此处,'T 表示参数字符串,例如 int ->string -> bool -> 或您的格式字符串指定的任何其他内容。

有了这个,任何格式都可以编译:

do! Simulation.log "Value: %d" x
do! Simulation.log "Value: %s" "foo"
do! Simulation.log "I have %d apples which are %s" 42 "rotten"