给定这种类型的构造函数:
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 ‘|’
既不编译。有人可以指出这些有什么问题,并提出更好的实施建议吗?谢谢!
答案 0 :(得分:5)
用户定义的数据类型的一个常见模式是为它们定义一个catamorphism;例如在标准库中,foldr
为[]
,maybe
为Maybe
,bool
为Bool
,either
为Either
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)
我同意丹尼尔的建议,但值得指出的是lens
有Prism
的概念,它允许你这样做(以及更多!)。特别是考虑到你在评论中链接的要点,这可能很有趣
{-# Language TemplateHaskell #-}
import Data.Time.Clock
import Control.Lens.TH
data DatabaseItem = DbString String
| DbNumber Integer
| DbDate UTCTime
makePrisms ''DatabaseItem
这会自动生成_DbString
,_DbNumber
和_DbDate
个函数,这些函数可以轻松地内联调整以执行getString
,getNumber
和{{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)