Reputation: 1543
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
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
Reputation: 30237
It sounds to me as if you're making an analogy between these two things:
Foo
and its variant constructors Bar
and Baz
;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
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
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 Foo
s 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