'F#: is no-op printf possible?
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 expensivekbprintfn
with 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, enablingTRACE
level 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
.
Solution 1:[1]
I tried the same a time ago and did not find a solution.
Meanwhile string interpolation was added and I find it better at all ends over F#s safe but (as we see here a bit tricky) printf. String interpolation is type safe, improves readability and leads to less brackets. Another disadvantage of printf is its behavior during debugging.
I do logging with an extension method that has the Conditional attribute
type ILogger with // this is Serilog.ILogger which I use
[<Conditional("LOG")>]
member o.InformationConditional(s: string) =
o.Information(s)
to be used like
let a = 72
let b = "hello"
logger.InformationConditional($"foo {a} bar {b}")
Keeping with printf you can create the string with sprintf.
Solution 2:[2]
The easiest way to get something like your Printf.noop
is to use Printf.kprintf
, which takes a continuation that is called with the composed string. If you pass ignore
as the continuation, this does nothing. However, this unfortunately does not quite work:
member x.Info (fmt: PrintfFormat<'Printer, _, _, _>): 'Printer =
if level <= LogLevel.INFO then x.Log fmt
else Printf.kprintf ignore fmt
This would logically do the trick, but the issue is that the type of fmt
expected by kprintf
and fprintf
differs (in that they have a different state). So, if you want to do this, you will always need to use kprintf
and then decide what to do based on the log level inside the continuation:
member x.Info (fmt: PrintfFormat<'Printer, _, _, _>): 'Printer =
fmt |> Printf.kprintf (fun s ->
if level <= LogLevel.INFO then x.Log "%s" s)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | citykid |
Solution 2 | Tomas Petricek |