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?
fuente
Create
también es impuro, porque lo llama.Create
funció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.Respuestas:
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,
return
un objeto real que encapsula el valor esperado de la función y un valor que indica una posible condición de error, como unMaybe
objeto o un objeto de Unidad de trabajo.fuente
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:
Si
func
es puro, un optimizador podría decidir quefunc(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
Error
clase como Java (y muchos otros lenguajes), habría sugerido lanzar un enError
lugar de unException
. 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 unaError
clase, y las excepciones de error de uso parecen derivar deException
. Mi sugerencia es lanzar unArgumentException
, 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.
fuente
(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.func
esa manera, el bloque que he dado podría haberse optimizado, porque en lugar de desenrollar la pila, la excepción lanzada se habría codificadoignoreThis
, 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.func(arg)
devolvería la tupla(<junk>, TheException())
, porignoreThis
lo que contendrá la tupla(<junk>, TheException())
, pero dado que nunca usamosignoreThis
la excepción no tendrá ningún efecto en nada.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:
Como Bart van Ingen Schenau dijo anteriormente:
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 :
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:
fuente
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).
fuente
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.
fuente
f
yg
son ambas funciones puras, entoncesf(x) + g(x)
debería tener el mismo resultado independientemente del orden que evalúef(x)
yg(x)
. Pero sif(x)
lanzaF
yg(x)
lanzaG
, entonces la excepción lanzada de esta expresión seríaF
sif(x)
se evalúa primero yG
sig(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 algoError(F) + Error(G)
independiente del orden de evaluación.