Hacker News new | ask | show | jobs
by Iceland_jack 851 days ago
IO distinguishes execution (of actions) from evaluation (of expressions).

To execute an `action :: IO Ty' for a value of `a :: Ty', you use <-

  do (a :: Ty) <- (action :: IO Ty)
     ..
The 'function' rand is not really a function, but an action. It doesn't make sense to ask which int rand() evaluates to, and we can't equationally reason about rand() as an int. We cannot factor it from `rand() + rand()', or replace it with `2 * rand()' because it is not an int! Haskell is explicit about this, `rand :: IO Int' an action that produces an Int when (effectfully) executed.

The addition of actions doesn't make sense `rand + rand': Num-eric operations do not automatically lift over IO Int.[1] Instead we explicitly write `liftA2 (+) rand rand'. Shorthand for

    do r1 <- rand
       r2 <- rand
       pure (r1 + r2)
where r{1,2} are Ints. The separation between evaluation and execution means we can factor rand out while still executing it twice.

  do let r :: IO Int
         r = rand

     r1 <- r
     r2 <- r
     pure (r1 + r2)
This factors out the 'recipe', not the value it produces. To factor out the result of the IO-action, we just use a single bind/draw <-.

  do r1 <- rand
     pure (r1 + r1)
[1] This can be changed with Applicative lifting:

  {-# Language DerivingVia #-}

  deriving via Ap IO a
    instance Num a => Num (IO a)
1 comments

See "Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell" by Simon Peyton Jones

https://www.microsoft.com/en-us/research/wp-content/uploads/...