Skuge
Skuge

Reputation: 1010

In Haskell how to pass a constructor as a parameter and match over it

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

Answers (5)

LiamGoodacre
LiamGoodacre

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

thor
thor

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

J. Abrahamson
J. Abrahamson

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, Prisms can form a weak kind of first-class pattern, but their use here would be certain overkill.

Upvotes: 9

Daniel Wagner
Daniel Wagner

Reputation: 152707

You might like first-class-patterns, a package that lets you pass and munge patterns.

Upvotes: 5

leftaroundabout
leftaroundabout

Reputation: 120711

You want a lensׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅׅ

Upvotes: 2

Related Questions