Why use an effect system?

Considering how many effect systems there are in Haskell now, such a detailed comparison would rival a comparison of programming languages and their features. But a comparison could at least hint at the possibility of a more elegant solution, by exposing a recurring pattern in the majority of effect systems…

…alright, in keeping with the maxim of “not making enemies of perfect and good” (or perfect and tolerable), is there a “less inelegant” solution?


Observation - IO a is a very simple effect system:

So how did the designers of SAC manage to confine nondeterminism, and separate access to it from other I/O effects? By providing nondeterminism in the form of a type (called Nondet here) whose values could be “dismantled” into two or more parts (its subvalues). This means IO a can be defined using Nondet and a simpler monadic type M a which conveys all other I/O effects - as an approximation:

type IO a = Nondet -> M a

Alternatively, Nondet can be thought of as an effect specifier (like io was in Rust):

type IO a = ENondet -> M a

Taking this to the limit, with numeric indices instead of names:

type IO a = E0 -> ... -> E -> Mempty a

then Mempty a exists only to provide a sequential context for the orderly use of E0 to E:

newtype Mempty a = Mempty a

instance Monad Mempty where
   return x = pseq x (Mempty x)
   Mempty x >>= k = k x

Analogous to runIdentity for Identity a, it’s possible to obtain the result of a Mempty a value:

\ (Mempty x) -> x

and Mempty a can be as simple as:

type Mempty a = a

As for IO a, it can then be as simple as:

type IO a = E0 -> ... -> E -> a

Or using a tuple:

type IO a = (E0, ... E) -> a

or another abstract type:

data OI -- (E0, ... E)
partOI :: OI -> (OI, OI) -- to obtain the parts (subvalues) of an OI value

type IO a = OI -> a

main :: OI -> () {- foreign export ... "hs_rts_Main_main" main :: OI -> () -}

foreign ... actionWithEffect :: ... -> OI -> ()

As for that interface, the one that even at university level, bachelor level students often struggle to comprehend:

unitIO :: a -> IO a
unitIO x = \ u -> pseq (partOI u) x

bindIO :: IO a -> (a -> IO b) -> IO b
bindIO m k = \ u -> case partOI u of
                      (u1, u2) -> case m u1 of
                                    x -> pseq x (k x u2)

Now for an edited quote by Philip Wadler:

  1. Pass a suitable effect specifier in a existing or new parameter down to that segment.

  2. A pair of effect specifiers can be used as a composite effect specifier:

    • E(stdin, stdout ) = (Estdin, Estdout)
  3. …of type Tresult (an ordinary effect-free Haskell expression).

  4. …of type Estdout -> Tresult (an action capable of a single form of interaction).

  5. …of type:

    • Estdin -> Estdout -> ... -> Tresult

    • E(stdin, stdout, … ) -> Tresult

    (an action capable of several forms of interaction).


So there is one “less inelegant” solution to the separate use of effects (but a detailed explanation of what is needed in Haskell to improve its elegance is another topic entirely) - are there any others?