Search code examples
f#printfpartial-applicationdebug-buildconditional-attribute

How to leverage power of TextWriterFormat for printfn style in combination with ConditionalAttribute which requires unit result


I set myself to creating a trace function that behaves like sprintf or printfn, but is disabled (JIT removes it on call site) for Release builds by using the ConditionalAttribute.

Result so far: I don't think it is possible.

The problem centers around the fact that when you use the Conditional("DEBUG") attribute, the function must return unit result. "Normal" arguments work as they should and the method is properly decorated (EDIT: decorated, yes, but curryable members are not erased, see discussion, must use tuple-form instead):

type Trace() =    
    [<Conditional("DEBUG")>]
    static member inline trace msg b = (msg, b) |> ignore

// using it, x is unit
let x = Trace.trace "test" "foo"

(note that, without the ignore, this won't compile because of the Conditional attribute)

But as soon as I try any variant of the Printf.TextWriterFormat<'T>, it fails and I don't see a way around it:

type Trace() =
    [<Conditional("DEBUG")>]
    static member inline trace msg = printfn msg    // inline or not doesn't matter

    // using it, x is unit
    let x = Trace.trace "hello: %s" "foobar"

This works without the attribute, but with the attribute, it will raise:

This expression was expected to have type
      unit
but here has type
      string -> unit

The error is specifically underlining Trace.trace "hello: %s". So it looks like the compiler not recognizing that the whole of the expression results in a unit, and raises the error because it internally creates a wrapper function that returns string -> unit, which is not allowed by the rules of the ConditionalAttribute.

When I try to fix it by explicitly specifying :unit on the function return type, or printfn msg |> ignore as the body, then I lose the ability to use the text writer format string with type safety, in fact, it won't recognize the second argument on the call-site at all anymore.

So, while the whole of the function signature obeys CLR's rules, it seems that the inline functions that F# creates do not, at least not in this specific case.

I have tried variants, including kprintf, sprintf to see if that helped, but all to no avail.

Any ideas? Or is this one of those situations where you try to lay out a carpet and once you have it properly smoothed in one corner, it bubbles up in the other corner, and vice versa, i.e. it never fits?


PS: in case you are wondering why I want it: just trying to create a convenience function that behaves like existing Trace, but with some other functionality going on under the hood. What I currently have works, but it just takes a string, not a statically type checked arguments, so it forces users to write something like:

(sprintf "Hello %s" >> Trace.trace) "foobar"

Solution

  • An overloading based version.

    You might want some more overloads

    #if DEBUG
    type Log() =
        static member inline log(x) = printfn x
        static member inline log(x,y) = printfn x y
    #else
    type Log =
        static member inline log(x) = ()
        static member inline log(x,y) = ()
    #end
    

    update:

    So this works:

    open System.Diagnostics
    type Log() =
        [<Conditional("DEBUG")>]  
        static member log(x) = printfn x
        [<Conditional("DEBUG")>]
        static member log(x,y) = printfn x y
    

    you need to switch to the tuple form to allow for overloading and use tuple form as curried things can't be overloaded