同一个列表中非常相似的类型(继承 vs 区分联合 vs 泛型?)
Posted
技术标签:
【中文标题】同一个列表中非常相似的类型(继承 vs 区分联合 vs 泛型?)【英文标题】:Very similar types in the same list (Inheritance vs Discriminated Unions vs Generics?) 【发布时间】:2021-12-09 23:24:57 【问题描述】:我正在创建一个带有函数的模块,每个函数都采用特定于函数的状态并产生一个浮点数和一个相同类型的状态:
函数f
使用FState
类型的状态
函数g
使用GState
类型的状态
函数h
使用HState
类型的状态
函数steps
用于评估函数列表,以相同顺序的状态列表。
Lib.fs
module Lib
type FState = y:int; z:string
let f (x:float) (s:FState) : float * FState = (x+1.0, s with y=s.y+1)
type GState = y:string
let g (x:float) (s:GState) : float * GState = (x*2.0, s)
type HState = z:float
let h (s:HState) : float * HState = (0.0, s)
let steps funs (states: 'a list) = List.mapi (fun i f -> f states.[i])
我希望这个模块的用户能够:
“将这些函数和状态放在一个列表中”,并对它们进行评估 定义自己的状态和函数,并“将它们放在同一个列表中”(例如函数e
和状态EState
,如下)
Program.fs
open Lib
type EState = y:float
let e (s:EState) : float * EState = (1.0, s with y=s.y/2.0)
[<EntryPoint>]
let main argv =
let funs : (? -> float * ?) list =
[ f 0.0
; f 1.0
; g 5.0
; h
; e
]
let states : ? list =
[ FState.y=1; z=""
; FState.y=1; z=""
; GState.y=""
; HState.z=2.0
; EState.y=4.0
]
let results : (float * ?) list = steps funs states
0 // return an integer exit code
我天真地打了一个问号?
,我不知道常见的类型应该是什么。
想法
我在想,我也许可以使用继承或有区别的联合创建通用类型。
但是使用有区别的联合,用户可能无法扩展通用类型 另一方面,我希望以前在函数式编程中已经做到了这一点,而不需要继承不管怎样,我什至不知道这是否可行。
问题
我宁愿对所有内容进行静态类型检查。
您将如何解决这个问题?额外背景
这些函数实际上是在计算同一图表上一条线的下一点的值(这就是我试图将它们放在同一个列表中的原因)。根据绘制的线,每个函数都需要跟踪不同的状态信息。在实际的当前代码中,一些函数产生超过 1 行的点以避免重复计算(尽管这可能是我的设计缺陷)
使用泛型尝试steps
(无法编译)
let step (f: 'a -> float * 'a) (s:'b) = f (s :?> 'a)
let steps funs (states: 'a list) = List.mapi (fun i f -> step f states.[i])
【问题讨论】:
问题 1: 你会如何用你知道的任何其他语言做到这一点?如果该语言恰好是动态类型的,那么问题 1a:您将如何在任何静态类型的语言中做到这一点? 问题 2: 当funs
与 states
不匹配时,您希望发生什么?
哦,还有问题 3: 我假设 printfn "%A"
只是为了演示,对吧?但是在一个真正的程序中,如果这些结果都是不同的类型,你会如何使用它们呢?
我不认为 DU 是一个解决方案,因为正如您所说,用户可能无法扩展它。但泛型可能是另一种选择。
函数并行做两件事,这似乎没有任何关系。我会将每个功能分成两个功能,然后从那里开始工作。函数和类型之间是否存在一对一或多对一或多对多的关系?我们这里有 XY 问题吗?
@BentTranberg 我希望这不是 XY 问题。我添加了额外的上下文
【参考方案1】:
当你发现自己在问“但是我如何让它们变成不同的类型?”时,答案通常(尽管不总是)最终会是“你实际上并不需要。”为了揭示问题的底层结构,您必须提出两个关于输入和输出的问题:
输入最终来自哪里?他们有什么共同点?由于您的目标是以相同的方式处理它们,因此必须有 somehting 共同点。它是什么?您可以将其编码为一种类型吗?在您的情况下,您的输入的共同点是它们是您可以“评估”以得出一个数字的数学函数。所以这应该是他们的类型。
产出最终将如何消费?他们有什么共同点?再说一遍:因为你的目标是以同样的方式消费它们,所以必须有 something 共同点。您可以将其编码为类型吗?在您的情况下,似乎有两种输出:(1) 表示当前值的数字,以及 (2) 用于计算的参数下一个值。前者没问题:到处都是相同的类型。但对于后者,问题适用:它最终是如何使用的?您声明它被直接反馈到同一个函数中,然后再次返回该值。所以这应该是它的类型:返回下一个值的函数。
如果您错过了上述详细信息中的一般方法,让我重申一下:我将如何使用这些值?我将在某个时候将它们转换为相同的类型。所以不要在某个时候做,马上做。那么所有的结果都是相同的类型。 或者,如果立即转换它们的成本太高,则返回一个“惰性值”——即执行转换的函数。
因此,将上述内容应用于您的案例,以下是重构解决方案的方法:
-
每个函数都返回一对
float
+ 相同的函数再次返回下一个float
+ 相同的函数再次返回下一个float
,以此类推。
当然,这需要一个递归类型,如果像(float, unit -> (float, unit -> (float, unit -> ...)))
这样天真地表达,它将是一个无限类型。但幸运的是,如果我们将递归类型包装在数据构造函数中,我们仍然可以创建它。
为了方便创建此类函数,请引入一个封装递归的包装函数。
“主”程序只会在每次迭代时不断调用函数以获取下一个值。
// ---- Lib.fs
type RecFn = RecFn of float * (unit -> RecFn)
let rec mkRecFn f state0 =
let (curValue, nextState) = f state0
RecFn (curValue, fun () -> mkRecFn f nextState )
type FState = y:int; z:string
let f x = mkRecFn <| fun (s: FState) -> (x + 1.0, s with y = s.y+1)
type GState = y:string
let g x = mkRecFn <| fun (s:GState) -> (x*2.0, s)
type HState = z:float
let h = mkRecFn <| fun (s:HState) -> (0.0, s)
let steps = List.map (fun (RecFn (x, next)) -> next ())
// ---- Program.fs
type EState = y:float
let e = mkRecFn <| fun (s:EState) -> (1.0, s with y=s.y/2.0)
[<EntryPoint>]
let main argv =
let funs : RecFn list =
[ f 0.0 FState.y=1; z=""
; f 1.0 FState.y=1; z=""
; g 5.0 GState.y=""
; h HState.z=2.0
; e EState.y=4.0
]
let results : RecFn list = steps funs
0 // return an integer exit code
注意:现在有一个列表 funs
,而不是两个列表 funs
和 states
。我推断这没关系,因为您在评论中声明等效的 C# 实现将具有单个基础对象列表。
【讨论】:
这是一个很好的解决方案(真的)。但我很抱歉没有提到,这些函数的状态并没有完全陷入闭环(尽管从概念上来说可能是这样)。该程序需要能够停止运行并在另一个时间恢复。国家也习惯于这样做。我假设RecFn
类型不能“保存”(序列化?),因为它包含函数 unit -> RecFn
这正是我问你如何用另一种语言实现它的原因。您提出的 C# 解决方案也不满足此条件。
为什么您认为 C# 解决方案也不能满足这一点?类可以序列化
你说的是内置的“免费”序列化吗?所以,这是有史以来最糟糕的主意。对代码进行一些更改后,您将无法反序列化。只是不要。
我说的是使用 JSON.NET以上是关于同一个列表中非常相似的类型(继承 vs 区分联合 vs 泛型?)的主要内容,如果未能解决你的问题,请参考以下文章