从数据构造函数的不相交联合中获取价值

时间:2017-09-20 21:57:58

标签: haskell

给定这种类型的构造函数:

data DatabaseItem = DbString String
                  | DbNumber Integer
                  | DbDate   UTCTime

我可以编写一个函数,将DatabaseItem展开,例如UTCTime

getDate :: DatabaseItem -> Maybe UTCTime
getDate (DbDate a) = Just a
getDate _ = Nothing

不是为3个数据构造函数中的每一个编写这样的函数,我想要一个通用函数(这也意味着我不再需要Maybe),但是我不知道如何写它。我试过了:

unwrap :: DatabaseItem -> a
unwrap (i a) = a

-- error: Parse error in pattern: i

unwrap :: DatabaseItem -> String | Integer | UTCTime
unwrap (DbString a) = a
unwrap (DbDate a) = a
unwrap (DbNumber a) = a

-- error: parse error on input ‘|’

既不编译。有人可以指出这些有什么问题,并提出更好的实施建议吗?谢谢!

3 个答案:

答案 0 :(得分:5)

用户定义的数据类型的一个常见模式是为它们定义一个catamorphism;例如在标准库中,foldr[]maybeMaybeboolBooleitherEither 1}},等等。一个catamorphism本质上是一个模式匹配到一个函数的具体化,以及递归类型的一点点的幻想,这在这里是无关紧要的。

对于您的类型,它可能如下所示:

databaseItem ::
    (String       -> a) ->
    (Integer      -> a) ->
    (UTCTime      -> a) ->
    (DatabaseItem -> a)
databaseItem string number date item = case item of
    DbString s -> string s
    DbNumber n -> number n
    DbDate   d -> date   d

例如,如果您想获得表示该项目的字符串,可以使用:

databaseItem id show (formatTime defaultTimeLocale "%c")
    :: DatabaseItem -> String

您还可以根据它实现特定于构造函数的提取器。

getDate = databaseItem (const Nothing) (const Nothing) Just

关于catamorphisms有更多的材料,以及为什么它们是消费分散在网络上的ADT的正确选择,如果这引起你的兴趣。

答案 1 :(得分:3)

我同意丹尼尔的建议,但值得指出的是lensPrism的概念,它允许你这样做(以及更多!)。特别是考虑到你在评论中链接的要点,这可能很有趣

{-# Language TemplateHaskell #-}
import Data.Time.Clock
import Control.Lens.TH

data DatabaseItem = DbString String
                  | DbNumber Integer
                  | DbDate   UTCTime

makePrisms ''DatabaseItem

这会自动生成_DbString_DbNumber_DbDate个函数,这些函数可以轻松地内联调整以执行getStringgetNumber和{{1} } 会做。即:

getDate

然而,main> import Control.Lens main> :t (^? _DbString) (^? _DbString) :: DatabaseItem -> Maybe String main> :t (^? _DbNumber) (^? _DbNumber) :: DatabaseItem -> Maybe Integer main> :t (^? _DbDate) (^? _DbDate) :: DatabaseItem -> Maybe UTCTime 更强大。它可以过滤您的数据库,也可以在一行中收集其中一个变体。例如,我可以仅使用lens获取theDatabase :: [DatabaseItem]中的所有日期。

答案 2 :(得分:1)

已编辑:已添加对评论的回复。

如果您无法弄清楚为什么没有人直接回答您的问题,请注意您可以使用unwrap编写Data.Typeable,如下所示:

import Data.Maybe
import Data.Time
import Data.Typeable

data DatabaseItem = DbString String
                  | DbNumber Integer
                  | DbDate   UTCTime

unwrap :: (Typeable a) => DatabaseItem -> a
unwrap x = case x of
             DbString x -> go x
             DbNumber x -> go x
             DbDate   x -> go x
  where go :: (Typeable a, Typeable b) => a -> b
        go = fromMaybe (error "unwrap: type mismatch") . cast

可以像这样使用:

> unwrap (DbNumber 1) :: Integer
1
> 1 + unwrap (DbNumber 1)
2
> 1 + unwrap (DbString "foo")
*** Exception: unwrap: type mismatch
CallStack (from HasCallStack):
  error, called at Unwrap.hs:16:25 in main:Main
>

现在,尝试在一些实际代码中使用它。您会发现这是一个非常令人沮丧的经历,并意识到要么具有单独的功能,例如:

getString (DbString x) = Just x
getString _ = Nothing

或使用catamorphism或prism将是一个更好的方法。

在后续评论中,您问为什么Haskell版本比TypeScript版本复杂得多,后者不需要强制转换:

type DatabaseItem<T> = { value: T }

let DbString = (value: string) => ({ value })
let DbNumber = (value: number) => ({ value })
let DbDate = (value: Date) => ({ value })

function unwrap<T>({ value }: DatabaseItem<T>) {
  return value
}

unwrap(DbString("Hello")) // "Hello"
unwrap(DbNumber(42)) // 42
unwrap(DbDate(new Date)) // Date

正如@amalloy所说,你的TypeScript示例与我们讨论过的Haskell示例并不完全相似。在Haskell示例中,DbString "Hello"DbNumber 42具有相同的类型。在TypeScript示例中,DbString("Hello")的类型为DatabaseItem<string>DbNumber(42)的类型为DatabaseItem<number>。这个TypeScript代码的Haskell版本看起来更像这样,它在结构上看起来与TypeScript示例非常相似,并且不涉及任何强制转换:

import Data.Time

newtype DatabaseItem a = Item { unwrap :: a }

dbString :: String -> DatabaseItem String
dbString = Item

dbNumber :: (Num a) => a -> DatabaseItem a
dbNumber = Item

dbDate :: UTCTime -> DatabaseItem UTCTime
dbDate = Item

main = do print $ unwrap (dbString "Hello")
          print $ unwrap (dbNumber 42)
          now <- zonedTimeToUTC <$> getZonedTime
          print $ unwrap (dbDate now)