turtle
turtle

Reputation: 8073

Convert nested lists to custom data type

I am trying to convert nested lists into a custom type called Mydata using a list comprehension as follows:

main = do
    let a = [["12.345", "1", "4.222111"],
             ["31.2", "12", "9.1234"],
             ["43.111111", "3", "8.13"],
             ["156.112121", "19", "99.99999"]]
    let b = foo a
    print b

foo xss = [(xs,xs) | xs <- xss, xs <- xss]
    where
         xs = Mydata (read xs!!0 :: Float) (read xs!!1 :: Int) (read xs!!2 :: Float)

data Mydata = Mydata {valA :: Float, valB :: Int, valC :: Float}

When I run my program I get the following error:

1.hs:11:28:
    Couldn't match expected type `String' with actual type `Mydata'
    In the first argument of `read', namely `xs'
    In the first argument of `(!!)', namely `read xs'
    In the first argument of `Mydata', namely `(read xs !! 0 :: Float)'

Can anyone help me figure out what the problem is? Thanks.

Upvotes: 1

Views: 508

Answers (3)

AndrewC
AndrewC

Reputation: 32455

{- I'm going to take the opportunity to tell you some other things I think will help in the long run as well as fix the problem. -}

import Control.Applicative
import Data.Maybe
import Network.CGI.Protocol (maybeRead)

{- The Control.Applicative lets me use <$> and <*> which are really handy functions for working with loads of things (more later). I'm going to use maybeRead later. I don't know why it's not in Data.Maybe.

Data structure first. I've derived show so we can print Mydata. -}

data Mydata = Mydata {valA :: Float, 
                      valB :: Int, 
                      valC :: Float}
  deriving Show

{- I've put somedata as a definition in the main body (you had let a = inside main), because I felt you were seriously overusing the IO monad. It's worth trying to do as much as possible in pure world, because it makes debugging much easier. Maybe in your actual problem, you'll have read somedata in from somewhere, but for writing the functions, having a bit of test data like this lying around is a big bonus. (Try though to only mention somedata as a definition here once, so you don't get a rash of global constants!) -}

somedata = [["12.345", "1", "4.222111"],
            ["31.2", "12", "9.1234"],
            ["43.111111", "3", "8.13"],
            ["156.112121", "19", "99.99999"]]

somewrong = [ ["1",   "2",   "3"     ],    -- OK
              ["1.0", "2",   "3.0"   ],    -- OK, same value as first one
              ["1",   "2.0", "3"     ],    -- wrong, decimal for valB
              ["",    "two",  "3.3.3"] ]   -- wrong, wrong, wrong.

{- Let's write a function to read a single Mydata, but use Maybe Mydata so we can recover gracefully if it doesn't work out. maybeRead :: Read a => String -> Maybe a, so it turns strings into Just what you want, or gives you Nothing if it can't. This is better than simply crashing with an error message. (Better still would be to return Either an error message explaining the problem or the Right answer, but I'm going to skip that for today.)

I'm going to write this three ways, getting nicer and nicer. -}

readMydata_v1 :: [String] -> Maybe Mydata
readMydata_v1 [as, bs, cs] = case (maybeRead as, maybeRead bs, maybeRead cs) of
   (Just a, Just b, Just c) -> Just $ Mydata a b c
   _                        -> Nothing
readMydata_v1 _ = Nothing    -- anything else is the wrong number of Strings

{- so we look at (maybeRead as, maybeRead bs, maybeRead cs) and if they all worked, we make a Mydata out of them, then return Just the right answer, but if something else happened, one of them was a Nothing, so we can't make a Mydata, and so we get Nothing overall.

Try it out in gchi with map readMydata_v1 somedata and map readMydata_v1 somewrong.

Notice how because I used the expression Mydata a b c, it forces the types of a, b and c to be Float, Int and Float in the (Just a, Just b, Just c) pattern. That pattern is the output of (maybeRead as, maybeRead bs, maybeRead cs), which forces the types of the three uses of maybeRead to be right - I don't need to give individual type signatures. Type signatures are really handy, but they're not pretty in the middle of a function.

Now I like using Maybe, but I don't like writing loads of case statements inside each other, so I could use the face that Maybe is a Monad. See Learn You a Haskell for Great Good http://learnyouahaskell.com for more details about monads, but for my purposes here, it's like I can treat a Maybe value like it's IO even though it's not. -}

readMydata_v2 :: [String] -> Maybe Mydata
readMydata_v2 [as,bs,cs] = do
      a <- maybeRead as
      b <- maybeRead bs
      c <- maybeRead cs
      return $ Mydata a b c
readMydata_v2 _ = Nothing    -- anything else is the wrong number of Strings

{- I seem to write no error handling code! Aren't Maybe monads great! Here we take whatever a we can get out of maybeRead as, whatever b we can get from reading bs and whatever c we get from cs, and if that has all worked we get Just $ Mydata a b c. The Maybe monad deals with any Nothings we get by stopping and returning Nothing, and wraps any correct answer in Just.

Whilst this is really nice, it doesn't feel very functional programming, so let's go the whole hog and make it Applicative. You should read about Applicative in http://learnyouahaskell.com, but for now, lets just use it.

Whenever you find yourself writing

foo x y z = do
   thing1 <- something x
   thing2 <- somethingelse y
   thing3 <- anotherthing x z
   thing4 <- yetmore y y z
   return $ somefunction thing1 thing2 thing3 thing4

it means you're using a monad when you could more cleanly use an "applicative functor". All this means in practice is that you could have written that

foo x y z = somefunction  <$>  something x  <*>  somethingelse y  <*>  anotherthing x z  <*>  yetmore y y z

or if you prefer,

foo x y z = somefunction  <$>  something x  
                          <*>  somethingelse y  
                          <*>  anotherthing x z  
                          <*>  yetmore y y z

this is nicer because (a) it feels more like ordinary function application (notice that <$> works like $ and <*> works like a space ) and (b) you don't need to invent names thing1 etc.

It means find out the results of something x and somethingelse y and anotherthing x z and yetmore y y z then apply somefunction to the result.

Let's do readMydata the applicative way: -}

readMydata_nice :: [String] -> Maybe Mydata
readMydata_nice [a,b,c] = Mydata <$> maybeRead a <*> maybeRead b <*> maybeRead c
readMydata_nice _       = Nothing

{- Aaaahhhhh, so clean, so functional, so easy. Mmmmm. :) Think more, write less.

This means take the resuts of maybeRead a and maybeRead b and maybeRead c and apply Mydata to the result, but because everything is Maybe, if anything along the way is Nothing, the answer will be Nothing.

Again, you can test this out in ghci with map readMydata_nice somedata or map readMydata_nice somewrong

Anyway, lets write main, which is now more functional too. -}

main = mapM_ print $ catMaybes $ map readMydata_nice somedata

{- This takes each list of Strings in somedata and reads them as Maybe Mydata, then throws away the Nothings and turns them into IO print commands and does them one after another. mapM_ works a bit like map but does every IO it creates. Because it's several prints, each one goes on a seperate line, which is much easier to read.

Here I've decided to use catMaybes to ignore the Nothing values and just print the ones that worked. In a real program, I'd use Either like I said, so that I can pass an error message instead of silently ignoring wrong data. All the tricks we used on Maybe also work on Either. -}

Upvotes: 3

rkhayrov
rkhayrov

Reputation: 10260

xs definition in list comprehension is (perhaps unintentionally) recursive, and makes no sense. Possible implementation follows:

data Mydata = Mydata {valA :: Float, valB :: Int, valC :: Float} deriving Show

-- rely on type inference instead of specifying explicit type for each `read'
dataFromList [a, b, c] = Mydata (read a) (read b) (read c)
dataFromList _ = error "dataFromList: not enough arguments in list"

main = do
    let a = [["12.345", "1", "4.222111"],
             ["31.2", "12", "9.1234"],
             ["43.111111", "3", "8.13"],
             ["156.112121", "19", "99.99999"]]
    let b = map dataFromList a
    -- alternatively
    -- let b = [dataFromList triple | triple <- a]
    print b

Upvotes: 3

Jan Christiansen
Jan Christiansen

Reputation: 3173

In the definition

where
  xs = Mydata (read xs!!0 :: Float) (read xs!!1 :: Int) (read xs!!2 :: Float)

you are using xs on the left and the right-hand side of the definition. Therefore, it has to have the same type on both sides. The compiler assumes that the type of xs is MyData and you cannot apply read to a value of that type.

Upvotes: 1

Related Questions