Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trouble understanding type inference when returning function from function in FSharp

Tags:

f#

I'm using log4net in my F# application, and wanted to make logging more idiomatic F# by creating a few functions.

For warnings I created the following function:

let log = log4net.LogManager.GetLogger("foo")
let warn format = Printf.ksprintf log.Warn format

This works pretty well, and I can do:

warn "This is a warning"
// and
warn "The value of my string is %s" someString

However I wanted to make this more generic and make it possible to pass in the logger I wanted. Thus I wrapped the function in another function that can take an ILog as parameter:

let getWarn (logger: log4net.ILog) =
    fun format -> Printf.ksprintf logger.Warn format

and I also tried:

let getWarn (logger: log4net.ILog) format =
    Printf.ksprintf logger.Warn format

When I now use it like this:

let warn = getWarn log4net.LogManager.GetLogger("bar")
warn "This is a warning"

This works, however when I do:

warn "The value of my string is %s" someString

I get a compiler error saying "The value is not a function and cannot be applied" If I remove the first warn statement, it works.

So I guess that the compiler infers the type based on the first statement where I use warn, and therefore I need to use it the same way always.

But why is this not the case in my first example where I use the warn function directly and not through getWarn?

like image 480
Nils Magne Lunde Avatar asked Feb 06 '23 23:02

Nils Magne Lunde


1 Answers

This is value restriction.

When you declare warn as a plain function, it's just a generic function, no fuss. But when you declare it without parameters, it becomes a value (it's a value of function type, but still a value; there is a subtle difference), and so is subject to value restriction, which in a nutshell says that values cannot be generic.

Try removing all usages of warn (not just the first one). You'll get the compiler complain about this: Value restriction. The value 'warn' has been inferred to have generic type. Follow the link at the top for more discussion of this.

Of course this is a bit too strong, so F# has a little relaxation of this rule: if the value is used in the vicinity of its declaration, the compiler will fix generic arguments based on the usage. This is why it works with the first usage, but not with the second. You understood that part correctly.

One workaround would be to add explicit arguments, thus making warn a "declared function", not "value of function type". Try this:

let warn() = getWarn log4net.LogManager.GetLogger("bar")

Another workaround (which is really the same thing - see below) is to declare the generic argument explicitly, and also add a type annotation:

let warn<'a> : StringFormat<'a, unit> -> 'a = log4net.LogManager.GetLogger("bar")

The type annotation is necessary in this case, because otherwise the compiler doesn't know how the generic argument relates to the value type.

This way, you can use it just as if it was a true generic value:

warn "This is a warning"
warn "The value of my string is %s" someString

But there is a catch: this trick is really the same as adding a unit parameter (the previous workaround, above). Behind the scenes, the compiler will emit this definition as a true declared generic function and give it a single unit argument. The implication of this (as well as the previous workaround) is that every time you use warn, it gets invoked - i.e. every time you call warn, you call getWarn and GetLogger, too. In this particular example, this is probably ok, but if you're doing some significant work before producing the return function, that work will get re-done on every call.

And this points us to a deeper problem with your approach: you're trying to pass around functions without losing their genericity. Can't do that. Can't have a function value, return it from a function or pass it to a function, and still have it remain generic. Try this:

let mapTuple f (a,b) = (f a, f b)
let makeList x = [x]

mapTuple makeList (1, "abc")

Instinctively one would expect this to work and produce a tuple of lists - ( [1], ["abc"] ), right? Well, it doesn't work like this. When you declare mapTuple f (a,b), that f parameter has to be of some particular type. It can't be of an open generic type, so that you could apply it to a and b even if they're of different types. Instead, the inference works the other way: since f is of one type, thinks the compiler, and it gets applied to both a and b, then a and b must be of the same type. And so the signature gets inferred to mapTuple: ('a -> 'b) -> 'a*'a -> 'b*'b.

So the bottom line is, if you just want to produce a warn function, just give it a parameter and you'll be fine. However, if you want to produce two at once (as you mentioned in the comments), you're out of luck: they would have to end up being of the same type and you can't call them as generic functions after they're produced.

If you want to have a function that returns a function (or several) without losing its genericity, you'll have to return an interface. Member functions on interfaces can be generic independently of the genericity of the interface itself:

type loggers =
    abstract member warn: Printf.StringFormat<'a, unit> -> 'a
    abstract member err: Printf.StringFormat<'a, unit> -> 'a

let getLoggers (log: log4net.ILog) = 
  { new loggers with
    member this.warn format = Printf.ksprintf log.Warn format
    member this.err format = Printf.ksprintf log.Error format }

let logs = getLoggers (log4net.LogManager.GetLogger("Log"))

logs.warn "This is a warning"
logs.warn "Something is wrong: %s" someString
like image 152
Fyodor Soikin Avatar answered Mar 10 '23 18:03

Fyodor Soikin