As a general rule, try to first write code as just pure functions without worrying where the data comes from - just assume it's there.
Next wrap that pure functionality in IO to feed your pure functions data and put the results somewhere.
It's OK that there's a lot of this going on in a chat application! The IO monad isn't inefficient at all,
it's just that we prefer to keep as much code as we can out of it since that's good design -
keep the data crunching apart from the IO. A chat application doesn't do a lot of calculation with the data it gets, so it's OK to have loads of IO code.
I think it's definitely better to stick in the IO monad than use unsafePerformIO, because unsafePerformIO is kind of presenting its result as pure data. I might be tempted to use it to get constants from a configuation file, but I've never actually done so, and there's no point if you're heavily in the IO monad anyway. There's a reason it's called unsafe! Petr Pudlák has good advice in the comment below.
I've heard Haskell's monads described as the best imperative programming language in the
world. I could split hairs over that description, but I agree with the sentiment, and yes,
stick with Haskell. Haskell is good at at the programming you're using it for.