Reputation: 1027
For context, I am playing with the partial dependency injection pattern outlined in https://fsharpforfunandprofit.com/posts/dependency-injection-1/ .
If I want to pass a function in to another function (say in a composition root for DI), it would be useful to be able to give the FSharp type engine some hints about what I'm trying to do, otherwise it sort of explodes into an unhelpful mess until the point that everything is working. To do this, I'd like to refer to "types of partially applied versions of existing functions"
For example, say I have the setup
let _getUser logger db userId =
logger.log("getting user")
User.fromDB (db.get userId)
let _emailUserId sendEmail getUser emailContents userId =
let user = getUser userId
do sendEmail emailContents user.email
// composition root
let emailUserId =
let db = Db()
let logger = Logger()
let sendEmail = EmailService.sendEmail
let getUser = _getUser logger db
_emailUserId sendEmail getUser
I want to provide type hints to _emailUserId like
let _emailUserId
// fake syntax. is this possible?
(sendEmail: typeof<EmailService.sendEmail>)
// obviously fake syntax, but do I have any way of defining
// `type partially_applied = ???` ?
(getUser: partially_applied<2, typeof<_getUser>>)
emailContents userId
=
...
because otherwise I have very little help from my IDE when writing _emailUserId.
(Added explicitly because hiding it away in a code block was a little misleading).
Does F#'s type system allow any way to refer to or build upon existing inferred types?
Such as type t = typeof<some inferred function without a manually written type signature>
And can F#'s type system let me express a partially applied function without manually writing its argument types?
E..g type partial1<'a when 'a is 'x -> 'y -> 'z> = 'y -> 'z>
, maybe used like partial1<typeof<some ineferred function without a manually written signature>
?
While I'm still grateful for feedback on the pattern I am using, that is not the core question, just the context. The question is applicable quite generally to F# development, in my opinion.
The only way I have found to get what I want is to hardcode full function types:
let _emailUserId
(sendEmail: string -> string -> Result)
(getUser: string -> User)
emailContents userId
=
...
which results in duplicated signatures, largely nullifies the benefits of F#'s great inference system, and is quite difficult to maintain when expanding this pattern beyond a toy StackOverflow example.
It's surprising to me that this is non-obvious or unsupported - my other experience with a rich type system is Typescript, where this sort of thing is quite easy to do in many cases with builtin syntax.
Upvotes: 2
Views: 156
Reputation: 1403
In F# it is very easy to create simple types. Subsequently, instead of primitive types, like strings, integers, etc. it is often better to build a "tree" made out of records / discriminated unions and only keep the primitive types at the very bottom. In your example, I think that I would just pass a single record of functions to the "composition root" e.g. I would do something along these lines:
type User =
{
userId : int
userName : string
// add more here
}
type EmailAttachment =
{
name : string
// add more to describe email attachment, like type, binary content, etc...
}
type Email =
{
address : string
subject : string option
content : string
attachments : list<EmailAttachment>
}
type EmailReason =
| LicenseExpired
| OverTheLimit
// add whatever is needed
type EmailResult =
| Sucess
| Error of string
type EmailProcessorInfo =
{
getUser : string -> User
getEmail : EmailReason -> User -> Email
sendMail : Email -> EmailResult
}
type EmailProcessor (info : EmailProcessorInfo) =
let sendEmailToUserIdImpl userId reason =
info.getUser userId
|> info.getEmail reason
|> info.sendMail
member p.sendEmailToUserId = sendEmailToUserIdImpl
// add more members if needed.
The EmailProcessor
is your _emailUserId
. Note that I am piping |>
the result of the previous calculation to the next in sendEmailToUserIdImpl
. That explains the choice of signatures.
You don't have to make EmailProcessor
class and, in fact, if it has only a single member, then it is probably better to leave it s a function, e.g.
let sendEmailToUserIdImpl (info : EmailProcessorInfo) userId reason =
info.getUser userId
|> info.getEmail reason
|> info.sendMail
However, if if does end up having several members based on the same info
, then there are some benefits of using the class.
As a final note. The reference that you are using in you question is great and I often refer to it when stuck at something. It covers a lot of edge cases and shows different variants to do similar things. However, if you are just starting to learn F#, then some of the concepts there might be hard to get through or even not needed for simple cases, like the one you considered. A simple option type might be enough to distinguish the cases when you can / cannot create some object and so the usage of a fully blown computation expression ResultBuilder
can be avoided. An extensive logging (which is basically a must in C#) is not really needed in F# due the absence of nulls (if designed properly), much smaller number of exceptions, extensive pattern matching, and so on and so forth.
Upvotes: 3
Reputation: 6629
You can define type aliases, which are basically new names for existing types:
type SendMail = string -> string -> Result
type GetUser = string -> User
You can use these everywhere you want/need to give type hints to the compiler, but they will not be new types or subtypes, and the compiler itself will still infer the original type. And you can define them for any type; the ones above are for function signatures, because that's your example, but they can be for record types or .NET classes just as well.
For function parameters you'd use them like this:
let _emailUserId (sendMail : SendMail) (getUser : GetUser) emailContents userId =
/* .... */
Upvotes: 3