是否可以在F#中引用推断类型或提取函数参数类型?

时间:2019-01-22 22:42:07

标签: types f# type-inference

对于上下文,我正在使用https://fsharpforfunandprofit.com/posts/dependency-injection-1/中概述的部分依赖注入模式。

如果我想将一个函数传递给另一个函数(例如,在DI的合成根目录中),能够为FSharp类型引擎提供一些有关我要执行的操作的提示会很有用,否则爆炸成无用的混乱,直到一切正常。 为此,我想引用“现有功能的部分应用版本的类型”

例如,说我有设置

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

我想向_emailUserId提供类型提示

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
  = 
  ...

因为否则在编写_emailUserId时,我的IDE几乎没有帮助。

问题

(明确添加是因为将其隐藏在代码块中有点误导)。

F#的类型系统是否允许任何方式引用或建立在现有的推断类型之上?

例如type t = typeof<some inferred function without a manually written type signature>

F#的类型系统是否可以让我表达部分应用的函数而无需手动编写其参数类型?

例如type partial1<'a when 'a is 'x -> 'y -> 'z> = 'y -> 'z>,例如partial1<typeof<some ineferred function without a manually written signature>

尽管我仍然很感谢我所使用的模式的反馈,但这不是核心问题,而只是上下文。我认为该问题通常适用于F#开发。

我发现得到我想要的唯一方法是对全部功能类型进行硬编码:

let _emailUserId
  (sendEmail: string -> string -> Result)
  (getUser: string -> User)
  emailContents userId
  = 
  ...

这将导致重复的签名,在很大程度上使F#强大的推理系统的优势无效,并且在将这种模式扩展到玩具StackOverflow示例之外时,很难维护。

令我感到惊讶的是,这是显而易见的或不受支持的-我对丰富类型系统的另一种体验是Typescript,在很多情况下使用内置语法可以很容易地完成这种事情。

2 个答案:

答案 0 :(得分:3)

您可以定义类型别名,这些别名基本上是现有类型的新名称:

type SendMail = string -> string -> Result
type GetUser = string -> User

您可以在希望/需要为编译器提供类型提示的任何地方使用它们,但它们不是是新类型或子类型,并且编译器本身仍会推断原始类型。您可以为任何类型定义它们;上面的代码用于函数签名,因为这是您的示例,但是它们也可以用于记录类型或.NET类。

对于函数参数,您可以像这样使用它们:

let _emailUserId (sendMail : SendMail) (getUser : GetUser) emailContents userId =
/* .... */

答案 1 :(得分:3)

在F#中,创建简单类型非常容易。随后,代替字符串,整数等原始类型,通常最好建立一个由记录/区分联合组成的“树”,并且仅将原始类型保持在最底层。在您的示例中,我认为我只会将函数的单个记录传递给“ composition root”,例如我会按照以下方式做些事情:

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.

EmailProcessor是您的_emailUserId。请注意,我正在将|>的上一个计算结果传递到sendEmailToUserIdImpl中的下一个计算结果。这说明了签名的选择。

您不必制作EmailProcessor类,实际上,如果它只有一个成员,那么最好将其保留为一个函数,例如

let sendEmailToUserIdImpl (info : EmailProcessorInfo) userId reason =
    info.getUser userId
    |> info.getEmail reason
    |> info.sendMail

但是,如果最终基于同一info拥有多个成员,那么使用该类会有一些好处。

最后一点。您在问题中使用的参考文献很棒,当我遇到某些问题时,我经常会引用它。它涵盖了很多边缘情况,并显示了不同的变体来完成类似的事情。但是,如果您刚刚开始学习F#,那么一些简单的案例可能很难理解甚至不需要,例如您所考虑的概念。一个简单的选项类型可能足以区分您可以/不能创建某些对象的情况,因此可以避免使用完全爆炸的计算表达式ResultBuilder。由于没有空值(如果设计正确),异常数量少得多,扩展模式匹配等等,因此在F#中实际上不需要大量的日志记录(在C#中基本上是必须的)。