Best way to filter and extract from a list of custom type?

Viewed 139

I have a data type that represents command-line options:

data Flag = Verbose | Help | Buffer Int deriving (Show, Eq)

When the program runs, I get a list of Flag, corresponding to the options the user specified. For example, [Buffer 10, Verbose].

My question is, what's the best way to extract the Int value from a Buffer in [Flag]?
The list may not even have a Buffer.
All I'm coming up with is some sort of convoluted traversal/fold that uses a case statement to filter out Buffer from the other Flags.

2 Answers

As a starting point, consider this function:

buffers :: [Flag] -> [Int]
buffers xs = [b | Buffer b <- xs]

It returns a list of just the Ints from inside of the Buffers. For [Verbose, Help], it will return []. For [Buffer 10, Verbose], it will return [10]. For [Buffer 123, Buffer 456], it will return [123, 456].

Now you have a [Flag] -> [Int]. If you also come up with an [Int] -> Int, you can compose them to get the [Flag] -> Int that you originally asked for, so now it's up to you how you get a single integer out of that list.

listToMaybe is one approach. It will return Nothing if the list is empty, or Just the first element otherwise. Compose it with fromMaybe to set a default buffer size if none is provided.

Alternatively, you can combine multiple buffer arguments somehow. max (0:buffers xs) will return the largest buffer size that was specified, or 0 if none were. (Without the 0:, your program would crash if no buffer sizes were specified.) sum (buffers xs) will return the sum of all of the buffer sizes that were specified (and automatically returns 0 if none were, since the empty sum is 0).

There's a very simple solution to this.

First, implement a type to represent a complete description of your program's behavior:

data Mode = NormalMode { verbose :: Bool, buffer :: Int } | HelpMode

Then, implement an update function to update the arguments based on one flag and a default argument array:

defaults :: Mode
defaults = NormalMode { verbose = False, buffer = defaultBuffer }
 -- assuming defaultBuffer is defined somewhere

update :: Flag -> Mode -> Mode
update Help _ = HelpMode
update _ HelpMode = HelpMode
update Verbose NormalMode { buffer = b } = NormalMode { verbose = True, buffer = b }
update (Buffer n) NormalMode { verbose = v } = NormalMode { verbose = v, buffer = n }

Then, to parse a list of Flags, just use foldr or foldl:

parseFlagsR :: [Flag] -> Mode
parseFlagsR = foldr update defaults
 -- This prioritizes the leftmost Buffer flag

parseFlagsL :: [Flag] -> Mode
parseFlagsL = foldl (flip update) defaults
 -- This prioritizes the rightmost Buffer flag

You could then get the buffer in any of the following ways, though I recommend the last one:

safeGetBuffer :: Mode -> Maybe Int
safeGetBuffer NormalMode { buffer = b } = Just b
safeGetBuffer _ = Nothing

getBufferWith :: Int -> Mode -> Int
getBufferWith _ NormalMode { buffer = b } = b
getBufferWith n _ = n

getBuffer :: Mode -> Int
getBuffer = getBufferWith defaultBuffer
 -- defaultBuffer from earlier, again, assuming it's defined.

This solution is more general than Joseph Sible's answer, and might be useful when it comes to extracting the verbosity value. This also explicitly formulates the precedence of flags and conveniently bundles all updating operations into one.

Additionally, if you want to know more about general implementations of this, I suggest you familiarize yourself with Lenses and other Optics.

Related