Reputation: 56935
I have a function like so:
test :: [Block] -> [Block]
test ((Para foobar):rest) = [Para [(foobar !! 0)]] ++ rest
test ((Plain foobar):rest) = [Plain [(foobar !! 0)]] ++ rest
Block
is a datatype that includes Para
, Plain
and others.
What the function does is not particularly important, but I notice that the body of the function ([Para [(foobar !! 0)]] ++ rest
) is the same for both Para
and Plain
, except that the constructor used is the type of foobar
.
Question: is there someway to concisely write this function with the two cases combined? Something like
test :: [Block] -> [Block]
test ((ParaOrPlain foobar):rest) = [ParaOrPlain [(foobar !! 0)]] ++ rest
where the first ParaOrPlain
matches either Para foobar
or Plain foobar
and the second ParaOrPlain
is Para
or Plain
respectively.
Note that Block
can also be (say) a BulletList
or OrderedList
and I don't want to operate on those. (edit: test x = x
for these other types.)
The key is that I don't want to replicate the function body twice since they're identical (barring the call to Para
or Plain
).
I get the feeling I could use Either
, or perhaps my own data type, but I'm not sure how to do it.
Edit To clarify, I know my function body is cumbersome (I'm new to Haskell) and I thank various answerers for their simplifications.
However at the heart of the problem, I wish to avoid the replication of the Para
and Plain
line. Something like (in my madeup language...)
# assume we have `test (x:rest)` where `x` is an arbirtrary type
if (class(x) == 'Para' or class(x) == 'Plain') {
conFun = Plain
if (class(x) == 'Para')
conFun = Para
return [conFun ...]
}
# else do nothing (ID function, test x = x)
return x:rest
i.e., I want to know if it's possible in Haskell to assign a constructor function based on the type of the input parameter in this way. Hope that clarifies the question.
Upvotes: 4
Views: 359
Reputation: 15683
This can be done pretty nicely with ViewPatterns
Note: view patterns isn't actually required here, it just makes it looks a bit nicer imho
Note': This assumes the list inside the block is of the same type in both cases
{-# LANGUAGE ViewPatterns #-}
paraOrPlain :: Block a- > Maybe (a -> Block a, a)
paraOrPlain (Plain xs) = Just (Plain,xs)
paraOrPlain (Para xs) = Just (Para,xs)
paraOrPlain _ = Nothing
-- or even better
para (Para xs) = Just (Para,xs)
para _ = Nothing
plain (Plain xs) = Just (Plain,xs)
plain _ = Nothing
paraOrPlain' b = para b <|> plain b -- requires Control.Applicative
test ((paraOrPlain -> Just (con,xs)) : rest) = con (take 1 xs) : rest
Of course you still need to do the pattern matching somewhere - you can't do that generically without TH or Generic - but you can do it in a way that lends itself to reuse.
Upvotes: 1
Reputation: 120731
The closest you can get to your original idea is probably a "generic Para
-or-Plain
-modifying" function.
{-# LANGUAGE RankNTypes #-}
modifyBlock :: (forall a . [a]->[a]) -> Block -> Block
modifyBlock f (Plain foobar) = Plain $ f foobar
modifyBlock f (Para foobar) = Plain $ f foobar
Observe that f
has in each use a different type!
With that, you can then write
test (b:bs) = modifyBlock (\(h:_) -> [h]) b : bs
Upvotes: 1
Reputation: 76280
Assuming a data type declaration in the form:
data Block
= Para { field :: [Something] }
| Plain { field :: [Something] }
...
You can simply use a generic record syntax:
test :: [Block] -> [Block]
test (x:rest) = x { field = [(field x) !! 0] } : rest
Upvotes: 3
Reputation: 6610
This is about as nice as I can get it without using Template Haskell or Data.Data :-)
First of all, we need to enable some extensions and fix the Block
datatype for concreteness (if my simplifying assumptions are wrong, tell me and I'll see what can be salvaged!)
{-# LANGUAGE GADTs, LambdaCase #-}
data Block = Para [Int]
| Plain [String]
| Other Bool Block
| YetAnother deriving (Show,Eq)
The important point here is that Para
and Plain
are unary constructors, but that the datatype itself can contain constructors with a different number of arguments.
As @leftaroundabout and @user5402 explained, we can separate the concerns of modifying a single Block
and applying that modification to the first element of a list only, so I'll focus on rewriting
modifyBaseline :: Block -> Block
modifyBaseline (Para xs) = Para [xs !! 0]
modifyBaseline (Plain xs) = Plain [xs !! 0]
modifyBaseline rest = rest
Next, we need to be able to talk about unary constructors as values. There are 3 important things here:
We package this up nicely in a custom type (t
is the type the constructor belongs to, and a
is what's inside):
data UnaryConstructor t a = UnaryConstructor {
destruct:: t -> Maybe a
construct :: a -> t
}
So now we can define
para :: UnaryConstructor Block [Int]
para = UnaryConstructor (\case { Para ns -> Just ns ; _ -> Nothing }) Para
plain :: UnaryConstructor Block [Int]
plain = UnaryConstructor (\case { Plain ss -> Just ss ; _ -> Nothing }) Plain
(You can get rid of the LambdaCase
extension if you write (\xs -> case xs of { Para ns -> Just ns; _ -> Nothing})
but this way it's nice and compact!)
Next, we need to 'package up' a unary constructor and the function to apply to the value contained in it:
data UnaryConstructorModifier t where
UnaryConstructorModifier :: UnaryConstructor t a -> (a -> a) -> UnaryConstructorModifier t
so that we can write
modifyHelper :: [UnaryConstructorModifier t] -> t -> t
modifyHelper [] t = t
modifyHelper ((UnaryConstructorModifier uc f):ucms) t
| Just x <- destruct uc t = construct uc (f x)
| otherwise = modifyHelper ucms t
and finally (the lambda could have been (\xs -> [xs !! 0])
or (\xs -> [head xs])
to taste):
modify :: Block -> Block
modify = modifyHelper [UnaryConstructorModifier para (\(x:_) -> [x]),
UnaryConstructorModifier plain (\(x:_) -> [x])]
If we now test it with
ghci> let blocks = [Para [1,2,3,4], Plain ["a","b"], Other True (Para [42,43]), YetAnother ]
ghci> map modify blocks == map modifyBaseline blocks
we get True
- hooray!
The repetitive bit is now in the definitions of para
and plain
where you have to write the name of the constructor three times, but there's no way around that without using Template Haskell or Data.Data (that I can see).
Further options for improvement would be to do something for constructors of different arity, and to put a function of type a -> Maybe a
in UnaryConstructorModifier
to deal with the partiality of (\(x:_) -> [x]
) but I think this answers your question nicely.
I hope you can make sense of this, as I've glossed over a couple of details, including what's going on in the definition of UnaryConstructorModifier
and the use of pattern guards in modifyHelper
- so ask if you need clarification!
Upvotes: 1
Reputation: 120731
First note that, as user5402 already wrote, you should replace [Para [(foobar !! 0)]] ++ rest
with Para [foobar !! 0] : rest
, which looks already a good deal nicer. Next note that basically, all you're doing is modify the head of a list, so as a meaningful helper I'd use
modifyHead :: (a->a) -> [a] -> [a]
modifyHead f (x:xs) = f x : xs
modifyHead _ [] = []
test = modifyHead m
where m (Para foobar) = Para [head foobar] -- `head` is bad, but still better than `(!!0)`!
m (Plain foobar) = Plain [head foobar]
Upvotes: 1
Reputation: 52049
One approach is to use a (polymorphic) helper function, e.g.:
helper ctor xs rest = ctor [ head xs ] : rest
test :: [Block] -> [Block]
test ((Para xs):rest) = helper Para xs rest
test ((Plain xs):rest) = helper Plain xs rest
test bs = bs
Note that Para
and Plain
are just functions of type [whatever] -> Block
.
Upvotes: 3