Haskell中的"Dependent optional"数据

12

考虑一个DateTime类型,日期必须存在,但是秒钟部分是可选的。如果时间部分存在,则可能会有一个可选的毫秒部分。如果毫秒存在,则可能会有纳秒部分。

有很多处理这种情况的方法,例如:

--rely on smart constructors 
data DateTime = DateTime { days:: Int, 
                           sec :: Maybe Int, 
                           ms :: Maybe Int, 
                           ns :: Maybe Int 
                         }

-- list all possibilities
data DateTime = DateOnly Int 
              | DateWithSec Int Int
              | DateWithMilliSec Int Int Int
              | DateWithNanoSec Int Int Int Int    

-- cascaded Maybe
data DateTime = DateTime Int (Maybe (Int, Maybe (Int, Maybe Int)))

-- cascaded data
data Nano = NoNano | Nano Int
data MilliSec = NoMilliSec | MilliSec Int Nano
data Sec = NoSec | Sec Int MilliSec
data Date = Date Int Sec
你会使用哪种结构(当然不仅限于上面的示例),为什么?
【意图】
我正在探索在Frege中实现日期类型的可能性(http://code.google.com/p/frege/),使用date4j的DateTime作为指南(因为Haskell的日期和时间库太复杂了,而java.util.Date太过混乱)。在我的目前的玩具实现中,所有字段都是必须的,但当然可以使用户摆脱不需要的精度(而原始实现中有可选字段)。
所以主要目标是:
  • 安全:必须尽一切可能避免非法状态
  • 方便:应该很容易使用这种类型,例如模式匹配很酷,日历计算应该很容易...
不那么重要的是:
  • 性能:当然,与类型一起工作不应该太慢,但对于典型用途,它不必挤出最后一个时钟周期
  • 内存:在真正重要的情况下,很容易导出更紧凑的存储格式
  • 简洁的实现:这是一个库,我愿意添加所有必要的代码使事情变得顺畅
话虽如此,所有这些都非常暂时,并不应该太严肃对待。

1
不确定在没有了解更多您的要求的情况下是否能够回答这个问题。它可能取决于许多因素,包括您计划在类型上定义哪些函数、性能和空间方面的考虑(间接数量、展开字段的机会等)。 - hammar
2
另一个选择是使用一个简单的构造函数 DateTime = DateTime [Int],该函数未从库中导出,并强制所有与类型的交互都通过诸如 mkDateTimeWithSec :: [Int] -> DateTimeisDateTimeWithSec :: DateTime -> BoolgetSeconds :: DateTime -> Maybe Int 等函数进行。您不能使用模式匹配(但也许这并不可怕,因为您只需用守卫替换它并使用访问器函数),但您明确禁止用户创建非法状态,因为您可以在构造时检查所有内容(例如,您可以检查负值)。 - Chris Taylor
1
原始的断言是未指定数据是可选的,而不是默认的,这使得类型非常牵强 - 编写用于操作它的函数将很繁琐。你也可能会遇到一些垃圾数据,比如只有纳秒值但没有秒数的情况。我会改变策略,看看在理论和实践中,对未指定时间值默认为0是否有害。 - stephen tetley
4个回答

10

这不是一个答案,但是它太长了以至于不能作为评论,把它放在这里会更加清晰。

另外一种处理方法是:使用单个 DateTime 类型来始终存储所有字段,并附带一个表示精度的参数,例如:

data Precision = Days | Seconds | Milliseconds | Nanoseconds deriving (Ord, Eq {- etc -})
data DateTime = DateTime { prec :: Precision,
                           days :: Int, 
                           sec :: Int,
                           ms :: Int,
                           ns :: Int }

使用智能构造函数,将未使用的参数设置为0。如果您有dateDifference或其他内容,则可以通过传播精度来实现(Ord实例会使这更加简洁)。

(我对此是否好/Haskell-y没有什么好主意,但其他解决方案似乎非常混乱,也许这更加优雅。)


1
我正想着自己提出这种解决方案。 - augustss
1
这是我会建议的一种方式,但我也会建议将 secmsns 字段合并为一个名为 withinDay 的字段,其解释取决于 prec 字段的值,可以是秒、毫秒或纳秒。 - Daniel Wagner
2
@DanielWagner,我认为这样做行不通,除非改变数据类型:(对于32位平台)Int的最大值是几十亿,但一天有860万亿纳秒。 - huon

8
“必须尽一切可能避免非法状态”和“模式匹配很酷”是良好的原则,但在此情况下直接相互冲突。此外,日期和时间是复杂的人类文化构造,有许多边缘案例和不规则角落。它们不是我们可以轻易编码到类型系统中的规则。因此,在这种情况下,我会选择不透明数据类型,智能构造函数和智能解构函数。在需要使用模式匹配时,总是可以使用视图模式和模式守卫。 (我甚至还没有讨论依赖可选数据作为激励因素。)

在这里,查看模式将非常有用,但目标语言Frege(除了非常类似于Haskell之外)目前不支持。 - Landei
很不幸。我认为无论如何我都会这样做,并希望Frege在未来支持视图模式。 - dave4420

3

受 @dbaupp 解决方案的启发,我想将幻影类型版本添加到候选项中:

-- using EmptyDataDecls
data DayPrec 
data SecPrec 
data MilliPrec 
data NanoPrec 

data DateTime a = DateTime { days :: Int, sec :: Int, ms :: Int, ns :: Int } 

date :: Int -> DateTime DayPrec
date d = DateTime d 0 0 0

secDate :: Int -> Int -> DateTime SecPrec
secDate d s = DateTime d s 0 0

...    

--will work only for same precision which is a Good Thing (tm)
instance Eq (DateTime a) where
  (DateTime d s ms ns) == (DateTime d' s' ms' ns') = [d,s,ms,ns] == [d',s',ms',ns'] 

如果我没有误解,这将允许我使用一种类型,但如果需要区分精度,则可以进行区分。但我想也会有一些缺点...

我喜欢这个,但我认为某些事情会变得有点棘手,例如计算DateTime DayPrecDateTime SecPrec之间的时间并获得正确的精度(大概是DayPrec):我认为这需要类似于函数依赖或类型家族的东西,或者需要键入大量实例声明。(假设这样的操作是期望的且有明确定义的。) - huon
1
我认为用户不应该混合使用不同的精度,而是在计算之前明确地截断或扩展精度。 - Landei
parseDateTime的类型将是什么?(如果程序员想要保留原始字符串中的精度级别,但在编译时不知道可以期望多少精度?) - dave4420
同意,那将是不方便的。但是,您可以编写像 isMilliPrecision :: String -> Bool 这样的函数来在实际解析之前进行测试,以及 parseMilliPrecision :: String -> Maybe (DateTime MilliPrec)。请注意,如果您从数据库中读取日期,或者转换 java.util.Date(应该被视为 MilliPrec),则不会出现此问题,因为您知道预期的精度。 - Landei
@dbaupp,你基本上需要执行一种类型级别的min操作,在Haskell世界中这并不罕见。 - Dan Burton

0

假设您只想解决这个问题(而不是更一般的问题),那么decimal库可能是您想要的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接