Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tuple initialization from IO data in Haskell

I would like to know what is the best way to get a tuple from data read from the input in Haskell. I often encounter this problem in competitive programming when the input is made up of several lines that contain space-separated integers. Here is an example:

1 3 10
2 5 8
10 11 0
0 0 0

To read lines of integers, I use the following function:

readInts :: IO [Int]
readInts = fmap (map read . words) getLine

Then, I transform these lists into tuples with of the appropriate size:

readInts :: IO (Int, Int, Int, Int)
readInts = fmap ((\l -> (l !! 0, l !! 1, l !! 2, l !! 3)) . map read . words) getLine

This approach does not seem very idiomatic to me.

The following syntax is more readable but it only works for 2-tuples:

readInts :: IO (Int, Int)
readInts = fmap ((\[x, y] -> (x, y)) . map read . words) getLine

(EDIT: as noted in the comments, the solution above works for n-tuples in general).

Is there an idiomatic way to initialize tuples from lists of integers without having to use !! in Haskell? Alternatively, is there a different approach to processing this type of input?

like image 648
Eneelk Avatar asked Jun 26 '26 02:06

Eneelk


2 Answers

How about this:

readInts :: IO (<any tuple you like>)
readInts = read . ("(" ++) . (++ ")") . intercalate "," . words <$> getLine
like image 162
luqui Avatar answered Jun 27 '26 19:06

luqui


Given that the context is 'competitive programming' (something I'm only dimly aware of as a concept), I'm not sure that the following offers a particularly competitive alternative, but IMHO I'd consider it idiomatic to use one of several available parser combinators.

The base package comes with a module called Text.ParserCombinators.ReadP. Here's how you could use it to parse the input file from the linked article:

module Q57693986 where

import Text.ParserCombinators.ReadP

parseNumber :: ReadP Integer
parseNumber = read <$> munch1 (`elem` ['0'..'9'])

parseTriple :: ReadP (Integer, Integer, Integer)
parseTriple =
  (,,) <$> parseNumber <*> (char ' ' *> parseNumber) <*> (char ' ' *> parseNumber)

parseLine :: ReadS (Integer, Integer, Integer)
parseLine = readP_to_S (parseTriple <* eof)

parseInput :: String -> [(Integer, Integer, Integer)]
parseInput = concatMap (fmap fst . filter (null . snd)) . fmap parseLine . lines

You can use the parseInput against this input file:

1 3 10
2 5 8
10 11 0
0 0 0

Here's a GHCi session that parses that file:

*Q57693986> parseInput <$> readFile "57693986.txt"
[(1,3,10),(2,5,8),(10,11,0),(0,0,0)]

Each parseLine function produces a list of tuples that match the parser; e.g.:

*Q57693986> parseLine "11 32 923"
[((11,32,923),"")]

The second element of the tuple is any remaining String still waiting to be parsed. In the above example, parseLine has completely consumed the line, which is what I'd expect for well-formed input, so the remaining String is empty.

The parser returns a list of alternatives if there's more than one way the input could be consumed by the parser, but again, in the above example, there's only one suggested alternative, as the line has been fully consumed.

The parseInput function throws away any tuple that hasn't been fully consumed, and then picks only the first element of any remaining tuples.

This approach has often served me with puzzles such as Advent of Code, where the input files tend to be well-formed.

like image 39
Mark Seemann Avatar answered Jun 27 '26 19:06

Mark Seemann



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!