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:
-
Pass a suitable effect specifier in a existing or new parameter down to that segment.
-
A pair of effect specifiers can be used as a composite effect specifier:
- E(stdin, stdout ) =
(Estdin,Estdout)
- E(stdin, stdout ) =
-
…of type
Tresult (an ordinary effect-free Haskell expression). -
…of type Estdout
-> Tresult (an action capable of a single form of interaction). -
…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?