我正在创建自己的自定义可变/冻结数据类型,内部包含MVector
/ Vector
。由于性能原因,它需要是可变的,因此切换到不可变数据结构不是我正在考虑的事情。
似乎为两个版本之一实现一个观察者函数应该允许我只是窃取另一个类型的实现。以下是我正在考虑的两个选项:
render :: Show a => MCustom s a -> ST s String
render mc = ...non trivial implementation...
show :: Show a => Custom a -> a
show c = runST $ render =<< unsafeThaw c
unsafeThaw
调用Vector.unsafeThaw
的封面,这应该是安全的,因为解冻的矢量永远不会变异,只能读取。这种方法感觉最干净,唯一的缺点是render
是严格的,强制show
是严格的,而重复的实现可以正确地流式传输字符串而不强制一次性。
另一种选择,感觉更脏,但我认为是安全的是这样做:
show :: Show a => Custom a -> a
show c = ...non trivial implementation that allows lazy streaming...
render :: Show a => MCustom s a -> ST s String
render mc = do
s <- show <$> unsafeFreeze mc
s `deepseq` pure s
这些都是我最好的选择吗?如果不是我该怎么办?
对我而言,从另一个版本构建一个版本似乎是最直观的。但似乎如果我将可变版本作为基本版本,那么我最终会得到更严格的要求,即使实现看起来相当干净和合乎逻辑,只是因为ST
需要严格,除非我投入一些unsafeInterleaveST
调用,但只有当通过不可变对象调用可变观察者时,这些调用才是安全的。
另一方面,如果我将不可变版本作为基本版本,那么我将使用更脏的deepseq
代码,有时我只需要重新实现。例如,只需复制冻结对象,然后在其上调用unsafeThaw
并在调用unsafeFreeze
并返回之前修改副本就可以非常轻松地在冻结对象上完成所有就地编辑功能。但是,相反的做法并不可行,因为用于不可变版本的副本修改无法转换为就地修改。
我是否应该将所有修改函数与可变实现一起编写,并将所有观察器函数与不可变实现一起编写。然后有一个依赖于这两者的文件通过unsafeThaw
和unsafeFreeze
统一所有内容?
答案 0 :(得分:0)
如何拥有纯粹的功能
show :: (StringLike s, Show a) => Custom a -> s
您可以使用s
的不同实例来获取延迟和严格输出,其中cons
是惰性或严格的;例如String
或Text
:
class StringLike s where
cons :: Char -> s -> s
nil :: s
uncons :: s -> Maybe (Char, s)
instance StringLike String where ...
instance StringLike Text where ...
您可以使用其他方法,例如幻象类型,或简单地有两个函数(showString
和showText
),以区分懒惰和严格输出,如果你愿意。但是如果你将类型看作函数语义的规范,那么表示懒惰或严格的地方就是该操作的返回类型。这样就无需对show
内的Custom
进行某种严格的ST
。
对于MCustom
版本,您可能不会导出String
版本,例如:
render :: MCustom s a -> ST s Text
render a = show <$> unsafeFreeze a
当函数运行时,您可以使用seq
强制执行结果,但无论如何都会强制使用整个Text
。
但最简单的解决方案似乎只是抽象出以不可变的方式使用可变结构的模式,例如:
atomically :: (NFData a) => (Custom x -> a) -> MCustom s x -> ST s a
atomically f v = do
r <- f <$> unsafeFreeze v
r `deepseq` pure r
这样可以避免在代码中使用unsafeFreeze/deepseq
,就像你有modify
对可变向量进行不可变操作一样。