从接口设计具有隐藏底层实现的 F# 类型
Designing F# types with hidden underlying implementations from interface
我是 F# 的新手,在尝试设计某些类型时,我注意到 OOP 在很大程度上影响了我的设计决策。我费了好大劲才找到这个问题,结果一无所获。
我将描述我在 C# 中尝试做的事情,因为我更熟悉这些术语。假设我有一个接口,它在类似容器 class 上指定了一些最少的必需方法。我们称它为 IContainer
。然后我有两个实现此接口的 classes,ContainerA
和 ContainerB
具有对用户隐藏的不同底层实现。这是一种非常常见的 OOP 模式。
我试图在 F# 中实现同样的事情,只使用不可变类型以保留在功能世界中,即我如何实现一种功能可互换的类型,但 public 功能用户将使用保持不变:
type 'a MyType = ...
let func1 mytype = ...
let func2 mytype -> int = ...
MyType 的定义未知,以后可以更改,例如如果找到更高效的函数版本(比如容器类型的更好实现),但不需要太多努力或需要重新设计整个模块。一种方法是在函数中使用模式匹配和可区分的联合,但这似乎不是很可扩展。
在函数式语言中使用比在 OO 语言中使用的类型简单得多的类型更为典型。
建模形状是 classic 示例。
这是一个典型的 OO 方法:
type IShape =
abstract member Area : double
type Circle(r : float) =
member this.Area = System.Math.PI * r ** 2.0
interface IShape with
member this.Area = this.Area
type Rectangle(w : float, h : float) =
member this.Area = w * h
interface IShape with
member this.Area = this.Area
请注意,使用这种方法添加新类型非常容易,我们可以相对轻松地引入 Triangle
或 Hexagon
class。我们只需创建类型并实现接口。
相比之下,如果我们想向 IShape
添加一个新的 Perimeter
成员,我们将不得不更改每个实现,这可能需要大量工作。
现在让我们看看如何用函数式语言对形状建模:
type Shape =
|Circle of float
|Rectangle of float * float
[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module Shape =
let area = function
|Circle r -> System.Math.PI * r ** 2.0
|Rectangle (w, h) -> w*h
现在,希望您能看到添加 perimeter
函数要容易得多,我们只需对每个 Shape
情况进行模式匹配,编译器就可以检查我们是否已经详尽地实现了它每个案例。
相比之下,现在添加新的 Shape
s 要困难得多,因为我们必须返回并更改作用于 Shape
s 的每个函数。
结果是,无论我们选择使用哪种建模形式,都有 trade-offs。这个问题叫做表达式问题。
您可以轻松地将第二种模式应用于您的 Container
问题:
type Container =
|ContainerA
|ContainerB
let containerFunction1 = function
|ContainerA -> ....
|ContainerB -> ....
这里有一个类型有两个或多个案例,每个案例的独特功能实现包含在模块函数中,而不是类型本身。
我是 F# 的新手,在尝试设计某些类型时,我注意到 OOP 在很大程度上影响了我的设计决策。我费了好大劲才找到这个问题,结果一无所获。
我将描述我在 C# 中尝试做的事情,因为我更熟悉这些术语。假设我有一个接口,它在类似容器 class 上指定了一些最少的必需方法。我们称它为 IContainer
。然后我有两个实现此接口的 classes,ContainerA
和 ContainerB
具有对用户隐藏的不同底层实现。这是一种非常常见的 OOP 模式。
我试图在 F# 中实现同样的事情,只使用不可变类型以保留在功能世界中,即我如何实现一种功能可互换的类型,但 public 功能用户将使用保持不变:
type 'a MyType = ...
let func1 mytype = ...
let func2 mytype -> int = ...
MyType 的定义未知,以后可以更改,例如如果找到更高效的函数版本(比如容器类型的更好实现),但不需要太多努力或需要重新设计整个模块。一种方法是在函数中使用模式匹配和可区分的联合,但这似乎不是很可扩展。
在函数式语言中使用比在 OO 语言中使用的类型简单得多的类型更为典型。
建模形状是 classic 示例。
这是一个典型的 OO 方法:
type IShape =
abstract member Area : double
type Circle(r : float) =
member this.Area = System.Math.PI * r ** 2.0
interface IShape with
member this.Area = this.Area
type Rectangle(w : float, h : float) =
member this.Area = w * h
interface IShape with
member this.Area = this.Area
请注意,使用这种方法添加新类型非常容易,我们可以相对轻松地引入 Triangle
或 Hexagon
class。我们只需创建类型并实现接口。
相比之下,如果我们想向 IShape
添加一个新的 Perimeter
成员,我们将不得不更改每个实现,这可能需要大量工作。
现在让我们看看如何用函数式语言对形状建模:
type Shape =
|Circle of float
|Rectangle of float * float
[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module Shape =
let area = function
|Circle r -> System.Math.PI * r ** 2.0
|Rectangle (w, h) -> w*h
现在,希望您能看到添加 perimeter
函数要容易得多,我们只需对每个 Shape
情况进行模式匹配,编译器就可以检查我们是否已经详尽地实现了它每个案例。
相比之下,现在添加新的 Shape
s 要困难得多,因为我们必须返回并更改作用于 Shape
s 的每个函数。
结果是,无论我们选择使用哪种建模形式,都有 trade-offs。这个问题叫做表达式问题。
您可以轻松地将第二种模式应用于您的 Container
问题:
type Container =
|ContainerA
|ContainerB
let containerFunction1 = function
|ContainerA -> ....
|ContainerB -> ....
这里有一个类型有两个或多个案例,每个案例的独特功能实现包含在模块函数中,而不是类型本身。