0

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"
Abel
  • 56,041
  • 24
  • 146
  • 247
  • I wonder if you could do this with an operator? – John Palmer Nov 19 '15 at 08:50
  • @JohnPalmer: any idea how? Operators, afaik, take one or two arguments, not a variable amount of arguments. What I find strange, rethinking my post, is that (1) forcing `unit` on the signature disables formatting arguments, even though it already returns unit, and that (2) returning unit after the `printfn` _also_ cancels the ability to use formatting arguments... I understand why, but I don't understand why...;) – Abel Nov 19 '15 at 08:59
  • The operator idea would be to write a bunch of overrides with a different number of arguments. The problem is that the return type of `printfn` is very weird and depends on the format string – John Palmer Nov 19 '15 at 10:36
  • @JohnPalmer, I'm not quite sure how that would help making it work with the `ConditionalAttribute`, but if you have a prototypical idea that shows what you're heading to, by all means, go ahead and add an answer ;) – Abel Nov 19 '15 at 10:40

1 Answers1

3

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

John Palmer
  • 25,356
  • 3
  • 48
  • 67
  • I initially thought the operator trickery would be required but it wasn't needed. You might actually be able to mark these with the conditional anyway. – John Palmer Nov 19 '15 at 10:54
  • Ah, now I get how it works, and it works! The type inference works correctly. The call site is completely removed by JIT, all's good! (Strange, because I thought I experimented with tuples, but apparently never tried the simple approach ;). This is quite easily extensible and my previous comment to that extend is superfluous, you just need a variant for each number of arguments (and you must tuple the args). – Abel Nov 19 '15 at 11:12
  • Any idea _why_ this works with tuples and not with curried arguments? – Abel Nov 19 '15 at 11:15
  • You can't overload curried functions – John Palmer Nov 19 '15 at 11:15
  • I understand, but that's not my point, I mean, where are the curried arguments in my original post? I.e. `log x = printfn x`. Or you mean that, as a result of the StringWriterFormat etc, I end up having curried arguments anyway? But in the OP I wasn't overloading them, I just had one definition... – Abel Nov 19 '15 at 11:45
  • 1
    @Abel As a side note, it is possible to encode polyvariadic functions (with curried arguments) in F# but it's very hacky. See [this answer](http://stackoverflow.com/questions/28243963/how-to-write-a-variadic-function-in-f-emulating-a-similar-haskell-solution/28244413#28244413). – Gus Nov 19 '15 at 12:22
  • @Abel - check what the type of `printfn "test"` and `printfn "%s"` are. – John Palmer Nov 19 '15 at 23:39
  • I understand that `printfn "%s"` creates a function that takes a string. But the thing is, if I decorate `fn a b = a + b |> ignore` with `Conditional`, there is no problem, but when I decorate `fn a b = printfn "s%s" a b` then it fails, though both have unit return type. I don't see the difference. The second one, when assigned to a variable `let x = fn "x" "s"` will show `x` having unit. But F# will complain and won't accept it with `Conditional`. – Abel Nov 20 '15 at 00:21