vek
vek

Reputation: 1543

Is there a way to use constructors of one type as types too?

For example:

data Foo = Bar Int | Baz String

I would like to use a function like:

qux :: [Foo] -> [Baz]
qux fs = filter f fs
    where
        f (Baz _) = True
        f _ = False

Normally, i need to:

data Bar = Bar Int
data Baz = Baz String

data Foo = FBar Bar | FBaz Baz

qux = ...

but I'm not happy with this solution, because it needs lots of redundant code.

Is there a elegant way to solve this issue?

Upvotes: 3

Views: 182

Answers (4)

Will Ness
Will Ness

Reputation: 71119

With list comprehensions,

qux fs = [Baz x | Baz x <- fs]

though the type stays the same, while you want an automatic "subtype downcast". Or, define

foo bar baz v | Bar x <- v = bar x | Baz y <- v = baz y

qux fs = filter (foo (const False) (const True)) fs

but I suspect that you want more complex subcase choices, like in case with more than two variants, accept all but one. The type analyzer function foo will help you with that too. Luis Casillas mentions it too, but in more complex setting. It can just be used with your original type, unaltered.

But, again, the result type stays the same, unlike what you wanted. If you must have type-level distinction for qux's results, you can just tag them, leaving Foo as is:

newtype Baz a = Baz' a
newtype Bar a = Bar' a

qux :: [Foo] -> Baz [Foo]
qux = Baz' . filter (foo (const False) (const True))

Or you could push Baz' inside the list, with qux :: [Foo] -> [Baz Foo]; qux = map Baz' . filter .... In any case the type analyzer function approach is helpful.

Upvotes: 1

Luis Casillas
Luis Casillas

Reputation: 30237

It sounds to me as if you're making an analogy between these two things:

  1. The type constructor Foo and its variant constructors Bar and Baz;
  2. An object-oriented type Foo and its subtypes Bar and Baz.

Haskell doesn't work that way at all. No type is ever a subtype or supertype of any other; type constructors like Foo and value constructors like Bar/Baz are completely separate entities.

There are several things you can try to do here. The first and simplest one is what jwodder suggests in a comment: write a [Foo] -> [String]. This has the virtue of being simple and direct, with the disadvantage that you may want a type that's not String.

A variant of this is to define types Bar and Baz separate from Foo, and define Foo in terms of these:

newtype Bar = Bar Int
newtype Baz = Baz String
data Foo = ABar Bar | ABaz Baz

This now has the virtue that you can't use a plain String where a Baz is expected; the downside is that it's noticeably more verbose, but you can write helper functions to make things briefer:

-- Virtual constructors for `Foo`
bar :: Int -> Foo
bar i = ABar (Bar i)

baz :: String -> Foo
baz s = ABaz (Baz s)

-- A helper function to do the pattern matching for you.  You give it a pair of
-- functions, one telling it how to process the `Int` in a `Bar`, the other how 
-- to process the `String` in a `Baz`. 
analyzeFoo :: (Int -> r) -> (String -> r) -> Foo -> r
analyzeFoo barCase bazCase (ABar (Bar i) = barCase i
analyzeFoo barCase bazCase (ABaz (Baz s) = bazCase s

Then there are the more advanced solutions, which perhaps you shouldn't worry about. David Young's uses GADTs and empty types to tag the Foo type to indicate what type of element it carries, so that Foo BarType can only be constructed with Bar, and Foo BazType only with Baz.

Another advanced solution goes this way: first, you parametrize your Foo type so that instead of Int and String you have type variables:

data Foo a b = Bar a | Baz b

Now, one way of specializing this type so that it can only be the Baz case is to instantiate type variable a to a type that contains no values; if a has no values, then you can't hit the Bar case unless the program errors out or runs forever.

The void library comes with such a type, called Void. Using that:

import Data.Void

data Foo' a b = Bar a | Baz b

type Foo = Foo' Int String
type Bar = Foo' Int Void
type Baz = Foo Void String

qux :: [Foo] -> [Baz]
qux = concatMap f
    where f (Bar i) = []
          f (Baz s) = [Baz s]

example :: [Baz] -> [Baz]
example = map go
    where go (Bar v) = absurd v        -- can't happen, no value exists for `v`
          go (Baz s) = Baz (s ++ "!")

The trick here is that the type Foo' Void String basically means "either a Bar that contains something that doesn't exist (Void), or a Baz that contains a String." Which means that it must be the latter.

Upvotes: 1

David Young
David Young

Reputation: 10791

One possibility is to use GADTs. The gist of it is that this allows us to have a so-called "phantom" type argument to a data type definition. One thing we can do with this is restrict the type of a function (for instance) that uses this data type so that it only works with values that could be created by a certain constructor.

Here is how I would write the example you gave in this style (Note: You need to have the GADTs extension enabled):

{-# LANGUAGE GADTs #-}
data BarType -- Note: These could be called Bar and Baz, but I named them this for clarity
data BazType

data Foo a where
    Bar :: Int -> Foo BarType
    Baz :: Int -> Foo BazType

With this, you can write functions like this:

onlyAcceptsBars :: Foo BarType -> Int
onlyAcceptsBars (Bar bar) = -- ...
-- There would be a compile-time error if you had a case like this:
--    onlyAcceptsBars (Baz baz) = ...

onlyAcceptsBazs :: Foo BazType -> Int
onlyAcceptsBazs (Baz baz) = -- ...

acceptsAnyFoo :: Foo a -> Int
acceptsAnyFoo (Bar bar) = -- ...
acceptsAnyFoo (Baz baz) = -- ...

Upvotes: 3

Chris Taylor
Chris Taylor

Reputation: 47402

No, there is no way to do exactly what you want. Let's take the following example

data Foo = Bar Int String | Baz Bool Int

isBaz (Baz _ _) = True
isBaz  _        = False

To achieve what you want, you have essentially three options. First, filter without bothering to change the type

qux :: [Foo] -> [Foo]
qux = filter isBaz 

Alternatively, you can turn your Foos into tuples

qux :: [Foo] -> [(Bool, Int)]
qux = map f . filter isBaz
  where
    f (Baz b i) = (b,i)

Or you can define a new type

data Baz' = Baz' Bool Int

qux :: [Foo] -> [Baz']
qux = map f . filter isBaz
  where
    f (Baz b i) = Baz' b i

Or you can rewrite Foo as you suggested in your question

data Bar = Bar Int String
data Baz = Baz Bool Int
data Foo = FooBar Bar | FooBaz Baz

isBaz (FooBaz _) = True
isBaz  _         = False

qux :: [Foo] -> [Baz]
qux = map f . filter isBaz
  where
    f (FooBaz baz) = baz

Hopefully you find one of these acceptable.

Upvotes: 1

Related Questions