Reputation: 1010
I have this code
data Container = Box Int | Bag Int
inBox :: [Container] -> Int
inBox [] = 0
inBox (x:ls) | (Box i) <- x = i + inBox ls
| otherwise = inBox ls
inBag :: [Container] -> Int
inBag [] = 0
inBag (x:ls) | (Bag i) <- x = i + inBag ls
| otherwise = inBag ls
Clearly InBox
and InBag
have the same structure. I would like to make a function that encompass both of them. What I don't know how to get is to pass the constructor (either Box
or Bag
) as a parameter.
Ideally, the general function woul look something like this:
inSome :: Constructor -> [Container] -> Int
inSome con [] = 0
inSome con (x:ls) | (con i) <- x = i + inSome con ls
| otherwise = inSome con ls
Obviously that doesn't work because constructor is not a defined type here. How can I do it?
One idea is to pass it as a function like so:
inSome :: (Int -> Container) -> [Container] -> Int
inSome _ [] = 0
inSome con (x:ls) | (con i) <- x = i + inSome ls
| otherwise = inSome ls
But then I get the error:
Parse error in pattern: con
Because it cannot match on functions like that.
The reason I want to do this is because I have a complicated data type that includes binary operations (e.g. +, #, ::, etc...) I have several functions that are almost identical for these constructors. I would hate to write all of them and modify them all together. There must me a way to do it in a function. Maybe someone can suggest another approach in the comments?
Upvotes: 4
Views: 2939
Reputation: 502
Splitting up your container into two datatypes:
data Sort = Bag | Box deriving (Eq, Show)
data Container = Container Sort Int deriving (Show)
Defining Sort
allows us to talk about sorts of containers without actually referring to any particular container. This lets us do more interesting things:
import Data.Maybe (fromMaybe)
-- Pull out the value regardless of sort
getVal :: Container -> Int
getVal (Container _ val) = val
-- If the sort passes a predicate, we get just the value
unwrapIf :: (Sort -> Bool) -> Container -> Maybe Int
unwrapIf p (Container b v) = if p b then Just v else Nothing
-- If a given sort matches the container, we get just the value
unwrap :: Sort -> Container -> Maybe Int
unwrap p = unwrapIf (p ==)
-- Unwrap with a default value if one was not found
unwrapDefault :: Int -> Sort -> Container -> Int
unwrapDefault def p = fromMaybe def . unwrap p
-- Unwrap with a default value of 0
unwrapValue :: Sort -> Container -> Int
unwrapValue = unwrapDefault 0
-- Unwrap Bag values, else 0
unbag :: Container -> Int
unbag = unwrapValue Bag
-- Unwrap Box values, else 0
unbox :: Container -> Int
unbox = unwrapValue Box
-- sum up the values in a list of containers
sumAll :: [Container] -> Int
sumAll = sum . map getVal
-- sum up the values in a list of one sort of container
sumContainer :: Sort -> [Container] -> Int
sumContainer s = sum . map (unwrapValue s)
-- sum up the bag values in a list of containers
sumBag :: [Container] -> Int
sumBag = sumContainer Bag
-- sum up the box values in a list of containers
sumBox :: [Container] -> Int
sumBox = sumContainer Box
Here sumContainer
is equivalent to your ideal inSome
function, and sumBag
/sumBox
are inBag
/inBox
respectively.
If you want to take it a step further, try generalising Container
to allow any value:
data Container a = Container Sort a deriving (Show)
Hope this helps!
Upvotes: 1
Reputation: 22460
If you want the function to count only one type depending on an argument, you could use a plain enum type to do the job. All that is needed essentially is a label to differentiate the type of operations you want. Passing the constructor or a wrapper such make_bag
, make_box
does not do better, and they can't be used in the pattern match as is.
data ConType = ON_BAG | ON_BOX
inCon :: ConType -> [Container] -> Int
inCon _ [] = 0
inCon t (x:ls) | ON_BAG <-t,(Bag i) <- x = i + (inCon t ls)
| ON_BOX <-t,(Box i) <- x = i + (inCon t ls)
| otherwise = inCon t ls
If you want to sum the numbers in the boxes and bags separately, I think you can do:
inCont :: [Container] -> (Int,Int)
inCont [] = (0,0)
inCont (x:ls) | (Bag i) <- x = addP (i,0) (inCont ls)
| (Box i) <- x = addP (0,i) (inCont ls)
| otherwise = inCont ls
To count them together is even easier:
inCont2 :: [Container] -> Int
inCont2 [] = 0
inCont2 (x:ls) | (Bag i) <- x = i + (inCont2 ls)
| (Box i) <- x = i + (inCont2 ls)
| otherwise = inCont2 ls
Upvotes: 1
Reputation: 74334
You can avoid using pattern matching here entirely.
data Container = Box Int | Bag Int
unBox, unBag :: Container -> Maybe Int
unBox (Box i) = Just i
unBox _ = Nothing
unBag (Bag i) = Just i
unBag _ = Nothing
The type of these functions captures the need to get the contained Int
out while switching on the structure of the Container
. This can then be used to build the functions you desire.
inSome :: (Container -> Maybe Int) -> [Container] -> Int
inSome get [] = 0
inSome get (x:ls) = fromMaybe 0 (get x) + inSome ls
inBag = inSome unBag
inBox = inSome unBox
As leftroundabout noted, the pattern of "get or fail" is (massively) generalized in the concept of the Lens
, or, in this case, the Prism
. In general, Prism
s can form a weak kind of first-class pattern, but their use here would be certain overkill.
Upvotes: 9
Reputation: 152707
You might like first-class-patterns, a package that lets you pass and munge patterns.
Upvotes: 5