Home > Software design >  how to filter sum types in Haskell
how to filter sum types in Haskell

Time:10-13

for example

data CampingStuff = Apple String Int Bool 
                  | Banana String Int 
                  | Pineapple String String
                  | Table String Bool Int
                  | Chairs String Int Int Int 

I want to have a query function

pickStuff :: [CampingStuff] ->  ??? -> [CampingStuff]

the ??? I want to pass Apple then the pickStuff is going to filter out all stuffs like

Apple "Jane" ...
Apple "Jack" ...
Apple "Lucy" ...

something I can think of is like

pickStuffs stuffs dummyStuff
  = filter 
      (\x ->
        (x == dummyStuff)
      stuffs

pickStuffs stuffs (Apple "" 0 False)

instance Eq CampingStuff where
  compare (Apple name1 number1 b1) (Apple name2 number2 b2)
     = True

the drawback of it is :

  • passing extra parameters to dummy value is not elegant and is not making any sense "" 0 False
  • it has to implement all the value constructor in Eq type class ( Apple, Table , Chair)
  • it is not scalable as in the future I would like to filter out all the apples from Janes
    • like this (Apple "Jane" _ _)

Thank you for reading this and appreciate any help how to filter on this [CampingStuff] by Data Constructor like Apple/Table ?

CodePudding user response:

The problem is that unsaturated constructors can't really be compared by value. The only things you can do with them are invoke them or pattern match on them. So if you want a function that tests for Apple, it'll have to be totally different from a function that tests for Banana - they can't share any code, because they have to compare against a different set of patterns.

This is all much easier if you refactor your type to remove the obvious duplication, leaving you with saturated value constructors. The generated Eq instance is all you'll need for comparing types:

data StuffType = Apple | Banana | Pineapple | Table | Chairs deriving Eq
data CampingStuff = Stuff { stuffType :: StuffType
                          , owner :: String
                          , quantity :: Int
                          }

Then you can easily write a function of type CampingStuff -> Bool by composing a couple functions.

hasType :: StuffType -> CampingStuff -> Bool
hasType t s = stuffType s == t

and use that to filter a list:

pickStuff :: StuffType -> [CampingStuff] -> [CampingStuff]
pickStuff = filter . hasType

In the comments, you ask: What if my constructors weren't all uniform, so I couldn't extract everything out to a product type with an enum in it?

I argue that, in such a case, you won't be happy with the result of a pickStuff function no matter how it's implemented. Let's imagine a simpler type:

data Color = Red | Green
data Light = Off | On Color

Now, you might wish to filter a [Light] such that it includes only lights that are On, regardless of their color. Fine, we can implement that. We won't even worry about generalizing, because the type is so small:

ons :: [Light] -> [Light]
ons = filter on
  where on Off = False
        on (On _) = True

Now you have lights :: [Light], and you can get onLights = ons lights :: [Light]. Amazing. What will you do with onLights next? Perhaps you want to count how many of each color there are:

import qualified Data.Map as M

colorCounts :: [Light] -> M.Map Color Int
colorCounts = M.fromListWith ( ) . map getColor
  where getColor (On c) = (c, 1)

colorCounts has a problem: it assumes all the lights are On, but there's no guarantee of that in the type system. So you can accidentally call colorCounts ls instead of colorCounts (ons ls), and it will compile, but give you an error at runtime.

Better would be to just do your pattern matching at the point when you'll know what to do with the results. Here, that's inside of colorCounts: just add a case for Off, and use mapMaybe instead of map so you have a chance to throw away values you don't like:

colorCounts' :: [Light] -> M.Map Color Int
colorCounts' = M.fromListWith ( ) . mapMabye getColor
  where getColor (On c) = Just (c, 1)
        getColor Off = Nothing

The same arguments all hold for more complicated types: don't pattern match on a value until you're ready to handle all the information you might find.

Of course, one way to handle such information is to put it into a new type that contains only the information you want. So you could very well write a function

colorsOfOnLights :: [Light] -> [Color]
colorsOfOnLights = mapMaybe go
  where go Off = Nothing
        go (On c) = Just c

This way, you can't possibly mix up the input of the "filter" function with the output: the output is clearly divorced from the original Light type, and its values can only have come from the On lights. You can do the same thing for your CampingStuff type by extracting a new product type for each of the constructors:

data CampingStuff = Apple AppleData
                  | Banana BananaData
                  -- ...

data AppleData = AppleData String Int Bool
data BananaData = BananaData String Int
-- ...

asApple :: CampingStuff -> Maybe AppleData
asApple (Apple ad) = Just ad
asApple _ = Nothing

apples :: [CampingStuff] -> [AppleData]
apples = mapMaybe asApple

You'll need separate functions for asApple and for asBanana and so on. This seems cumbersome, and I don't exactly disagree, but in practice people don't really need large numbers of functions like this. It's usually better to do as I described before: delay the pattern match until you know what to do with the results.

CodePudding user response:

For the function you want to have, you could create functions such as

isApple :: CampingStuff -> Bool
isApple Apple{} = True
isApple _ = False

and then use filter isApple. When you want to filter by Jane, you add another 5 functions for each type, like isAppleFrom :: String -> CampingStuff -> Bool and do filter (isAppleFrom "Jane").

Another approach is the following:

data StuffType = AppleT | BananaT | PineappleT | TableT | ChairsT deriving Eq
data Query = ByStuff StuffType | ByName String deriving Eq

pickStuff :: [CampingStuff] -> [Query] -> [CampingStuff]
pickStuff xs qs = filter cond xs
  where
    cond :: CampingStuff -> Bool
    cond x = all (\q -> case (q, x) of
      (ByStuff AppleT, Apple{}) -> True
      ...other pairs...
      (ByName name1, Apple name2 _) -> name1 == name2
      ...
      _ -> False) qs

That is, separate querying from the data types. The above is an example and may be written better.

  • Related