HalogenM is already stack safe since it is implemented with Free (which interprets in a stack safe loop), but Effect isn’t. It’s not necessary to use MonadRec with HalogenM, but an instance is provided for completeness.
I forgot to finish and send this message I drafted:
The main reason why HalogenM doesn’t do anything special for tail recursion is that it’s built on an optimized free monad (Control.Monad.Free - purescript-free - Pursuit), which represents the structure of monadic binds without evaluating/unnesting them. This pushes the responsibility for being stack safe onto the interpreter for the free monad. In Halogen’s case, it is always interpreted into Aff. And Aff is stack-safe by default, since its implementation uses a trampoline to execute effects.
For Effect the story is different: it is compiled to plain functions, and there’s no interpretation step. What you see in the MonadRec instance is what you would write out if you were compiling a tail recursive function to JavaScript. Indeed, normal tail recursive functions are optimized like that in PureScript, and functions in Effect also are, but only if they are monomorphic (not like the polymorphic forall m. MonadEffect m => m Unit). An example of the compiled JS:
var test = function ($copy_ref) {
return function ($copy_a) {
return function __do() {
var $tco_var_ref = $copy_ref;
var $tco_var_a = $copy_a;
var $tco_done = false;
var $tco_result;
function $tco_loop(ref, a) {
var b = liftEffect(Effect_Ref.read(ref))();
var $5 = a < b;
if ($5) {
$tco_done = true;
return a;
};
$tco_var_ref = ref;
$tco_var_a = a - 1 | 0;
return;
};
while (!$tco_done) {
$tco_result = $tco_loop($tco_var_ref, $tco_var_a);
};
return $tco_result;
};
};
};