Reputation: 3172
Given this type constructor:
data DatabaseItem = DbString String
| DbNumber Integer
| DbDate UTCTime
I can write a function that unwraps a DatabaseItem
to, for example, a UTCTime
:
getDate :: DatabaseItem -> Maybe UTCTime
getDate (DbDate a) = Just a
getDate _ = Nothing
Rather than write a function like this for each of the 3 data constructors, I want a general function (which also means I don't need the Maybe
anymore), but I can't quite figure out how to write it. I tried:
unwrap :: DatabaseItem -> a
unwrap (i a) = a
-- error: Parse error in pattern: i
And:
unwrap :: DatabaseItem -> String | Integer | UTCTime
unwrap (DbString a) = a
unwrap (DbDate a) = a
unwrap (DbNumber a) = a
-- error: parse error on input ‘|’
Neither compiles. Could someone point out what's wrong with these, and suggest a better implementation? Thanks!
Upvotes: 0
Views: 155
Reputation: 50929
Edited: Added response to comment.
In case you can't figure out why no one is answering your question directly, note that you can write unwrap
as follows, using 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
It can be used like this:
> 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
>
Now, go try to use it in some actual code. You will find it a largely frustrating experience and realize that either having separate functions, like:
getString (DbString x) = Just x
getString _ = Nothing
or using a catamorphism or prism will be a much better approach.
In a follow-up comment, you asked why the Haskell version was so much more complicated than the TypeScript version, which doesn't require a cast:
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
As @amalloy noted, your TypeScript example isn't really analogous to the Haskell example we've been discussing. In the Haskell example, DbString "Hello"
and DbNumber 42
have the same type. In the TypeScript example, DbString("Hello")
is of type DatabaseItem<string>
and DbNumber(42)
is of type DatabaseItem<number>
. A Haskell version of this TypeScript code would look more like this, which seems pretty similar in structure to the TypeScript example and doesn't involve any casts:
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)
Upvotes: 1
Reputation: 32319
I agree with Daniel's suggestion, but it is worth pointing out that lens
has the notion of Prism
which lets you do this too (and much more!). Especially given the gist you link in the comments, this might be interesting
{-# Language TemplateHaskell #-}
import Data.Time.Clock
import Control.Lens.TH
data DatabaseItem = DbString String
| DbNumber Integer
| DbDate UTCTime
makePrisms ''DatabaseItem
This automatically generates _DbString
, _DbNumber
and _DbDate
functions which can easily be adapted inline to do what getString
, getNumber
, and getDate
would do. Namely:
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
However, lens
is a fair bit more powerful. It can filter through your data base to collect one of the variants in one line too. For example, I can get all the dates in theDatabase :: [DatabaseItem]
using just theDatabase ^.. each . _DbDate
.
Upvotes: 3
Reputation: 152927
A common pattern for user-defined datatypes is to define a catamorphism for them; e.g. in the standard library there are foldr
for []
, maybe
for Maybe
, bool
for Bool
, either
for Either
, and so forth. A catamorphism is essentially a reification of a pattern match into a function, together with a tiny bit of fanciness for recursive types which isn't relevant here.
For your type, it might look like this:
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
For example, if you wanted to get a string representing the item, you might use:
databaseItem id show (formatTime defaultTimeLocale "%c")
:: DatabaseItem -> String
You can also implement your constructor-specific extractors in terms of it.
getDate = databaseItem (const Nothing) (const Nothing) Just
There is significantly more material on catamorphisms and why they are the Right Choice for consuming ADTs scattered around the web if this piques your interest.
Upvotes: 5