idea de cambio / coincidencia de patrón

He estado mirando F # recientemente, y aunque no es probable que salte la valla en el corto plazo, definitivamente destaca algunas áreas donde C # (o el soporte de la biblioteca) podría facilitar la vida.

En particular, estoy pensando en la capacidad de coincidencia de patrones de F #, que permite una syntax muy rica, mucho más expresiva que el interruptor actual / equivalentes de C # condicional. No intentaré dar un ejemplo directo (mi F # no está a la altura), pero en resumen, permite:

  • coincidencia por tipo (con verificación de cobertura completa para las uniones discriminadas) [tenga en cuenta que esto también deduce el tipo de la variable vinculada, dando acceso a los miembros, etc.]
  • coincidencia por predicado
  • combinaciones de los anteriores (y posiblemente algunos otros escenarios que desconozco)

Si bien sería agradable para C # tomar prestado [ejem] algo de esta riqueza, mientras tanto he estado viendo qué se puede hacer en tiempo de ejecución, por ejemplo, es bastante fácil agrupar algunos objetos para permitir:

var getRentPrice = new Switch() .Case(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle .Case(30) // returns a constant .Case(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .ElseThrow(); // or could use a Default(...) terminator 

donde getRentPrice es un Func .

[nota – tal vez Switch / Case aquí es los términos incorrectos … pero muestra la idea]

Para mí, esto es mucho más claro que el equivalente que usa el if / else repetido, o un condicional ternario compuesto (que se vuelve muy complicado para expresiones no triviales, entre paréntesis). También evita una gran cantidad de conversión, y permite la extensión simple (ya sea directamente o mediante métodos de extensión) a las coincidencias más específicas, por ejemplo, un partido InRange (…) comparable al VB Select … Caso “x To y “uso.

Estoy tratando de medir si la gente piensa que hay mucho beneficio de construcciones como la anterior (en ausencia de soporte de idiomas)?

Tenga en cuenta, además, que he estado jugando con 3 variantes de lo anterior:

  • una versión de Func para evaluación, comparable a declaraciones condicionales ternarias compuestas
  • una versión de Action – comparable a if / else if / else if / else if / else
  • una versión de Expression <Func > – como la primera, pero utilizable por proveedores arbitrarios de LINQ

Además, el uso de la versión basada en expresiones permite reescribir el árbol de expresiones, esencialmente alineando todas las twigs en una única expresión condicional compuesta, en lugar de usar la invocación repetida. No lo he comprobado recientemente, pero en algunas versiones anteriores de Entity Framework me parece recordar que es necesario, ya que no me gustó mucho InvocationExpression. También permite un uso más eficiente con LINQ-to-Objects, ya que evita repetidas invocaciones de delegado: las pruebas muestran una coincidencia como la anterior (utilizando el formulario Expression) con la misma velocidad [marginalmente más rápido, de hecho] en comparación con el equivalente C # statement condicional compuesta. Para completar, la versión basada en Func tomó 4 veces más tiempo que la sentencia condicional C #, pero sigue siendo muy rápida y es poco probable que sea un cuello de botella importante en la mayoría de los casos de uso.

Doy la bienvenida a cualquier idea / input / crítica / etc. sobre lo anterior (o sobre las posibilidades de un soporte de lenguaje C más rico … aquí está esperando ;-p).

Sé que es un tema viejo, pero en c # 7 puedes hacer:

 switch(shape) { case Circle c: WriteLine($"circle with radius {c.Radius}"); break; case Rectangle s when (s.Length == s.Height): WriteLine($"{s.Length} x {s.Height} square"); break; case Rectangle r: WriteLine($"{r.Length} x {r.Height} rectangle"); break; default: WriteLine(""); break; case null: throw new ArgumentNullException(nameof(shape)); } 

El excelente blog de Bart De Smet tiene una serie de 8 partes sobre cómo hacer exactamente lo que describes. Encuentra la primera parte aquí .

Después de tratar de hacer cosas “funcionales” en C # (e incluso intentar un libro sobre él), he llegado a la conclusión de que no, con algunas excepciones, estas cosas no ayudan demasiado.

La razón principal es que los lenguajes como F # obtienen gran parte de su poder si realmente soportan estas características. No “puedes hacerlo”, pero “es simple, está claro, se espera”.

Por ejemplo, en la coincidencia de patrones, obtienes que el comstackdor te diga si hay una coincidencia incompleta o si nunca se golpeará otra coincidencia. Esto es menos útil con los tipos abiertos, pero cuando se combina una unión discriminada o tuplas, es muy ingenioso. En F #, esperas que la gente coincida con el patrón, y al instante tiene sentido.

El “problema” es que una vez que comienzas a usar algunos conceptos funcionales, es natural querer continuar. Sin embargo, el aprovechamiento de tuplas, funciones, aplicación de métodos parciales y currying, coincidencia de patrones, funciones anidadas, generics, soporte de mónadas, etc. en C # se pone muy feo, muy rápidamente. Es divertido, y algunas personas muy inteligentes han hecho algunas cosas muy interesantes en C #, pero en realidad su uso se siente pesado.

Lo que terminé usando a menudo (a través de proyectos) en C #:

  • Funciones de secuencia, a través de métodos de extensión para IEnumerable. Cosas como ForEach o Process (“Aplicar”? – hacer una acción en un elemento de secuencia como se enumera) encajan porque la syntax de C # lo soporta bien.
  • Resumiendo patrones comunes de statement. Complicados bloques try / catch / finally u otros bloques de código involucrados (a menudo muy generics). Aquí también se puede ampliar LINQ-to-SQL.
  • Tuplas, hasta cierto punto.

** Pero tenga en cuenta: la falta de generalización automática e inferencia de tipo realmente dificulta el uso de incluso estas características. **

Dicho todo esto, como alguien más mencionó, en un equipo pequeño, para un propósito específico, sí, quizás puedan ayudarlo si está atrapado con C #. Pero en mi experiencia, por lo general se sentían más molestos de lo que valían, YMMV.

Algunos otros enlaces:

  • Mono.Rocks playground tiene muchas cosas similares (además de adiciones de progtwigción no funcional pero útiles).
  • La biblioteca funcional de C # de Luca Bolognese
  • C # funcional de Matthew Podwysocki en MSDN

Podría decirse que la razón por la que C # no hace que sea simple activar el tipo es porque es principalmente un lenguaje orientado a objetos, y la manera ‘correcta’ de hacerlo en términos orientados a objetos sería definir un método GetRentPrice en Vehicle y anularlo en clases derivadas.

Dicho esto, he pasado un tiempo jugando con lenguajes multi-paradigma y funcionales como F # y Haskell que tienen este tipo de capacidad, y he encontrado varios lugares donde sería útil antes (por ejemplo, cuando no está escribiendo los tipos que necesita para encender, por lo que no puede implementar un método virtual en ellos) y es algo que agradecería en el lenguaje junto con las uniones discriminadas.

[Editar: Se eliminó parte sobre el rendimiento ya que Marc indicó que podría estar en cortocircuito]

Otro problema potencial es el de usabilidad: desde la última llamada queda claro qué sucede si el partido no cumple con alguna condición, pero ¿cuál es el comportamiento si coincide con dos o más condiciones? ¿Debería lanzar una excepción? ¿Debería devolver el primer o el último partido?

Una manera que tiendo a usar para resolver este tipo de problema es usar un campo de diccionario con el tipo como la clave y el lambda como el valor, que es bastante escueto de construir usando la syntax del inicializador de objetos; sin embargo, esto solo tiene en cuenta el tipo concreto y no permite los predicados adicionales, por lo que puede no ser adecuado para casos más complejos. [Nota: si observa el resultado del comstackdor de C #, con frecuencia convierte las sentencias de cambio en tablas de salto basadas en diccionario, por lo que no parece haber una buena razón por la que no pueda admitir el cambio de tipos]

No creo que este tipo de bibliotecas (que actúan como extensiones de lenguaje) tengan una amplia aceptación, pero son divertidas para jugar y pueden ser realmente útiles para equipos pequeños que trabajan en dominios específicos donde esto es útil. Por ejemplo, si está escribiendo toneladas de ‘reglas / lógica de negocios’ que hacen pruebas de tipo arbitrarias como esta y otras, puedo ver cómo sería útil.

No tengo idea si es probable que sea una característica de lenguaje C # (parece dudoso, pero ¿quién puede ver el futuro?).

Como referencia, el F # correspondiente es aproximadamente:

 let getRentPrice (v : Vehicle) = match v with | :? Motorcycle as bike -> 100 + bike.Cylinders * 10 | :? Bicycle -> 30 | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20 | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20 | _ -> failwith "blah" 

asumiendo que había definido una jerarquía de clase a lo largo de las líneas de

 type Vehicle() = class end type Motorcycle(cyl : int) = inherit Vehicle() member this.Cylinders = cyl type Bicycle() = inherit Vehicle() type EngineType = Diesel | Gasoline type Car(engType : EngineType, doors : int) = inherit Vehicle() member this.EngineType = engType member this.Doors = doors 

Para responder a su pregunta, sí creo que los constructos sintácticos coincidentes son útiles. Por mi parte, me gustaría ver el soporte sintáctico en C #.

Aquí está mi implementación de una clase que proporciona (casi) la misma syntax que usted describe

 public class PatternMatcher { List, Func>> cases = new List,Func>>(); public PatternMatcher() { } public PatternMatcher Case(Predicate condition, Func function) { cases.Add(new Tuple, Func>(condition, function)); return this; } public PatternMatcher Case(Predicate condition, Func function) { return Case( o => o is T && condition((T)o), o => function((T)o)); } public PatternMatcher Case(Func function) { return Case( o => o is T, o => function((T)o)); } public PatternMatcher Case(Predicate condition, Output o) { return Case(condition, x => o); } public PatternMatcher Case(Output o) { return Case(x => o); } public PatternMatcher Default(Func function) { return Case(o => true, function); } public PatternMatcher Default(Output o) { return Default(x => o); } public Output Match(Object o) { foreach (var tuple in cases) if (tuple.Item1(o)) return tuple.Item2(o); throw new Exception("Failed to match"); } } 

Aquí hay un código de prueba:

  public enum EngineType { Diesel, Gasoline } public class Bicycle { public int Cylinders; } public class Car { public EngineType EngineType; public int Doors; } public class MotorCycle { public int Cylinders; } public void Run() { var getRentPrice = new PatternMatcher() .Case(bike => 100 + bike.Cylinders * 10) .Case(30) .Case(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .Default(0); var vehicles = new object[] { new Car { EngineType = EngineType.Diesel, Doors = 2 }, new Car { EngineType = EngineType.Diesel, Doors = 4 }, new Car { EngineType = EngineType.Gasoline, Doors = 3 }, new Car { EngineType = EngineType.Gasoline, Doors = 5 }, new Bicycle(), new MotorCycle { Cylinders = 2 }, new MotorCycle { Cylinders = 3 }, }; foreach (var v in vehicles) { Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v)); } } 

Coincidencia de patrones (como se describe aquí ), su propósito es deconstruir los valores de acuerdo con su especificación de tipo. Sin embargo, el concepto de una clase (o tipo) en C # no concuerda contigo.

El diseño del lenguaje de múltiples paradigmas no está bien, por el contrario, es muy agradable tener lambdas en C #, y Haskell puede hacer cosas imprescindibles, por ejemplo, IO. Pero no es una solución muy elegante, no de la moda de Haskell.

Pero dado que los lenguajes de progtwigción procedural secuenciales se pueden entender en términos de cálculo lambda, y C # encaja bien dentro de los parámetros de un lenguaje de procedimiento secuencial, es una buena opción. Pero, tomar algo del contexto funcional puro de, digamos, Haskell, y luego poner esa característica en un lenguaje que no es puro, bueno, haciendo exactamente eso, no garantizará un mejor resultado.

Mi punto es esto, lo que hace que la coincidencia de patrones coincida con el diseño del idioma y el modelo de datos. Habiendo dicho eso, no creo que la coincidencia de patrones sea una característica útil de C # porque no resuelve los problemas típicos de C # ni encaja bien dentro del paradigma de progtwigción imperativa.

En mi humilde opinión, la manera OO de hacer tales cosas es el patrón de visitante. Sus métodos de miembro visitante simplemente actúan como construcciones de casos y permite que el lenguaje maneje el despacho apropiado sin tener que “echar un vistazo” a los tipos.

Aunque no es muy ‘C-sharpey’ para activar el tipo, sé que el constructo sería bastante útil en el uso general: tengo al menos un proyecto personal que podría usarlo (aunque es un ATM manejable). ¿Hay mucho de un problema de comstackción de rendimiento, con la escritura del árbol de expresiones?

Creo que esto se ve realmente interesante (+1), pero hay que tener cuidado con esto: el comstackdor de C # es bastante bueno para optimizar las declaraciones de cambio. No solo para cortocircuitos: obtiene IL completamente diferente según la cantidad de casos que tenga, y así sucesivamente.

Su ejemplo específico hace algo que me parece muy útil: no hay syntax equivalente a caso por tipo, ya que (por ejemplo) typeof(Motorcycle) no es una constante.

Esto se vuelve más interesante en la aplicación dinámica: su lógica aquí podría ser fácilmente manejada por datos, dando la ejecución de estilo ‘rule-engine’.

Puedes lograr lo que buscas utilizando una biblioteca que escribí, llamada OneOf

La principal ventaja sobre el switch (y if y las exceptions as control flow ) es que es seguro en tiempo de comstackción, no hay un controlador predeterminado o no funciona

  OneOf vehicle = ... //assign from one of those types var getRentPrice = vehicle .Match( bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle bike => 30, // returns a constant car => car.EngineType.Match( diesel => 220 + car.Doors * 20 petrol => 200 + car.Doors * 20 ) ); 

Está en Nuget y apunta a net451 y netstandard1.6