← Back to context

Comment by sparkie

3 days ago

To retain referential transparency, we basically need to ensure that a function provided the same arguments always returns the same result.

A simple way around this is to never give the same value to a function twice - ie, using uniqueness types, which is the approach taken by Clean. A uniqueness type, by definition, can never be used more than once, so functions which take a uniqueness type as an argument are referentially transparent.

In Haskell, you never directly call a function with side effects - you only ever bind it to `main`.

Functions with (global) side effects return a value of type `IO a`, and the behavior of IO is fully encapsulated by the monadic operations.

    instance Monad IO where
        return :: a -> IO a
        (>>=) :: IO a -> (a -> IO b) -> IO b    -- aka "bind"

return lifts a pure value into IO, and bind sequences IO operations. Importantly, there cannot exist any function of type `IO a -> a` which escapes IO, as this would violate referential transparency. Since every effect must return IO, and the only thing we can do with the IO is bind it, the eventual result of running the program must be an IO value, hence `main` returns a value of type `IO ()`.

    main :: IO ()

So bind encapsulates side effects, effectively using a strategy similar to Clean, where each `IO` is a synonym of some `State# RealWorld -> (# State# RealWorld, a #)`. Bind takes a value of IO as it's first argument, consumes the input `State# RealWorld` value and extracts a value of type `a` - feeds this value the next function in the sequence of binds, returning a new value of type `IO b`, which has a new `State# RealWorld`. Since `bind` enforces a linear sequencing of operations, this has the effect that each `RealWorld` is basically a unique value never used more than once - even though uniqueness types themselves are absent from Haskell.