types - 是否可以在 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,在许多情况下,使用内置语法很容易做到这一点。
解决方案
在 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# 中基本上是必须的)。
推荐阅读
- scheme - 方案中的 if 和 cond
- actionscript-3 - gearsandcogs 扩展是否仍在最新的 AIR 版本 32.0+ 中工作?
- python - 如何修复“RuntimeError:在应用程序上下文之外工作”。使用 Flask 创建蓝图时?
- c# - 实体框架取消删除
- amazon-web-services - AWS API Gateway:如何在 HTTP 代理直通集成中删除/替换查询字符串参数?
- java - JPA @Query 计算每个对象有多少关系
- sql - PostgreSQL 内部是否使用 pg_largeobject 表?
- python - 将数据着色器添加到 matplotlib 子图 - 位置参数错误
- c - 如何打印小数点后具有不同数字的数组?
- python - 在python中使用socketcan J1939过滤器