You have to make the function into an IO action. Haskell strictly enforces that you separate code that does IO from code that doesn't:
foo :: Int -> IO Int
foo i = do
putStrLn "Foo Function has run!"
return (i * 2)
You could then extract this value inside another IO function:
main :: IO ()
main = do
bar1 <- foo 6
bar2 <- foo bar1
print (bar1, bar2)
The reasons why Haskell chooses this route are many and varied, but some of the important ones (in no particular order) are:
- Testability. Any pure function is much easier to test because it always only depends on its inputs, not on the state of the application
- Equational reasoning. This means that you can reason about your code as if it were just fancy mathematical functions. You can shuffle around your code much easier.
- Compiler optimizations. The compiler is free to choose when and how to call your function if it has no side effects, it can defer the call until later, and it can make very tight inner loops in the compiled code that run much faster.
- Composability. Haskell type system is very rich, and segregating types in this fashion actually allows you to write more abstractions than you might be able to in C#, Java, or Python.
- Code clarity. If you see a function just returns a value and not an IO action, you immediately know a lot about that function. That type signature is a promise that you don't have to worry as much that the function might break your application. On the other hand if it does return an IO action, this signifies that you have to be more careful.