¿Es la variable predeterminada de Perl como una mónada FP?

7

He estado pensando en las mónadas en la programación funcional y parece que hay algunos puntos en común entre la variable predeterminada de Perl $_ y las mónadas FP.

¿Es esto cierto? ¿Hay similitudes? Si no, ¿por qué no? Tal vez esto me permita desarrollar monadas de una vez por todas al comprender por qué el uso de $_ no es una mónada como:

Mi comprensión (en esta etapa vaga) de una mónada es que un aspecto es que se puede usar para encadenar comandos, de modo que la salida de uno sea mágicamente la entrada del siguiente.

Entonces, por ejemplo, este perl:

while (<STDIN>) {
   chomp;
   s/Hello/Goodbye/;
   print;
}

Toma del stdin, elimina el espacio en blanco (chomp) reemplaza "Hello" con "Goodbye" e imprime el resultado. Cada uno de esos comandos usa el valor en la variable implícita $_ y genera el resultado en ella.

El mismo código pero que muestra el uso implícito de la variable:

while ($_ = <STDIN>){
    chomp $_;
    $_ =~ s/Hello/Goodbye/;
    print $_;
}

Corríjame si me equivoco, pero se parece mucho a un aspecto de las mónadas.

    
pregunta Kevin Pluck 27.10.2015 - 15:01

2 respuestas

5

Entiendo por qué ves una similitud entre las mónadas y la variable $_ :

  • una mónada encapsula un valor en algún contexto y permite que se realicen operaciones con ese valor.
  • la variable $_ se refiere al contexto actual. Las operaciones pueden usar este contexto.

Entonces, la parte común es que hay algo de valor, algo de contexto y algunas operaciones. Desafortunadamente, esto podría describir toda la programación. La variable $_ no es una mónada porque no tiene ninguna propiedad de una mónada. Lo más importante es que el uso de la variable $_ no se compone.

Un aspecto de las mónadas es que pueden definirse mediante dos operaciones: una para envolver un valor en un contexto (a veces llamado constructor o una operación return ) y una operación bind para aplicar una acción a un valor. La acción dada a bind toma un valor no envuelto y devuelve una mónada del tipo esperado nuevamente. Es realmente importante que bind no devuelva un valor desnudo, sino una mónada. La mónada es responsable de decidir si, con qué frecuencia, y en qué orden se aplica la acción bind a su valor envuelto, si corresponde. Esta flexibilidad permite a las mónadas modelar colecciones con cualquier número de elementos, y también controlar construcciones de flujo como las condicionales.

En Perl, las listas tienen un comportamiento similar a una mónada. Su construcción se rastrea de forma implícita a través del contexto de la lista, y su operación bind es el map builtin. El contexto especial de una lista es que todos los valores de la lista están ordenados. Ejemplos:

# 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)

Podemos demostrar que esto cumple con las leyes de la mónada:

  • el constructor es la operación neutral para el enlace: dada una lista @list , map { ($_) } @list siempre es igual que @list , y envuelve un valor $x en una lista ($x) y luego se asigna una función f sobre ella map { f($_) } ($x) tiene el mismo efecto que f($x) .

  • las acciones vinculantes satisfacen una equivalencia específica. Para un @list dado y todas las funciones (puras) f y g , estas dos expresiones deben ser equivalentes:

    map { g($_) } map { f($_) } @list
    map { map { g($_) } f($_) } @list
    

Ahora contrastemos esto con operaciones como s/foo/bar/ que operan en $_ por defecto. Claramente tenemos un tipo de constructor para $_ , por ej. for loops coloca el valor en $_ por defecto. Y tenemos un tipo de mecanismo de enlace que permite que $_ sea utilizado por alguna operación. Estas operaciones toman el valor de $_ y generalmente lo modifican en el lugar. Sin embargo, las modificaciones in situ están fundamentalmente en desacuerdo con el comportamiento monádico: la operación de enlace espera una acción que toma un valor no envuelto y devuelve una mónada. Con la modificación in situ, debemos devolver exactamente un valor. No hay envoltura / desenvolvimiento que se lleve a cabo de manera explícita o implícita.

El punto en el que el siguiente paso usa el resultado de cada cálculo puede parecer monádico, pero es solo una programación imperativa. Ser una variable con estado es todo lo que $_ es. Por supuesto, puede modelar cálculos imperativos con estado con una mónada de estado, pero eso es más sobre el operador ; que la variable $_ .

Por cierto, el único aspecto especial de $_ es que las variables *_ son super-globales y siempre se resuelven al paquete principal. La sintaxis general de su código de ejemplo podría reflejarse con cualquier otra variable implícita, por ejemplo, $::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;
};
    
respondido por el amon 28.10.2015 - 00:04
2

Bueno, no conozco a Perl, pero ese código me recuerda a la mónada State . Así que aquí está mi intento de imitarlo en Haskell:

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.State
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as TextIO

main :: IO ()
main = flip evalStateT "" $ do
  stdin <- lift TextIO.getContents
  forLinesM_ stdin $ do
    modify T.strip                        -- chomp
    modify (T.replace "Hello" "Goodbye")  -- s/Hello/Goodbye/
    get >>= (lift . TextIO.putStrLn)      -- print 

-- | Auxiliary function to loop over the lines of a 'Text', putting
-- each line into the implicit state of the 'StateT' monad transformer.
forLinesM_ :: Monad m => Text -> StateT Text m b -> StateT Text m ()
forLinesM_ text body =
    forM_ (T.lines text) $ \line -> do
      put line
      body

La mónada State requiere que el tipo de estado sea uniforme en todo el cálculo, por lo que supongo que es bastante más restrictivo que la variable implícita de Perl.

Me parece que el código anterior es digno de mención. Lo que Perl hace con las variables implícitas, Haskell lo hace con la función o la composición monádica. Así que un mejor programa de Haskell para esto sería:

{-# LANGUAGE OverloadedStrings #-}

import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as TextIO

main :: IO ()
main = TextIO.interact go
  -- The '.' operator is right-to-left function composition, so 
  -- this reads from bottom to top:
  where go = 
       -- Join the lines back together
         T.unlines

       -- Replace strings in each line  
       . map (T.replace "Hello" "Goodbye")

       -- trim whitespace from each line
       . map T.strip                    

       -- Split input into lines
       . T.lines  

La composición de funciones se parece más a las tuberías de Unix que a las variables implícitas de Perl.

    
respondido por el sacundim 12.11.2015 - 02:15

Lea otras preguntas en las etiquetas