'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:

  1. fprintfn TextWriter.Null, I believe, still formats all args and can be unnecessarily expensive
  2. kbprintfn with a continuation that conditionally calls writer.WriteLine(sb) has the same issue
  3. In C++ codebase, this kind of dynamic config is often complemented with preprocessor directives (#if ...), like, enabling TRACE 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.

f#


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