4

For didactical purposes I am trying to follow and implement IO inside.

The idea is to express a type MIO a ("my IO") which is a RW -> (a, RW).

RW is the real world and, for the sake of simplicity, it's just an Integer; I cannot use the real RealWorld because there are no constructors around.

The first lines I've come up with are:

type RW = Integer

putString :: String -> RW -> ((), RW)
putString str world = (unsafePerformIO $ putStrLn str, world + 1)

getString :: RW -> (String, RW)
getString world = let input = unsafePerformIO getLine
  in (input, world + 1)

After that I try to define several ways to interact with the user, like asking one or more questions. The whole code is here.

Already starting from getString I get a very funny output:

*Main> getString 0
("This is my input
This is my input",1)
*Main>

I see the result of getString is correct, but the screen gets messy. Asking questions makes it even messier.

In the case of getString I wish the initial (" came together with the final result. Is it possible in ghci? (I doubt it, but I ask anyhow)

When I studied these same matters in JavaScript, things were easier because I could get popups for free from the browser. Is there some Haskell-ready environment where I could plug in my code?

I know that by using unsafePerformIO I have damned my soul to eternal sufferings, but reminding me that is gonna be beside my point.

Marco Faustinelli
  • 3,734
  • 5
  • 30
  • 49
  • 4
    Well one of the main reasons why `unsafePerformIO` is, well..., unsafe, is exactly because the order of evaluation is quite unpredictable. Haskell's laziness, will likely indeed first print the `("`, and then query for input. The main reason why there is an `IO` monad, is to guarantee evaluation order of `IO` operations. It is thus possible that `unsafePerformIO`s are never evaluated, evaluated multiple times, and in a different order. – Willem Van Onsem Oct 12 '19 at 14:59
  • 3
    Haskell is designed to prevent you from doing actual IO unless using the IO monad. You could instead "simulate" IO by reading/writing from your RW type. Instead of letting it be a single integer, make it a `([String], [String])` the first list of strings being the strings to be read as input in the future, and the latter being the strings which were printed in the past. When you `putString` you append to the second list, when you `getString` you remove a string form the first list (if empty, return "EOF" or something). – chi Oct 12 '19 at 15:48
  • @WillemVanOnsem - returning a modified world every time and making sure the last world is in the final result is my safeguard that all actions are performed. The fact that they are done in order is another story. So far, so good; but I am on the lookout for trouble, as I proceed. Could you see a way to make my sequences clearer to the compiler? I was thinking to try to define my own `bind`, but I wonder whether it will help... – Marco Faustinelli Oct 12 '19 at 15:51
  • @chi - your suggestion is interesting. I'll follow it. Thank you! – Marco Faustinelli Oct 12 '19 at 15:53
  • 1
    [Possibly related answer](https://stackoverflow.com/a/57752935/3234959) which also shows a "pure implementation of IO" (kind of). – chi Oct 12 '19 at 17:09
  • Related: https://stackoverflow.com/questions/10447914/io-implementation-inside-haskell . – atravers Oct 17 '20 at 03:33

1 Answers1

3

Your problem is easily solved with some seq.

getString :: RW -> (String, RW)
getString world =
  let input = unsafePerformIO getLine
  in input `seq` (input, world + 1)

Behold:

λ getString 0
Hello!
("Hello!",1)

How cool is that?

But I want to also nudge you a bit beyond what you are asking: can you make your tuple a monad? (Hint: you will need to modify it in some way.) Then you could use do notation and your RW IO will look just like the real thing!

 

P.S. As @chi points out in a comment, you should rather prefer pseq when you wish to ensure order. There is a veritable wall written about it on Haskell Wiki.

Ignat Insarov
  • 4,660
  • 18
  • 37
  • 1
    This should indeed "work" in practice, but let me add that `seq` does not guarantee ordering. The compiler is entitled to rewrite ``x `seq` y `seq` z`` as ``y `seq` x `seq` z``, in theory (I don't know if this actually happens, though). Instead, `pseq` guarantees ordering. (Even with that, it's hard to reason about `unsafe` primitives, so they should be avoided in general, as the OP already understands.) – chi Oct 12 '19 at 17:08