In the script below I have created a module LoggedCalculations which ‘hides’ the functions in the module Calculations and provides new functions with the same names.
module Logging =
let logEntry remark value =
printfn "%A: %s %A"
module Calculations =
let multiplyBy20 x = x * 20
let squareIt x = x * x
module LoggedCalculations =
let multiplyBy20 = logEntry "Multiplying :" >> multiplyBy20 >> logEntry "Multiplied value ="
let squareIt = logEntry "Squaring :" >> squareIt >> logEntry "Squared value ="
open LoggedCalculations // Can be swapped with Calculations to disable logging.
The output looks like this:
28/04/2022 12:56:59: Squaring : 50
28/04/2022 12:56:59: Squared value = 2500
Is this ‘module wrapping/hiding’ an acceptable thing to be doing?
Are there any potential pitfalls that I should be aware of (apart from forgetting to swap the opened module back to Calculations when I don’t want logging)?
Is there a better way to do it?
Note: There are probably much better ways of logging stuff but it’s the ‘hiding’ of functions that I’m curious about here.
This is a pretty opinionated topic, but I’m happy to share my opinions .
I’ve made use of this feature before, for example in an extreme case by pretty much globally shadowing the “partial” functions in the container modules (like
Seq.head here) to attach a compiler warning to the function (I used this trick in a codebase where we try to stick to just total functions). I haven’t run into any pitfalls beyond what I’m sure you’re already aware of - it can cause surprise if folks are expecting the original version. The scope can be important. I’ll shadow a function as a local binding inside another small function without a second thought. I’ll think twice before I shadow a function in a good size module. And I’ll only globally shadow something after a lot of thought and a really good reason. If scoped to a module of decent size, I would probably want a better reason to shadow something besides just logging (unless I was debugging something, and intended to revert it), preferring to just give it a new name instead. But there are cases where I will think it worthwhile to shadow something inside a module.
Not everyone agrees with my sentiments though. For example, I do a lot of programming in PureScript, which also allows for shadowing names like that, but any shadowing results in a compiler warning by default. So there are certainly folks out there that think that shadowing is a bad practice in general because of the potential confusion.
I particularly like to shadow when some computation invalidates the original value, e.g.,
let fn xs =
// shadow `xs` here
let xs = xs |> Array.filter isValid
// anywhere below this point, it would be a bug to use the original `xs`
// because it potentially contains invalid values
Thanks for the advice.
I don’t understand all of it yet but, from what I do understand, it looks like I could be setting myself up for a nasty fall if I continue doing things this way as a matter of course.
Giving functions unique names sounds like the better way until I really need to shadow things under specific conditions.
I had a feeling that I might have been going about this the wrong way as it didn’t feel quite right, so it’s good to get some confirmation that I was right to question myself.
So much to learn, but always interesting to do so.