Jarrad
Jarrad

Reputation: 1027

Is it possible to refer to inferred types or extract function argument types in F#?

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.

The Question

(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

Answers (2)

Konstantin Konstantinov
Konstantin Konstantinov

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

TeaDrivenDev
TeaDrivenDev

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

Related Questions