3

I have read about continuations and partial applications; I am also aware of the kprintf function.

But I still don't know how to write something like:

let myPrintFunction format variable_length_arguments_list =
    let a = sprintf format variable_length_ argument_list
    do other things

what would be the syntax this this?

so I could use it like:

myPrintFunction "%s : %i" "hello" 3

Edit:

This is different than How do I implement a method with a variable number of arguments? because that question is asking how to make a method with a variable number of arguments, but the issue I am facing is to pass that variable number of argument to the next function (sprintf) that takes a variable number of arguments too. Or, at least that's where I suppose the problem is.

The test code, based on the solution proposed by Scott can be found here: https://dotnetfiddle.net/oCzcS9

Thomas
  • 10,933
  • 14
  • 65
  • 136
  • 2
    Does this answer your question? [How do I implement a method with a variable number of arguments?](https://stackoverflow.com/questions/42696008/how-do-i-implement-a-method-with-a-variable-number-of-arguments) – Caramiriel Nov 12 '19 at 14:47
  • No, the F# format system does not exist in C#. – Bent Tranberg Nov 12 '19 at 22:23

3 Answers3

3

I want to demonstrate the ksprintf function, because that one accepts a continuation that will allow you to pass on the resulting string to e.g. a log system.

For the purpose of demonstration, let's first create something that can take a single string as input and pass it on, in this case to the console.

let writeStringToConsole (s: string) = Console.WriteLine ("OUTPUT : " + s)

So now, if writeStringToConsole is all we have, how to we make it accept F# formatting?

let printToConsole format = Printf.ksprintf writeStringToConsole format

Example that demonstrates that it works.

type DU = A | B
let i = 7
let s = "thirteen"
let du = B

printToConsole """an int %i and a string "%s" here""" i s
printToConsole """an int %i and a string "%s" and DU %A here""" i s du

// OUTPUT : an int 7 and a string "thirteen" here
// OUTPUT : an int 7 and a string "thirteen" and DU B here

// Note that OUTPUT is also part of the actual output,
// and it demonstrates how you can add e.g. a timestamp
// or line number or something to the output string, without
// it being part of the formatting.

edit: Some additional notes

The format string must be a literal. That's because the literal string must be read at compile time in order to compute the function that must be returned in order to gobble up whatever values/types that follow the format string.

For example, if you do printToConsole "%i %s %A %A" 7 "x" myType yourType, then you'll see int -> string -> MyType -> YourType in the signature of printToConsole where it's used.

There is a way to use plain strings as format strings with this system, but I don't remember how it's done, and anyway it spoils the type safety. It comes in handy when doing internationalization of strings, and your format strings must come from a resource and not F# source due to external translator services.

edit 2 : Wrap e.g. log system

I created an interface to use for various logging systems, which pretty much share the same features.

type ILogger =

...

    abstract member Debugf: StringFormat<'h, unit> -> 'h
    abstract member Verbosef: StringFormat<'h, unit> -> 'h
    abstract member Infof: StringFormat<'h, unit> -> 'h
    abstract member Warningf: StringFormat<'h, unit> -> 'h
    abstract member Errorf: StringFormat<'h, unit> -> 'h
    abstract member Fatalf: StringFormat<'h, unit> -> 'h

Then an implementation for my currently used logging system looks like this.

type internal SiLogger(session: Session) =
    let slogf = Printf.ksprintf

...

    interface ILogger with

...

    member _.Debugf format = slogf session.LogDebug format
    member _.Verbosef format = slogf session.LogVerbose format
    member _.Infof format = slogf session.LogMessage format
    member _.Warningf format = slogf session.LogWarning format
    member _.Errorf format = slogf session.LogError format
    member _.Fatalf format = slogf session.LogFatal format

And there is a null logger.

let slogf = Printf.ksprintf

let dummyLog _ = () // The parameter is the title string.

let dummy format = slogf dummyLog format

let getNullLogger () =
    { new ILogger with

...

        member _.Debugf format = dummy format
        member _.Verbosef format = dummy format
        member _.Infof format = dummy format
        member _.Warningf format = dummy format
        member _.Errorf format = dummy format
        member _.Fatalf format = dummy format

...

        }
Bent Tranberg
  • 3,445
  • 26
  • 35
  • Is there a way to know how many arguments 'format' has? I'm wondering if it's possible to detect printToConsole "hello" as a single argument and print the string as it, alleviating the need for printToConsole "%s" "hello" – Thomas Nov 13 '19 at 00:32
  • As a side note if your doing this to do logging in a production service there's more to it then that and other requirements will come up. I would consider the use of an F# logging library for this. I had to write LoggerWrapper.FSharp to make logging functions that do the things your probably looking for as you go down the rabbit hole. e.g if logging is disabled it shouldn't attempt to build the string/allocate memory for performance reasons - which is usually true for debug logging. kprintf does this without wrapping the whole thing up and therefore it can get tricky to implement. – akara Nov 13 '19 at 01:27
  • I suspect the null logger that I've now added to the answer will do work that isn't needed. That could be an advantage if you want the load during debugging, but you could also I guess add something like `if enabled then dummy format`. Or just not use ksprintf at all in the null logger. – Bent Tranberg Nov 13 '19 at 06:06
  • Oh, and I also have members Debug, Verbose, etc, that just plain outputs a string without using formatting. For performance reasons. – Bent Tranberg Nov 13 '19 at 06:09
2
open System

let myPrintFunction (format: Printf.StringFormat<_>) ([<ParamArray>] args) =
    let a = sprintf format args
    a

myPrintFunction "%s : %i" "hello" 3
Scott Hutchinson
  • 1,703
  • 12
  • 22
  • Is there a way to know the number of arguments in 'args'? – Thomas Nov 12 '19 at 18:32
  • 1
    actually it doesn't work when called from a class: [FS0003] This value is not a function and cannot be applied. With the code being: let Print (format: Printf.StringFormat<_>) ([] args) = \n let message = sprintf format args \n ConsoleInfoEvent.Trigger message \n printfn "%s" message – Thomas Nov 12 '19 at 19:03
  • 1
    and the call being: Print "%s : %i" "hello" 3 – Thomas Nov 12 '19 at 19:04
  • So you want myPrintFunction to be a member function of a class? – Scott Hutchinson Nov 12 '19 at 19:05
  • yes; I don't understand why the code would be different though? – Thomas Nov 12 '19 at 19:07
  • That fiddle link did not seem to arrive at the correct code. – Scott Hutchinson Nov 12 '19 at 19:57
  • Make sure your main function returns an int like 0. This answer works as is, but not if you attempt to add a side effect like a `printfn` call. – Scott Hutchinson Nov 12 '19 at 20:38
  • what this function is supposed to do is take parameters, like a printfn, generate a string with sprintf, call an event and print out the result. I have seen similar code online with kprintf, but I didn't quite understand how kprintf works – Thomas Nov 12 '19 at 20:40
  • 1
    Maybe this will help you understand kprintf better: https://stackoverflow.com/a/5570101/5652483 – Scott Hutchinson Nov 12 '19 at 23:41
  • This compiles and works without the type annotations in F# 7.0. – silvalli Apr 08 '23 at 23:21
0

To add the PrintF as a member function, this is the closest I could get. As you see, I had to pass the format string separately (in the constructor, or I could have used a property setter). I could find no way to pass the format string as the first parameter of the PrintF function as I could for a free function (see my other answer at https://stackoverflow.com/a/58822618/5652483).

Also, if I uncomment the line this.RaiseSomeEvent msg, then it breaks. So I could find no way to enable the PrintF function to have a side effect.

Hopefully, someone else can solve these issues.

type Foo (format: Printf.StringFormat<_>) =
    member this.RaiseSomeEvent msg = printf "%s" msg

    member this.PrintF ([<ParamArray>] args) =
        let msg = sprintf format args
        //this.RaiseSomeEvent msg
        msg

let foo = Foo("%s : %i")
foo.PrintF "hello" 3
Scott Hutchinson
  • 1,703
  • 12
  • 22