0

Background: I am trying to expand my existing logging framework, which is currently a wrapper of static non-thread-safe methods over printfn and friends. My design goals are: having a generic interface ILog and concrete classes like Dbg, Log, Trace. I cannot use modules and functions, unfortunately, because I like to leverage the ConditionalAttribute on the methods.

I have a working approach that looks something like this:

type ILog<'T, 'U, 'V> =
    abstract member log: 'T -> unit
    abstract member log: 'U * 'V -> unit

type Dbg<'T, 'U, 'V when 'T :> Printf.StringFormat<string> and 'U :> Printf.StringFormat<'V -> string>>() =
    static member inline _do_other_stuff() = "other stuff"

    static member inline private _helper() = 
        printfn "%s %s" (Dbg<_,_,_>._do_other_stuff())

    interface ILog<'T, 'U, 'V> with
        [<Conditional("DEBUG")>]  // removed from call site
        member this.log(msg) = sprintf msg |> Dbg<_,_,_>._helper()
        [<Conditional("DEBUG")>]  // removed from call site
        member this.log(fmt, a) = sprintf fmt a |> Dbg<_,_,_>._helper()

module TestMe =
    let test() = 
        let dbg = new Dbg<_,_,_>() :> ILog<_,_,_>
        dbg.log("test%i", 2)

Here, the compiler and syntax checker and coloring detects properly the dbg.log("test%i", 2) line, which is exactly what I want. It will also properly raise an error if I were to write "test%s" instead.

Now, if I take the above approach and expand it to create more overloads of the ILog.log method, this gets pretty hairy pretty quickly because of all the type annotations and the required use of syntax like Dbg<_,_,_>. Some of it can be hidden away, but still I figured there must be a better way.

One approach I tried, which seemed very FSharpish is:

type ILog =
    abstract member log: _ -> unit
    abstract member log: _ * _ -> unit

This compiles the interface and infers the type to be 'a0 and 'a0 -> 'a1 respectively (which seems wrong, why is the second log member getting the same 'a0?). But, I can't find any way of implementing such overly generic interface:

type Dbg() =
    interface ILog with
        member this.log v = v + 1 |> ignore      // it won't infer int here

It raises:

The declared type parameter '?' cannot be used here, since the type parameter cannot be resolved at compile time.

Does F# 4.0 have a way of declaring interfaces in a more generic way, or am I stuck to having to declare them specifically (while this works, it is tedious).

Abel
  • 56,041
  • 24
  • 146
  • 247
  • indeed you are close but I think you don't fully understand how generic you made your `ILog` - first: no fear: `a0` there is the same name for a generic parameter in two different functions - that does not mean that both types have to fit because **it can be any type at all** - and that exactly is the problem with your implementation: it should accept any type but yours only accept `int`s! – Random Dev Nov 25 '15 at 06:57
  • but IMO your original design was a good fit – Random Dev Nov 25 '15 at 06:59
  • @Carsten, ok, that seems to make some sense, but how can I either leverage that power and automatically restrict it to the `Printf.xxx` types without getting totally polluted code for more overloads? And my original design goes berzerk by the time I have three more overloads... – Abel Nov 25 '15 at 06:59
  • IMO that's the price you have to pay - you could use reflection .. but meh ... as I said this seems not to bad to me (the picture for the libs. users should be me nicer thanks to type-inference) – Random Dev Nov 25 '15 at 07:05
  • Logging is a cross-cutting concern. In FP, it's better addressed with function composition: http://stackoverflow.com/q/24755201/126014 – Mark Seemann Nov 25 '15 at 07:16
  • @MarkSeemann, I agree, but using partial application (as the `printf` family of functions does) means I cannot use class members. And not using class members means I cannot use the `ConditionalAttribute`, which is vital here (F# does not allow that attribute on curried functions). About decoupling: that is kinda the whole point here ;). – Abel Nov 25 '15 at 07:19
  • The point about function composition is that you get to decide what to compose. You can do that at compile-time as well. If you don't want the logging compiled in, then don't compose those functions when you compile... – Mark Seemann Nov 25 '15 at 07:27
  • @MarkSeemann, I hope you're not suggesting to have every log statement wrapped inside `#if...#endif`, because that's exactly what I am trying to prevent. While at the same time keeping type safety with the `Printf.StringFormat` type. If you know of another way to achieve both without having to use OO style with `Conditional`, by all means, enlighten me :) – Abel Nov 25 '15 at 07:33
  • You don't have to wrap every *expression* wrapped in `#if..#endif`, but you can wrap the logging *functions* themselves in `#if..#endif`. If I understand correctly, you only have a handful of those anyway, like `logInformation`, `logError`, and so on. Turn those functions into no-ops if the relevant compilation flag isn't set. – Mark Seemann Nov 25 '15 at 07:38
  • @MarkSeemann, that won't help, because it will lead to different implementations in the logger assembly having those attributes depending on compile settings. The trick here is to remove them from the call site, *not* remove the implementations, which is why the `ConditionalAttribute` is there for in the first place. – Abel Nov 25 '15 at 07:41
  • Then do it when you *compose* the functions together... – Mark Seemann Nov 25 '15 at 07:49
  • @MarkSeemann, when you compose the functions together, F# does not support the `ConditialAttribute`, which requires `unit` and that is not detected even if you compose them correctly (probably because partial composed functions technically have no `unit` return type). – Abel Nov 25 '15 at 07:53
  • I understand that. Compose them using `#if..#endif`. Composition should happen in a central part of your application anyway, so you'd only need a handful of those conditional compilation statements. – Mark Seemann Nov 25 '15 at 07:55
  • @MarkSeemann, forgive me, but I think I'm a bit lost now to your design suggestions. It may be viable, I don't know, perhaps you can suggest it as an answer? But it still sounds like getting different builds and losing (some) compile time checking, but I'm not sure... – Abel Nov 25 '15 at 07:57
  • I though it was a poor fit for an answer, because that's not really what your question is about, so instead, I wrote a [blog post](http://blog.ploeh.dk/2015/11/30/to-log-or-not-to-log). HTH – Mark Seemann Nov 30 '15 at 08:50
  • @MarkSeemann, sorry for this late response, but I did read your blog post at the time. It's a good summary to some well-known best practices applied to F#, but indeed it is not quite what I was after here. – Abel Dec 18 '15 at 23:29

0 Answers0