I have a couple of wrappers of Printf-family API (e.g. for logging)
type Logger(writer: TextWriter) =
...
member x.Log (fmt: PrintfFormat<'Printer, _, _, _>): 'Printer =
Printf.fprintfn writer fmt
Let's say I want to introduce the notion of "log level" to it, add Logger.Info(), Debug(), Trace() etc. which are basically another layer on top of Logger.Log(), and if logger.Level <- LogLevel.Info then Debug() and Trace() should turn into no-ops. Pretty common requirement, I'd say.
But I can't seem to return a 'Printer-typed value without actually calling any of Printf-family API.
type LogLevel = TRACE | DEBUG | INFO
type LevelLogger(writer: TextWriter, level: LogLevel) =
inherit Logger(writer)
member x.Info (fmt: PrintfFormat<'Printer, _, _, _>): 'Printer =
if level <= LogLevel.INFO then
x.Log fmt
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
warning FS0064: This construct causes code to be less generic than indicated by the type annotations. The type variable 'Printer has been constrained to be type 'unit'.
let l0 = Logger(Console.Error)
l0.Log "foo %d %s %f" 1 "foo" 1.23
let l1 = LevelLogger(Console.Error, LogLevel.INFO)
l1.Info "foo %d %s %f" 1 "foo" 1.23
^^^^^^^^^^^^^^^^^^^^^^
error FS0001: This expression was expected to have type
'unit'
but here has type
''a -> string -> 'b -> unit'
error FS0003: This value is not a function and cannot be applied.
It would be ideal if I could write
member x.Info (fmt: PrintfFormat<'Printer, _, _, _>): 'Printer =
if level <= LogLevel.INFO then x.Log fmt
else Printf.noop fmt
But does there exist such a utility API? Or can we write one?
Workarounds just for "getting things done" that I'm aware of:
fprintfn TextWriter.Null, I believe, still formats all args and can be unnecessarily expensivekbprintfnwith a continuation that conditionally callswriter.WriteLine(sb)has the same issue- In C++ codebase, this kind of dynamic config is often complemented with preprocessor directives (
#if...), like, enablingTRACElevel only under-DDEBUG; but I don't know if we can write an ergonomic API with them in F#.
Also, PrintfFormat is always allocated out of a format string anyways, then the "no-op" API can never be a real no-op? But I'm hoping it might still be much light-weight than TextWriter.Null, if that's ever possible.
Ahhh why is PrintfImpl internal??
https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/printf.fs
Update: My attempts so far
https://sharplab.io/#gist:70dd6f915ba14f36c122f601732af1c1
LevelLogger0 doesn't type check, and my understanding is that both LevelLogger1 and 2 would waste CPU & RAM in logger.Trace "%A" (Array.zeroCreate<double> 1_000_000_000) even when the current threshold is INFO.