sibbl
sibbl

Reputation: 3219

Read lines of a file, sort and return middle element

I am kind of new to IO with Haskell and although I read through it a lot, my code still won't work.

What I want the app to do:

  1. Read all lines of a file (file1.txt, file2.txt, ...), where all contain numbers each line (floats like 1.12345)
  2. Sort all these lines (string sorting or float sorting doesn't matter, I assume string sorting is faster?)
  3. Get the middle element of the list and print it out

This is the code I do have so far. I can assure that the function "middle" works fine when passing a [String].

middle :: [a] -> a
middle xs = (drop ((l - 1) `div ` 2) xs) !! 0
            where l = length xs

getSortedMiddleElement :: Int -> String
getSortedMiddleElement i = do
    dat <- readFile $ "file" ++ (show i) ++ ".txt"
    return $ middle $ sort $ lines dat

I am calling getSortedMiddleElement from a "Int -> Content" function (I use Yesod), where the number is being passed via URL and the middle element should be returned to the user. To get Content out of a string, it needs to be "String", not "IO String"... How can this be easily achieved?

Thanks in advance!

Upvotes: 0

Views: 337

Answers (1)

Levi Pearson
Levi Pearson

Reputation: 4984

Your type signature says that your function is pure (i.e., it takes an Int and returns a String) but inside, you are performing IO! Haskell will not let you write such a function. Anything you read from a file is forever stuck in the IO monad, and that's that (barring unsafe functions, of course).

In this case, that turns out to not be so bad, because Yesod is a heavily IO-based framework. All network traffic is stuck in the IO monad as well!

When you're in a monad transformer stack, you have access to monadic computations at each level of the stack, but only one of them directly. You use lift to move a computation from a monad one layer down in the stack into the transformed monad. If IO is in the stack, no matter how many layers down, you can access its actions directly via liftIO.

So if you have type T = ReaderT String IO then you may have a function foo :: Int -> T String. In this function, you'll be operating in the T monad, which transforms the IO monad with the Reader monad capabilities. In this context, you can say lift readFile and instead of getting an IO String result, you'll get a T String result! That's just an IO String wrapped in the ReaderT type, though, so don't think we did anything tricky like escaping the IO monad. That might have been a bit confusing, so let's look at an example:

import Control.Monad.Reader (ReaderT)
import Control.Monad.Writer (WriterT)
import Control.Monad.Trans  (lift, liftIO)

type T = ReaderT String IO
getSortedMiddleElement :: Int -> IO String

foo :: Int -> T String
foo n = do
  str <- lift $ getSortedMiddleElement n --str holds a pure String now
  lift $ putStrLn str                    --get `putStrLn` from IO and pass the String
  return str                             --let's wrap it back in T now

But what if we're more than one layer away from IO? Let's try it out:

type W = WriterT String T -- WriterT String (ReaderT String IO)

-- This doesn't work; lift only gives you access to the next layer's actions
-- but IO is now more than one layer away!
--
--bar n = do
--  str <- lift $ getSortedMiddleElement n

-- Instead, we need liftIO, which will access IO across many transformer layers
bar :: Int -> W String
bar n = do
  str <- liftIO $ getSortedMiddleElement n
  liftIO $ putStrLn str
  return str

Upvotes: 5

Related Questions