I understand why you see a similarity between monads and the $_
variable:
- a monad encapsulates a value in some context, and allows operations to be performed on that value.
- the
$_
variable refers to the current context. Operations can use this context.
So the common part is that there is some value, some context, and some operations. Unfortunately, this could describe all programming. The $_
variable is not monad like because it does not actually have any properties of a monad. Most importantly, using the $_
variable does not compose.
One aspect of monads is that they can be defined by two operations: one to wrap a value in a context (sometimes called a constructor, or a return
operation), and one bind
operation to apply an action to a value. The action given to bind
takes an unwrapped value, and returns a monad of the expected kind again. It is really important that bind
does not return a naked value, but a monad. The monad is responsible for deciding whether, how often, and in which order the bind
action is applied to its wrapped value, if any. This flexibility allows monads to model collections with any number of elements, and also control flow constructs such as conditionals.
In Perl, lists have mostly monad-like behaviour. Their construction is implicitly tracked through list context, and their bind
operation is the map
builtin. The special context of a list is that all values in the list are ordered. Examples:
# the empty list
() #=> ()
# a list of one element
(1) #=> (1)
# a longer list
(1, 2, 3, 4) #=> (1, 2, 3, 4)
# identity
map { ($_) } (1, 2, 3) #=> (1, 2, 3)
# double each item
map { ($_, $_) } (1, 2, 3) #=> (1, 1, 2, 2, 3, 3)
We can show that this satisfies the monad laws:
the constructor is the neutral operation for bind: Given a list @list
, map { ($_) } @list
is always the same as @list
, and wrapping a value $x
in a list ($x)
and then mapping a function f
over it map { f($_) } ($x)
has the same effect as f($x)
.
binding actions satisfies a specific equivalence. For a given @list
and all (pure) functions f
and g
, these two expressions must be equivalent:
map { g($_) } map { f($_) } @list
map { map { g($_) } f($_) } @list
Now let's contrast this with operations such as s/foo/bar/
that operate on $_
by default. We clearly have a kind of constructor for $_
, e.g. for
loops place the value in $_
by default. And we have a kind of binding mechanism that allows $_
to be used by some operation. These operations take the value from $_
and usually modify it in-place. However, in-place modifications are fundamentally at odds with monadic behaviour – the bind operation expects an action that takes an unwrapped value and returns a monad. With in-place modification, we must return exactly one value. There's no wrapping/unwrapping taking place either explicitly or implicitly.
The point that the result of each computation is used by the next step may look monadic, but it's just imperative programming. Being a stateful variable is all that $_
is. Of course, you can model imperative stateful computations with a state monad, but that is more about the ;
operator than the $_
variable.
By the way, the only special aspect of $_
is that the *_
variables are super-global and always resolve to the main package. The general syntax of your example code could be mirrored with any other implicit variable, e.g. $::florp
:
sub florpyreadwhile {
my ($fh, $body) = @_;
while ($::florp = <$fh>) {
$body->();
}
}
sub florpychomp {
return chomp $_[0] if @_;
return chomp $::florp;
}
sub florpysubstitute {
my ($re, $sub) = @_;
my $str_ref = (@_ >= 3) ? \$_[2] : \$::florp;
return $$str_ref =~ s/$re/$sub/;
}
sub florpyprint {
return print @_ if @_;
return print $::florp;
}
# using florpy sugar
florpyreadwhile \*STDIN => sub {
florpychomp;
florpysubstitute qr/Hello/ => 'Goodbye';
florpyprint;
};
# without florpy sugar
florpyreadwhile \*STDIN => sub {
florpychomp $::florp;
florpysubstitute qr/Hello/ => 'Goodbye', $::florp;
florpyprint $::florp;
};