【问题标题】:Is it possible to refer to inferred types or extract function argument types in F#?是否可以在 F# 中引用推断类型或提取函数参数类型?
【发布时间】:2019-01-22 22:42:06
【问题描述】:

对于上下文,我正在使用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&lt;some inferred function without a manually written type signature&gt;

F# 的类型系统可以让我表达一个部分应用的函数,而无需手动编写它的参数类型吗?

例如type partial1&lt;'a when 'a is 'x -&gt; 'y -&gt; 'z&gt; = 'y -&gt; 'z&gt;,也许像partial1&lt;typeof&lt;some ineferred function without a manually written signature&gt;一样使用?

虽然我仍然很感谢对我使用的模式的反馈,但这不是核心问题,只是上下文。在我看来,这个问题非常普遍地适用于 F# 开发。

我发现得到我想要的东西的唯一方法是硬编码完整的函数类型:

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

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

令我惊讶的是,这是不明显或不受支持的——我使用丰富类型系统的另一个经验是 Typescript,在许多情况下,使用内置语法很容易做到这一点。

【问题讨论】:

  • 您不需要对所有类型进行硬编码。您也可以将单个类型“打开”,例如(sendEmail : string -&gt; 'TContent -&gt; Result)TContent 现在是泛型类型参数,您可以再次将其用于函数内部的类型注释以指定该类型。您可以根据需要将所涉及的类型设为泛型;如果你违反了任何类型系统规则,编译器会告诉你。但是,为了能够做到这一点,您必须至少写出相关函数签名的结构,以便您可以放置​​和命名通用参数。
  • 如果你想保留类型通用/推断但不想命名它们,你也可以使用通配符_:(sendEmail: _ -&gt; 'TContent -&gt; _)

标签: types f# type-inference


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 感谢您的回复,但这是我特别想避免做的事情,因为这意味着我不能使用类型推断来定义 getUser 函数。不过,这基本上是我正在做的一种解决方法,所以我想我们同意它仍然是处理事情的好方法!我已将问题编辑得更加明确。
【解决方案2】:

在 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。请注意,我将|&gt; 上一个计算的结果传递给sendEmailToUserIdImpl 中的下一个。这就解释了签名的选择。

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

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

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

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

【讨论】:

  • 感谢您的回复,但我认为我的问题措辞有点不清楚。虽然我很感激关于设计模式的指针,但这不是我的核心问题(你回答的是“这个人真正需要什么?”而不是:p)。我已将问题编辑得更加明确。
  • @Jarrad 那么答案就在您的参考资料中。 Kleisli 组合运算符或类似的东西可能是要走的路。但是,它会导致代码过于笼统。是的,有时这正是我们所需要的。但是,在许多情况下不需要它!多亏了一些聪明的建议,我最近放弃了大多数 SRTP(可以做你描述的事情),转而支持上述结构。如果您正在编写一个非常通用的框架,那么可能需要进行极端的概括。但是,如果你有一个特定的领域要建模,那么简单的通用事情就可以了。
猜你喜欢
  • 2016-01-14
  • 2020-03-09
  • 2013-11-10
  • 2017-12-10
  • 2020-07-10
  • 2019-09-26
  • 1970-01-01
  • 1970-01-01
  • 2016-11-01
相关资源
最近更新 更多