Reputation: 11595
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
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
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