¿Capturar / lanzar excepciones hace que un método puro sea impuro?

27

Los siguientes ejemplos de código proporcionan contexto a mi pregunta.

La clase Room se inicializa con un delegado. En la primera implementación de la clase Room, no hay guardias contra los delegados que arrojan excepciones. Dichas excepciones aparecerán en la propiedad Norte, donde se evalúa al delegado (nota: el método Main () demuestra cómo se usa una instancia de Room en el código del cliente):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Siendo que prefiero fallar en la creación de objetos en lugar de leer la propiedad North, cambio el constructor a privado e introduzco un método de fábrica estático llamado Create (). Este método detecta la excepción lanzada por el delegado y genera una excepción de contenedor, que tiene un mensaje de excepción significativo:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

¿El bloque try-catch hace que el método Create () sea impuro?

Rock Anthony Johnson
fuente
1
¿Cuál es el beneficio de la función Room Create? Si está utilizando un delegado, ¿por qué llamarlo antes de que el cliente llame al 'Norte'?
SH-
1
@SH Si el delegado lanza una excepción, quiero averiguarlo al crear el objeto Room. No quiero que el cliente encuentre la excepción en el uso, sino en la creación. El método Crear es el lugar perfecto para exponer delegados malvados.
Rock Anthony Johnson
3
@RockAnthonyJohnson, sí. ¿Qué hace cuando ejecuta al delegado solo puede funcionar la primera vez o devolver una habitación diferente en la segunda llamada? Llamar al delegado puede considerarse / causar un efecto secundario en sí mismo?
SH-
44
Una función que llama a una función impura es una función impura, por lo que si el delegado es impuro Createtambién es impuro, porque lo llama.
Idan Arye
2
Su Createfunción no lo protege de obtener una excepción al obtener la propiedad. Si su delegado lanza, en la vida real es muy probable que lo haga solo bajo ciertas condiciones. Lo más probable es que las condiciones para el lanzamiento no estén presentes durante la construcción, pero están presentes al obtener la propiedad.
Bart van Ingen Schenau

Respuestas:

26

Sí. Esa es efectivamente una función impura. Crea un efecto secundario: la ejecución del programa continúa en otro lugar que no sea el lugar al que se espera que regrese la función.

Para que sea una función pura, returnun objeto real que encapsula el valor esperado de la función y un valor que indica una posible condición de error, como un Maybeobjeto o un objeto de Unidad de trabajo.

Robert Harvey
fuente
16
Recuerde, "puro" es solo una definición. Si necesita una función que arroje, pero de cualquier otra manera es referencialmente transparente, entonces eso es lo que escribe.
Robert Harvey
1
En haskell, al menos cualquier función puede lanzar, pero debes estar en IO para atrapar, una función pura que a veces se llama normalmente se llama parcial
jk.
44
Tuve una epifanía por tu comentario. Si bien estoy intelectualmente intrigado por la pureza, lo que realmente necesito en la práctica es el determinismo y la transparencia reverencial. Si logro la pureza, eso es solo una ventaja adicional. Gracias. Para su información, lo que me ha llevado por este camino es el hecho de que Microsoft IntelliTest es efectivo solo contra el código que es determinista.
Rock Anthony Johnson
44
@RockAnthonyJohnson: Bueno, todo el código debe ser determinista, o al menos tan determinista como sea posible. La transparencia referencial es solo una forma de obtener ese determinismo. Algunas técnicas, como los hilos, tienden a trabajar en contra de ese determinismo, por lo que hay otras técnicas como las tareas que mejoran las tendencias no deterministas del modelo de hilos. Cosas como los generadores de números aleatorios y los simuladores de Monte-Carlo también son deterministas, pero de una manera diferente en función de las probabilidades.
Robert Harvey
3
@zzzzBov: No considero que la definición real y estricta de "puro" sea tan importante. Vea el segundo comentario, arriba.
Robert Harvey
12

Pues sí ... y no.

Una función pura debe tener Transparencia referencial , es decir, debe poder reemplazar cualquier posible llamada a una función pura con el valor devuelto, sin cambiar el comportamiento del programa. * Se garantiza que su función siempre arrojará ciertos argumentos, por lo que no hay un valor de retorno para reemplazar la llamada a la función, por lo tanto, ignórelo . Considera este código:

{
    var ignoreThis = func(arg);
}

Si funces puro, un optimizador podría decidir que func(arg)podría reemplazarse con su resultado. Todavía no sabe cuál es el resultado, pero puede decir que no se está utilizando, por lo que puede deducir que esta declaración no tiene ningún efecto y eliminarla.

Pero si func(arg)sucede que arroja, esta afirmación hace algo: ¡arroja una excepción! Por lo tanto, el optimizador no puede eliminarlo; importa si se llama a la función o no.

Pero...

En la práctica, esto importa muy poco. Una excepción, al menos en C #, es algo excepcional. Se supone que no debe usarlo como parte de su flujo de control regular; se supone que debe intentar atraparlo y, si atrapa algo, maneje el error para revertir lo que estaba haciendo o para lograrlo de alguna manera. Si su programa no funciona correctamente porque un código que hubiera fallado se optimizó, está utilizando excepciones incorrectas (a menos que sea un código de prueba, y cuando construye para las pruebas, las excepciones no deberían optimizarse).

Habiendo dicho eso...

No arroje excepciones de funciones puras con la intención de que sean atrapadas: hay una buena razón por la cual los lenguajes funcionales prefieren usar mónadas en lugar de excepciones de desenrollado de pila.

Si C # tuviera una Errorclase como Java (y muchos otros lenguajes), habría sugerido lanzar un en Errorlugar de un Exception. Indica que el usuario de la función hizo algo mal (pasó una función que arroja), y tales cosas están permitidas en funciones puras. Pero C # no tiene una Errorclase, y las excepciones de error de uso parecen derivar de Exception. Mi sugerencia es lanzar un ArgumentException, dejando en claro que la función se llamó con un mal argumento.


* Computacionalmente hablando. Una función de Fibonacci implementada utilizando la recursión ingenua tomará mucho tiempo para un gran número y puede agotar los recursos de la máquina, pero esto ya que con tiempo y memoria ilimitados, la función siempre devolverá el mismo valor y no tendrá efectos secundarios (aparte de la asignación memoria y alterar esa memoria) - todavía se considera puro.

Idan Arye
fuente
1
+1 Por su uso sugerido de ArgumentException, y por su recomendación de no "lanzar excepciones de funciones puras con la intención de que sean atrapadas".
Rock Anthony Johnson
Su primer argumento (hasta el "Pero ...") me parece poco sólido. La excepción es parte del contrato de la función. Conceptualmente, puede reescribir cualquier función como (value, exception) = func(arg)y eliminar la posibilidad de lanzar una excepción (en algunos idiomas y para algunos tipos de excepciones, realmente necesita enumerarla con la definición, igual que los argumentos o el valor de retorno). Ahora sería tan puro como antes (siempre que siempre devuelva aka lanza una excepción dado el mismo argumento). Lanzar una excepción no es un efecto secundario, si usa esta interpretación.
AnoE
@AnoE Si hubiera escrito de funcesa manera, el bloque que he dado podría haberse optimizado, porque en lugar de desenrollar la pila, la excepción lanzada se habría codificado ignoreThis, lo que ignoramos. A diferencia de las excepciones que desenrollan la pila, las excepciones codificadas en el tipo de retorno no violan la pureza, y es por eso que muchos lenguajes funcionales prefieren usarlas.
Idan Arye
Exactamente ... no hubieras ignorado la excepción para esta transformación imaginada. Supongo que todo es una cuestión de semántica / definición, solo quería señalar que la existencia u ocurrencia de excepciones no necesariamente invalida todas las nociones de pureza.
AnoE
1
@AnoE Pero el código que he writted sería ignorar la excepción para esta transformación imaginado. func(arg)devolvería la tupla (<junk>, TheException()), por ignoreThislo que contendrá la tupla (<junk>, TheException()), pero dado que nunca usamos ignoreThisla excepción no tendrá ningún efecto en nada.
Idan Arye
1

Una consideración es que el bloque try - catch no es el problema. (Basado en mis comentarios a la pregunta anterior).

El problema principal es que la propiedad del Norte es una llamada de E / S .

En ese punto de la ejecución del código, el programa necesita verificar las E / S proporcionadas por el código del cliente. (No sería relevante que la entrada sea en forma de delegado, o que la entrada ya se haya pasado, nominalmente).

Una vez que pierde el control de la entrada, no puede garantizar que la función sea pura. (Especialmente si la función puede lanzar).


No tengo claro por qué no desea verificar la llamada a Move [Check] Room. Según mi comentario a la pregunta:

Sí. ¿Qué haces al ejecutar el delegado solo puede funcionar la primera vez o devolver una habitación diferente en la segunda llamada? Llamar al delegado puede considerarse / causar un efecto secundario en sí mismo?

Como Bart van Ingen Schenau dijo anteriormente:

Su función Crear no lo protege de obtener una excepción al obtener la propiedad. Si su delegado lanza, en la vida real es muy probable que lo haga solo bajo ciertas condiciones. Lo más probable es que las condiciones para tirar no estén presentes durante la construcción, pero están presentes cuando se obtiene la propiedad.

En general, cualquier tipo de carga diferida difiere implícitamente los errores hasta ese punto.


Sugeriría usar un método Mover [Verificar] Habitación Esto le permitiría separar los aspectos impuros de E / S en un solo lugar.

Similar a la respuesta de Robert Harvey :

Para que sea una función pura, devuelva un objeto real que encapsule el valor esperado de la función y un valor que indique una posible condición de error, como un objeto Quizás o un objeto Unidad de Trabajo.

Correspondería al escritor de código determinar cómo manejar la (posible) excepción de la entrada. Luego, el método puede devolver un objeto Room, o un objeto Room nulo, o quizás eliminar la excepción.

Este punto depende de:

  • ¿El dominio de la sala trata las excepciones de la sala como nulas o algo peor?
  • Cómo notificar al código del cliente que llama al Norte en una sala de excepción / nula. (Fianza / Variable de estado / Estado global / Devolver una mónada / Lo que sea; Algunos son más puros que otros :)).
SH-
fuente
-1

Hmm ... no me sentía bien con esto. Creo que lo que estás intentando hacer es asegurarte de que otras personas hagan bien su trabajo, sin saber lo que están haciendo.

Dado que el consumidor pasa la función, que usted o otra persona podrían escribir.

¿Qué sucede si la función pass in no es válida para ejecutarse en el momento en que se crea el objeto Room?

Por el código, no podía estar seguro de lo que está haciendo el Norte.

Digamos que si la función es reservar la habitación, y necesita el tiempo y el período de reserva, obtendrá una excepción si no tiene la información lista.

¿Qué pasa si es una función de larga duración? No querrás bloquear tu programa mientras creas un objeto Room, ¿qué sucede si necesitas crear 1000 objetos Room?

Si usted fuera la persona que escribirá al consumidor que consume esta clase, se aseguraría de que la función que se pasa se escriba correctamente, ¿no?

Si hay otra persona que escribe al consumidor, es posible que él / ella no conozca la condición "especial" de crear un objeto Room, lo que podría causar una exención y se rascarían la cabeza tratando de averiguar por qué.

Otra cosa es que no siempre es bueno lanzar una nueva excepción. La razón es que no contendrá la información de la excepción original. Sabes que obtienes un error (un mensaje amigable para una excepción genérica), pero no sabrías cuál es el error (referencia nula, índice fuera del límite, división por cero, etc.). Esta información es muy importante cuando necesitamos hacer una investigación.

Debe manejar la excepción solo cuando sepa qué y cómo manejarla.

Creo que su código original es lo suficientemente bueno, lo único es que es posible que desee manejar la excepción en el "Principal" (o cualquier capa que sepa cómo manejarlo).

usuario251630
fuente
1
Mire nuevamente el segundo ejemplo de código; Inicializo la nueva excepción con la excepción más baja. Se conserva todo el seguimiento de la pila.
Rock Anthony Johnson
Ups Mi error.
user251630
-1

No estaré de acuerdo con las otras respuestas y diré que no . Las excepciones no hacen que una función pura se vuelva impura.

Cualquier uso de excepciones podría reescribirse para usar comprobaciones explícitas de resultados de error. Las excepciones podrían considerarse azúcar sintáctico además de esto. El azúcar sintáctico conveniente no hace que el código puro sea impuro.

JacquesB
fuente
1
El hecho de que pueda reescribir la impureza no hace que la función sea pura. Haskell es una prueba de que el uso de funciones impuras puede reescribirse con funciones puras: utiliza mónadas para codificar cualquier comportamiento impuro en los tipos de retorno funciones puras puras. La existencia de mónadas IO no implica que la IO regular sea pura. ¿Por qué la existencia de mónadas de excepciones implica que las excepciones regulares son puras?
Idan Arye
@IdanArye: Estoy argumentando que no hay impureza en primer lugar. El ejemplo de reescritura fue solo para demostrar eso. IO regular es impuro porque causa efectos secundarios observables, o puede devolver valores diferentes con la misma entrada debido a alguna entrada externa. Lanzar una excepción no hace ninguna de esas cosas.
JacquesB
El código libre de efectos secundarios no es suficiente. La transparencia referencial también es un requisito para la pureza de la función.
Idan Arye
1
El determinismo no es suficiente para la transparencia referencial: los efectos secundarios también pueden ser deterministas. La transparencia referencial significa que la expresión puede reemplazarse por sus resultados, por lo que no importa cuántas veces evalúe expresiones transparentes referenciales, en qué orden y si las evalúa o no, el resultado debe ser el mismo. Si se arroja una excepción de manera determinista, somos buenos con el criterio "no importa cuántas veces", pero no con los otros. He discutido sobre el criterio de "si o no" en mi propia respuesta, (cont ...)
Idan Arye
1
(... cont) y en cuanto al "en qué orden" - si fy gson ambas funciones puras, entonces f(x) + g(x)debería tener el mismo resultado independientemente del orden que evalúe f(x)y g(x). Pero si f(x)lanza Fy g(x)lanza G, entonces la excepción lanzada de esta expresión sería Fsi f(x)se evalúa primero y Gsi g(x)se evalúa primero, ¡así no es cómo deben comportarse las funciones puras! Cuando la excepción se codifica en el tipo de retorno, ese resultado terminaría como algo Error(F) + Error(G)independiente del orden de evaluación.
Idan Arye