首页 > 解决方案 > 是否可以在 F# 中引用推断类型或提取函数参数类型?

问题描述

对于上下文,我正在使用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,在许多情况下,使用内置语法很容易做到这一点。

标签: typesf#type-inference

解决方案


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

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

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

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


推荐阅读