设计模块时如何决定是在类型级别参数化还是在模块级别参数化?
How to decide whether to parameterize on the type-level or the module-level when designing modules?
我正在努力深入理解 ML 风格的模块:我认为
概念很重要,我喜欢他们鼓励的那种思维。我只是
现在发现参数类型和
参数化模块。我正在寻找工具来思考将要发生的事情
帮助我在构建程序时做出明智的设计决策。
拳头我会尽量描述我的问题。然后我会提供一个
我正在从事的学习项目中的具体示例。最后,我会
重新审视一般性问题,以便得出结论。
(很抱歉,我还不够了解,无法更简洁地提出这个问题。)
总的来说,我发现的压力是:函数是最
当我们为他们提供参数化
类型签名(在适当的情况下)。然而,模块是最灵活和开放的
当我们将函数的参数化密封在
模块,而是在给定类型上参数化整个模块。
可以在比较模块中找到这种差异的现成示例
与那些实现的人一起实现 LIST
签名
ORD_SET
。模块 List:LIST
提供了一堆有用的功能,
在任何类型上参数化。一旦我们定义或加载了 List
模块,我们就可以
随时应用它提供的任何功能来构造、操作或
检查任何类型的列表。例如,如果我们同时使用字符串和
整数,我们可以使用同一个模块来构造和操作
两种类型的值:
val strList = List.@ (["a","b"], ["c","d"])
val intList = List.@ ([1,2,3,4], [5,6,7,8])
另一方面,如果我们想处理有序集,事情就不同了:
有序集要求其所有元素都具有排序关系,
并且不能有单一的具体功能 compare : 'a * 'a -> order
为每种类型产生这种关系。因此,我们需要一个不同的
满足我们希望放入的每种类型的 ORD_SET
签名的模块
有序集。因此,为了构造或操作有序的字符串集
和整数,我们必须为每种类型实现不同的模块[1]:
structure IntOrdSet = BinarySetFn ( type ord_key = int
val compare = Int.compare )
structure StrOrdSet = BinarySetFn ( type ord_key = string
val compare = String.compare )
然后我们必须使用适当模块中的拟合函数
希望对给定类型进行操作:
val strSet = StrOrdSet.fromList ["a","b","c"]
val intSet = IntOrdSet.fromList [1,2,3,4,5,6]
这里有一个非常简单的权衡:LIST
模块提供功能
范围在你喜欢的任何类型,但他们不能利用任何关系
介于任何特定类型的值之间; ORD_SET
模块提供
必须受限于仿函数中提供的类型的函数
参数,但通过相同的参数化,他们能够
包含有关内部结构和关系的特定信息
他们的目标类型。
很容易想象我们想要设计替代系列的情况
列表模块,使用仿函数将类型和其他值参数化为
提供具有更复杂结构的类列表数据类型:例如,指定
有序列表的数据类型,或使用自平衡二进制表示列表
搜索树。
在创建模块时,我认为也很容易识别它何时会
能够提供多态函数以及何时需要参数化
在某些类型上。对我来说,似乎更困难的是弄清楚哪一种
在进一步下游工作时你应该依赖的模块。
一般来说,我的问题是:当我设计一个系统时
各种相关的模块,我怎样才能弄清楚是否围绕模块进行设计
提供使用仿函数生成的多态函数或模块
在类型和值上参数化?
我希望用下面的例子来说明这个困境及其重要性,
取自我正在从事的玩具项目。
我有一个functor PostFix (ST:STACK) : CALCULATOR_SYNTAX
。这需要一个
堆栈数据结构的实现并生成一个解析器读取
具体后缀 ("reverse polish") 表示法转换为抽象语法(待
由下游的计算器模块评估),反之亦然。现在,我已经
使用提供多态堆栈类型的标准堆栈接口和
对其进行操作的函数数量:
signature STACK =
sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
end
这很好用,并且给了我一些灵活性,因为我可以使用基于列表的堆栈
或基于矢量的堆栈,或其他。但是,假设我想添加一个简单的日志记录
堆栈模块的功能,以便每次将元素推入或
从堆栈弹出,它打印出堆栈的当前状态。现在我会
需要 fun toString : 'a -> string
作为堆栈收集的类型,并且
据我了解,这不能合并到 STACK
模块中。现在我
需要将类型密封到模块中,并通过类型参数化模块
收集在堆栈中和一个 toString
函数,可以让我生成一个
收集类型的可打印表示。所以我需要像
这样的东西
functor StackFn (type t
val toString: t -> string ) =
struct
...
end
这将不会产生一个匹配STACK
签名的模块,因为它
不提供多态类型。因此,我必须更改所需的签名
对于 PostFix
函子。如果我有很多其他模块,我必须改变
所有这些也是如此。这可能会带来不便,但真正的问题是我
不能再使用我简单的基于列表或基于向量的 STACK
模块
PostFix
当我 不想 想要记录时的仿函数。现在看来,我得回去了
并重写这些模块,使其也具有密封类型。
因此 return 扩展并(幸运地)完成我的问题:
- 是否有一些方法可以指定由
StackFn
这样他们最终会成为 "special cases" of STACK
?
- 或者,有没有办法为
PostFix
模块编写签名
这将允许 StackFn
生成的模块和那些
满足STACK
?
- 一般来说,有没有一种方法来思考两者之间的关系?
将来可以帮助我 catch/anticipate 这类事情的模块?
(如果你已经读到这里了。非常感谢!)
正如您所发现的,参数多态性与 SML 和 OCaml 中的 functors/module 之间存在紧张关系。这主要是由于模块的 "two language" 性质和缺乏临时多态性。 1ML and modular implicits 两者都针对该问题提供了不同的解决方案。第一个是统一两种参数化,第二个是允许在需要时激发一些特殊的多态性。
回到实际考虑。使用仿函数,单态化给定的数据结构相当容易(但 verbose/annoying)。这是一个示例(在 OCaml 中)。有了这个,您仍然可以编写通用实现并在以后专门化它们(通过提供打印功能)。
module type POLYSTACK = sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
val toString : ('a -> string) -> 'a stack -> string
end
module type STACK = sig
type elt
type t
exception EmptyStack
val empty : t
val isEmpty : t -> bool
val push : (elt * t) -> t
val pop : t -> t
val top : t -> elt
val popTop : t -> t * elt
val toString : t -> string
end
module type PRINTABLE = sig
type t
val toString : t -> string
end
module Make (E : PRINTABLE) (S : POLYSTACK)
: STACK with type elt = E.t and type t = E.t S.stack
= struct
type elt = E.t
type t = E.t S.stack
include S
let toString = S.toString E.toString
end
module AddLogging (S : STACK)
: STACK with type elt = S.elt and type t = S.t
= struct
include S
let push (x, s) =
let s' = S.push (x, s) in print_string (toString s') ; s'
end
我正在努力深入理解 ML 风格的模块:我认为 概念很重要,我喜欢他们鼓励的那种思维。我只是 现在发现参数类型和 参数化模块。我正在寻找工具来思考将要发生的事情 帮助我在构建程序时做出明智的设计决策。
拳头我会尽量描述我的问题。然后我会提供一个 我正在从事的学习项目中的具体示例。最后,我会 重新审视一般性问题,以便得出结论。
(很抱歉,我还不够了解,无法更简洁地提出这个问题。)
总的来说,我发现的压力是:函数是最 当我们为他们提供参数化 类型签名(在适当的情况下)。然而,模块是最灵活和开放的 当我们将函数的参数化密封在 模块,而是在给定类型上参数化整个模块。
可以在比较模块中找到这种差异的现成示例
与那些实现的人一起实现 LIST
签名
ORD_SET
。模块 List:LIST
提供了一堆有用的功能,
在任何类型上参数化。一旦我们定义或加载了 List
模块,我们就可以
随时应用它提供的任何功能来构造、操作或
检查任何类型的列表。例如,如果我们同时使用字符串和
整数,我们可以使用同一个模块来构造和操作
两种类型的值:
val strList = List.@ (["a","b"], ["c","d"])
val intList = List.@ ([1,2,3,4], [5,6,7,8])
另一方面,如果我们想处理有序集,事情就不同了:
有序集要求其所有元素都具有排序关系,
并且不能有单一的具体功能 compare : 'a * 'a -> order
为每种类型产生这种关系。因此,我们需要一个不同的
满足我们希望放入的每种类型的 ORD_SET
签名的模块
有序集。因此,为了构造或操作有序的字符串集
和整数,我们必须为每种类型实现不同的模块[1]:
structure IntOrdSet = BinarySetFn ( type ord_key = int
val compare = Int.compare )
structure StrOrdSet = BinarySetFn ( type ord_key = string
val compare = String.compare )
然后我们必须使用适当模块中的拟合函数 希望对给定类型进行操作:
val strSet = StrOrdSet.fromList ["a","b","c"]
val intSet = IntOrdSet.fromList [1,2,3,4,5,6]
这里有一个非常简单的权衡:LIST
模块提供功能
范围在你喜欢的任何类型,但他们不能利用任何关系
介于任何特定类型的值之间; ORD_SET
模块提供
必须受限于仿函数中提供的类型的函数
参数,但通过相同的参数化,他们能够
包含有关内部结构和关系的特定信息
他们的目标类型。
很容易想象我们想要设计替代系列的情况 列表模块,使用仿函数将类型和其他值参数化为 提供具有更复杂结构的类列表数据类型:例如,指定 有序列表的数据类型,或使用自平衡二进制表示列表 搜索树。
在创建模块时,我认为也很容易识别它何时会 能够提供多态函数以及何时需要参数化 在某些类型上。对我来说,似乎更困难的是弄清楚哪一种 在进一步下游工作时你应该依赖的模块。
一般来说,我的问题是:当我设计一个系统时 各种相关的模块,我怎样才能弄清楚是否围绕模块进行设计 提供使用仿函数生成的多态函数或模块 在类型和值上参数化?
我希望用下面的例子来说明这个困境及其重要性, 取自我正在从事的玩具项目。
我有一个functor PostFix (ST:STACK) : CALCULATOR_SYNTAX
。这需要一个
堆栈数据结构的实现并生成一个解析器读取
具体后缀 ("reverse polish") 表示法转换为抽象语法(待
由下游的计算器模块评估),反之亦然。现在,我已经
使用提供多态堆栈类型的标准堆栈接口和
对其进行操作的函数数量:
signature STACK =
sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
end
这很好用,并且给了我一些灵活性,因为我可以使用基于列表的堆栈
或基于矢量的堆栈,或其他。但是,假设我想添加一个简单的日志记录
堆栈模块的功能,以便每次将元素推入或
从堆栈弹出,它打印出堆栈的当前状态。现在我会
需要 fun toString : 'a -> string
作为堆栈收集的类型,并且
据我了解,这不能合并到 STACK
模块中。现在我
需要将类型密封到模块中,并通过类型参数化模块
收集在堆栈中和一个 toString
函数,可以让我生成一个
收集类型的可打印表示。所以我需要像
functor StackFn (type t
val toString: t -> string ) =
struct
...
end
这将不会产生一个匹配STACK
签名的模块,因为它
不提供多态类型。因此,我必须更改所需的签名
对于 PostFix
函子。如果我有很多其他模块,我必须改变
所有这些也是如此。这可能会带来不便,但真正的问题是我
不能再使用我简单的基于列表或基于向量的 STACK
模块
PostFix
当我 不想 想要记录时的仿函数。现在看来,我得回去了
并重写这些模块,使其也具有密封类型。
因此 return 扩展并(幸运地)完成我的问题:
- 是否有一些方法可以指定由
StackFn
这样他们最终会成为 "special cases" ofSTACK
? - 或者,有没有办法为
PostFix
模块编写签名 这将允许StackFn
生成的模块和那些 满足STACK
? - 一般来说,有没有一种方法来思考两者之间的关系? 将来可以帮助我 catch/anticipate 这类事情的模块?
(如果你已经读到这里了。非常感谢!)
正如您所发现的,参数多态性与 SML 和 OCaml 中的 functors/module 之间存在紧张关系。这主要是由于模块的 "two language" 性质和缺乏临时多态性。 1ML and modular implicits 两者都针对该问题提供了不同的解决方案。第一个是统一两种参数化,第二个是允许在需要时激发一些特殊的多态性。
回到实际考虑。使用仿函数,单态化给定的数据结构相当容易(但 verbose/annoying)。这是一个示例(在 OCaml 中)。有了这个,您仍然可以编写通用实现并在以后专门化它们(通过提供打印功能)。
module type POLYSTACK = sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
val toString : ('a -> string) -> 'a stack -> string
end
module type STACK = sig
type elt
type t
exception EmptyStack
val empty : t
val isEmpty : t -> bool
val push : (elt * t) -> t
val pop : t -> t
val top : t -> elt
val popTop : t -> t * elt
val toString : t -> string
end
module type PRINTABLE = sig
type t
val toString : t -> string
end
module Make (E : PRINTABLE) (S : POLYSTACK)
: STACK with type elt = E.t and type t = E.t S.stack
= struct
type elt = E.t
type t = E.t S.stack
include S
let toString = S.toString E.toString
end
module AddLogging (S : STACK)
: STACK with type elt = S.elt and type t = S.t
= struct
include S
let push (x, s) =
let s' = S.push (x, s) in print_string (toString s') ; s'
end