bcherny
bcherny

Reputation: 3172

Get value from disjoint union of data constructors

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

Answers (3)

K. A. Buhr
K. A. Buhr

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

Alec
Alec

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

Daniel Wagner
Daniel Wagner

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

Related Questions