Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Numbering output lines in Haskell

Tags:

haskell

I want to show the content of lists with arbitrary types, one element per line, numbered starting at 1 like this :

String Example:

> bs "Hallo"
1. 'H'
2. 'a'
3. 'l'
4. 'l'
5. 'o'

Integer Example

> bs [5,6,1,2]
1. 5
2. 6
3. 1
4. 2

Tuples Example

> bs [(4,"Test"),(3,"Aye"),(5,"Fives")]
1. (4,"Test")
2. (3,"Ayes")
3. (4,"Fives)

I found this to be one solution:

bs' :: Show a => [a] -> Integer -> IO ()
bs' [] _ = return ()
bs' (x:xs) y = do
  putStrLn  $ (show y) ++ ". " ++ (show x)
  bs' xs $ succ y

bs x = bs' x 1
  1. As I am absolute beginner to Haskell I wonder what is the "best" way to solve this problem? Am I on the right trail or is that just plain "bad" Haskell.

  2. How to output the Chars in the String example without the '' and still be able to output any type which has an instance of Show ?

  3. I would like to know about other ways to solve this task, from different perspectives like: readability, efficiency, code reuse.

I also did it like this and found it even stranger (but somehow cool):

bs' :: Show a => [(Integer,a)] -> IO ()
bs' [] = return ()
bs' ((x1,x2):xs) = do
  putStrLn $ (show x1) ++ ". " ++ (show x2)
  bs' xs

bs x = bs' (zip [1..] x)

I have done about 25 years of imperative programming and being really interested in learning something new. At the same time if feels incredible "strange" to code in Haskell and I still can't imagine how a big project is done with this "crazy language from the moon" :)

EDIT: I wanna thank everybody. I choose one Answer because I have to but all are very helpful! I also want to say that the top solution I had was because "in the real problem" where that came from I had to skip some list elements and the numbering got wrong when using the zip approach. After reading all the answers I am pretty sure, that even then the solution is to first filter the list and then zip map the output function.

like image 887
OderWat Avatar asked Jul 18 '14 11:07

OderWat


2 Answers

There is also Text.Printf, if you're nostalgic for printf:

import Text.Printf

bs :: Show a => [a] -> IO ()
bs = sequence_ . zipWith (\n x -> printf "%d. %s\n" n (show x)) [(1 :: Int)..]

Or if you don't want to use printf:

bs xs = sequence_ $ zipWith (\n x -> mapM_ putStr [show n, ". ", show x, "\n"]) [1..] xs

Neither of these is very idiomatic, I think most people would make a pure function that returns a string and then print that when necessary:

bs' xs = unlines $ zipWith (\n x -> show n ++ ". " ++ show x) [1..] xs

bs xs = putStr (bs' xs)
like image 64
Omar Antolín-Camarena Avatar answered Oct 13 '22 12:10

Omar Antolín-Camarena


It is good to decompose your task into small parts. In this case you want to 1) render each line by showing the element and prepending a number then 2. Print out each rendering on its own line in a terminal.

So the rendering is just some string munging:

renderLine :: Show a => Integer -> a -> String
renderLine i a = show i ++ ". " ++ show a

And the combination of many lines needs to pass in successive numbers into the render:

bs :: Show a => [a] -> String
bs = unlines . zipWith renderLine [1..]

This gives us results such as:

*Main> putStr $ bs "Hello"
1. 'H'
2. 'e'
3. 'l'
4. 'l'
5. 'o'
*Main> putStr $ bs [1,2,3,4]
1. 1
2. 2
3. 3
4. 4
*Main> putStr $ bs [(4,"Test"),(3,"Aye"),(5,"Fives")]
1. (4,"Test")
2. (3,"Aye")
3. (5,"Fives")

Questions

As I am absolute beginner to Haskell I wonder what is the "best" way to solve this problem? Am I on the right trail or is that just plain "bad" Haskell.

I'd say the best way is the one that is cleanest to read for practiced Haskell programmers, which usually means using the common functions from Prelude, such as zipWith, and avoiding manual primitive recursion when possible.

How to output the Chars in the String example without the '' and still be able to output any type which has an instance of Show ?

To perform different operations on different types with the same function you'll need a type class.

EDIT I didn't read carefully enough. I now see you wanted to make this work for anything that is an instance of Show.

There are many long answers that can (and probably will) be given here about how making Char behave one way but then effectively lift all other Show instances leaves some ambiguity which the compiler must resolve. I'll skip that and just tell you we need several extensions to the language via the {-# LANGUAGE ... #-} pragma you see below:

{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE IncoherentInstances #-}
renderLine :: BS a => Integer -> a -> String
renderLine i a = show i ++ ". " ++ r a

bs :: BS a => [a] -> String
bs = unlines . zipWith renderLine [1..]

class BS a where
    r :: a -> String

instance BS Char where
    r c = [c]

instance (Show a) => BS a where
    r = show

And in practice:

*Main> putStr $ bs [(4,"Test"),(3,"Aye"),(5,"Fives")]
1. (4,"Test")
2. (3,"Aye")
3. (5,"Fives")
*Main> putStr $ bs "Hello"
1. H
2. e
3. l
4. l
5. o
like image 21
Thomas M. DuBuisson Avatar answered Oct 13 '22 11:10

Thomas M. DuBuisson