After reading some very basic haskell now I know how to \"chain\" monadic actions using bind, like:
echo = getLine >>= putStrLn
This typechecks:
import System.IO
filepath :: IO FilePath
filepath = undefined
someString :: IO String
someString = undefined
testfun = filepath   >>= (\fp -> 
          someString >>= (\str -> 
          writeFile fp str  ))
But I feel using do notation is more readable.
TL;DR:
writeFile <$> getFilename <*> getString >>= id   :: IO ()
Since ghc 7.10 every Monad (including IO) is also an Applicative, but even before that, you could make an Applicative out of any Monad using the equivalent of
import Control.Applicative -- not needed for ghc >= 7.10
instance Applicative M where
  pure x = return x
  mf <*> mx = do
    f <- mf
    x <- mx
    return (f x)
And of course IO is a functor, but Control.Applicative gives you <$> which can be defined as f <$> mx = fmap f mx.
<$> and <*> let you use pure functions f over arguments produced by an Applicative/Monadic computation, so if f :: String -> String -> Bool and getFileName, getString :: IO String then 
f <$> getFileName <*> getString :: IO Bool
Similarly, if g :: String -> String -> String -> Int, then 
g <$> getString <*> getString <*> getString :: IO Int
IO (IO ()) to IO ()That means that
writeFile <$> getFilename <*> getString :: IO (IO ())
but you need something of type IO (), not IO (IO ()), so we need to either use join :: Monad m => m (m a) -> m a as in Xeo's comment, or we need a function to take the monadic result and run it, i.e. of type (IO ()) -> IO () to bind it with. That would be id then, so we can either do
join $ writeFile <$> getFilename <*> getString :: IO ()
or
writeFile <$> getFilename <*> getString >>= id :: IO ()
It's much easier to use do notation for this, rather than asking for a combinator
action1 :: MyMonad a
action2 :: MyMonad b
f :: a -> b -> MyMonad c
do
    x <- action1
    y <- action2
    f x y