Scott Nimrod
Scott Nimrod

Reputation: 11595

How do I invoke a function value that serves as a parameter on a function?

How do I invoke a function value that serves as a parameter on a function?

Specifically, my goal is to leverage a parameter of a function in which the parameter is actually a function.

In my case, I am trying to implement an interface for logging data.

Here's my code:

let logToFile (filePath:string) (message:string) =
    let file = new System.IO.StreamWriter(filePath)
    file.WriteLine(message)
    file.Close()

let makeInitialDeposit deposit =
    let balance = deposit |> insert []
    sprintf "Deposited: %f" balance

let logDeposit deposit (log:'medium ->'data -> unit) =
    deposit |> makeInitialDeposit 
            |> log

Note the following function:

let logDeposit deposit (log:'medium ->'data -> unit) =
    deposit |> makeInitialDeposit 
            |> log

I get a compile error on the log function:

This construct causes code to be less generic than indicated by the type annotations. The type variable 'medium has been constrained to be type 'string'.

I understand that makeInitialDeposit returns string. However, that string type is mapped to the generic type 'data. Hence, a generic can be of any type right?

I then tried supplying the medium (i.e. file) argument:

let logDeposit deposit (log:'medium ->'data -> unit) medium =
    deposit |> makeInitialDeposit 
            |> log medium

Then my error got updated to:

This construct causes code to be less generic than indicated by the type annotations. The type variable 'data has been constrained to be type 'string'.

My Goal

Ultimately, I just want to have an interface called log and pass in an implementation of that interface (i.e. logToFile).

Any guidance on how I should interpret the compile error based on my initial interpretation?

Insert function dependencies

let getBalance coins =
    coins |> List.fold (fun acc d -> match d with
                                     | Nickel         -> acc + 0.05
                                     | Dime           -> acc + 0.10
                                     | Quarter        -> acc + 0.25
                                     | OneDollarBill  -> acc + 1.00
                                     | FiveDollarBill -> acc + 5.00) 0.00

let insert balance coin =
    coin::balance |> getBalance

Upvotes: 0

Views: 89

Answers (2)

TheInnerLight
TheInnerLight

Reputation: 12184

The issue is that you have defined log :'medium ->'data -> unit.

You then take the result of deposit |> makeInitialDeposit, which has type string and pipe it into the log function. The compiler, logically, then infers that 'medium = string.

If you accept a 'medium argument in your logDeposit function then you simply move that inference along a step, deposit |> makeInitialDeposit is still a string so now 'data = string.


I think you are struggling though because these functions don't well model your domain and your logging logic is bleeding out into the rest of your code.

Why does makeInitialDeposit return a string?

Why does getBalance return a float but insert accepts a Coin list as its balance argument?

I would start by making a logging function that accepts three arguments:

let logToFile (filePath:string) (formatf : 'data -> string) data =
    use file = new System.IO.StreamWriter(filePath)
    file.WriteLine(formatf data)
    data

It has type filePath : string -> (formatf : 'data -> string) -> (data : 'data) -> data. It accepts a path to log to, a function that formats something of type 'data as a string and some 'data to log to the file. Finally, it returns the data argument you supplied unchanged. That means you can, in principle, insert logging of any arbitrary value in your code anywhere.

I then set up some functions in the domain like this:

let valueOf = function
    | Nickel         -> 0.05m
    | Dime           -> 0.10m
    | Quarter        -> 0.25m
    | OneDollarBill  -> 1.00m
    | FiveDollarBill -> 5.00m

let totalValue coins =
    coins |> List.fold (fun acc coin -> acc + valueOf coin) 0.0m

let insert coins coin = coin::coins // returns Coin list

let makeInitialDeposit deposit = insert [] deposit // returns Coin list

I can then use these functions, inserting logging at any arbitrary point:

let balance =
    makeInitialDeposit OneDollarBill
    |> logToFile "file.txt" (sprintf "Initial Deposit: %A")
    |> totalValue
    |> logToFile "file2.txt" (sprintf "Balance : $%M")

This approach lets you fit logging around your domain rather than building your domain around logging.

Upvotes: 2

Ringil
Ringil

Reputation: 6537

First off, it seems odd that you're passing in a string like "Deposited: 0.25" as your path name. What you probably wanted is to have message as your first parameter and filePath as your second parameter for logToFile. And accordingly for log to be 'data -> 'medium -> unit.

Getting that out of the way, the issue for your compiler error is that makeInitialDeposit returns a string and when you then pipe that result into log the constraint happens. One way to get it to work is to add another parameter to logDeposit to converts string to 'data like this:

let logDeposit deposit (log: 'data ->'medium-> unit) (stringConverter: string -> 'data) =
    deposit |> makeInitialDeposit
            |> stringConverter
            |> log

With this something like let dummyLog (a:int) (b:string) = () will work assuming you pass in the appropriate converter from string to int.

Upvotes: 1

Related Questions