idea de coincidencia de interruptor / patrón


151

He estado mirando F # recientemente, y aunque no es probable que salte la valla en el corto plazo, definitivamente resalta algunas áreas donde C # (o 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 sintaxis muy rica, mucho más expresiva que el interruptor actual / equivalentes condicionales de C #. No intentaré dar un ejemplo directo (mi F # no está a la altura), pero en resumen me permite:

  • coincidencia por tipo (con comprobación de cobertura completa para uniones discriminadas) [tenga en cuenta que esto también infiere el tipo de la variable vinculada, que da acceso a los miembros, etc.]
  • partido por predicado
  • combinaciones de lo anterior (y posiblemente algunos otros escenarios que no conozco)

Si bien sería maravilloso que C # tomara prestada [ejem] parte de esta riqueza, mientras tanto he estado mirando lo que se puede hacer en tiempo de ejecución; por ejemplo, es bastante fácil juntar algunos objetos para permitir:

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

donde getRentPrice es un Func <Vehicle, int>.

[nota: tal vez Switch / Case aquí son los términos incorrectos ... pero muestra la idea]

Para mí, esto es mucho más claro que el equivalente usando if / else repetido o un condicional ternario compuesto (que se vuelve muy desordenado para las expresiones no triviales, corchetes en abundancia). También evita una gran cantidad de conversión, y permite una extensión simple (ya sea directamente o mediante métodos de extensión) a coincidencias más específicas, por ejemplo, una coincidencia InRange (...) comparable a la VB Select ... Caso "x A y "uso.

Solo estoy tratando de evaluar si la gente piensa que hay beneficios de construcciones como las anteriores (en ausencia de soporte de idiomas).

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

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

Además, el uso de la versión basada en Expression permite la reescritura de Expression-tree, esencialmente alineando todas las ramas en una sola Expresión condicional compuesta, en lugar de usar invocación repetida. No lo he comprobado recientemente, pero en algunas de las primeras compilaciones de Entity Framework parece recordar que esto 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 (usando el formulario de Expresión) funcionando a la misma velocidad [marginalmente más rápido, de hecho] en comparación con el equivalente C # declaración condicional compuesta. Para completar, la versión basada en Func <...> tardó 4 veces más que la declaración condicional de 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.

Agradezco cualquier pensamiento / entrada / crítica / etc. sobre lo anterior (o sobre las posibilidades de un soporte de lenguaje C # más rico ... aquí está la esperanza ;-p).


"¿Solo estoy tratando de evaluar si la gente piensa que hay beneficios de construcciones como las anteriores (en ausencia de soporte de idiomas)?" En mi humilde opinión, sí. ¿No existe ya algo similar? Si no es así, siéntase animado a escribir una biblioteca ligera.
Konrad Rudolph el

10
Podría usar VB .NET que lo admite en su declaración de caso de selección. Eek!
Jim Burger

También tocaré mi propio claxon y agregaré un enlace a mi biblioteca: funcional-dotnet
Alexey Romanov

1
Me gusta esta idea y es una forma muy agradable y mucho más flexible de una caja de interruptor; sin embargo, ¿no es esta realmente una forma embellecida de usar la sintaxis tipo Linq como un contenedor if-then? Desalentaría a alguien de usar esto en lugar del trato real, es decir, una switch-casedeclaración. No me malinterpreten, creo que tiene su lugar y probablemente buscaré una forma de implementarlo.
Resumen de

2
Aunque esta pregunta tiene más de dos años, parece pertinente mencionar que C # 7 saldrá pronto (ish) con capacidades de coincidencia de patrones.
Abion47

Respuestas:


22

Sé que es un tema antiguo, 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("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

La diferencia notable aquí entre C # y F # es la integridad de la coincidencia de patrones. Que la coincidencia de patrones cubre todos los casos posibles disponibles, completamente descritos, advertencias del compilador si no lo hace. Si bien puede argumentar legítimamente que el caso predeterminado hace esto, a menudo también es en la práctica una excepción en tiempo de ejecución.
VoronoiPotato

37

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

La razón principal es que los lenguajes como F # obtienen gran parte de su poder al admitir realmente estas características. No "puedes hacerlo", sino "es simple, está claro, se espera".

Por ejemplo, en la coincidencia de patrones, obtienes el compilador que te dice si hay una coincidencia incompleta o cuándo nunca se alcanzará otra coincidencia. Esto es menos útil con los tipos abiertos, pero cuando coincide con una unión o tuplas discriminadas, es muy ingenioso. En F #, espera que las personas coincidan con los patrones, y tiene sentido instantáneamente.

El "problema" es que una vez que comienza a usar algunos conceptos funcionales, es natural querer continuar. Sin embargo, el aprovechamiento de tuplas, funciones, aplicación parcial de métodos y currículum, coincidencia de patrones, funciones anidadas, genéricos, soporte de mónada, etc. en C # se vuelve muy feo, muy rápidamente. Es divertido, y algunas personas muy inteligentes han hecho cosas muy interesantes en C #, pero en realidad usarlo se siente pesado.

Lo que terminé usando a menudo (entre proyectos) en C #:

  • Funciones de secuencia, a través de métodos de extensión para IEnumerable. Cosas como ForEach o Process ("Apply"? - hacer una acción en un elemento de secuencia como se enumera) encajan porque la sintaxis de C # lo admite bien.
  • Resumen de patrones de afirmación común. Bloques de prueba / captura / finalmente complicados u otros bloques de código involucrados (a menudo muy genéricos). La extensión de LINQ-to-SQL también encaja aquí.
  • Tuplas, hasta cierto punto.

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

Todo esto dicho, como alguien más mencionó, en un equipo pequeño, para un propósito específico, sí, tal vez puedan ayudar si estás 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:


25

Podría decirse que la razón por la que C # no facilita el encendido del tipo es porque es principalmente un lenguaje orientado a objetos, y la forma '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 poco de tiempo jugando con lenguajes multi-paradigmáticos y funcionales como F # y Haskell que tienen este tipo de capacidad, y me he encontrado con varios lugares donde sería útil antes (por ejemplo, cuando no escriben los tipos que necesita activar para que no pueda implementar un método virtual en ellos) y es algo que agradecería en el lenguaje junto con uniones discriminadas.

[Editar: Parte eliminada sobre el rendimiento ya que Marc indicó que podría estar en cortocircuito]

Otro problema potencial es el de la usabilidad: en 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 arrojar una excepción? ¿Debería devolver el primer o el último partido?

Una forma en que suelo usar para resolver este tipo de problema es usar un campo de diccionario con el tipo como clave y la lambda como valor, que es bastante breve para construir usando la sintaxis del inicializador de objeto; sin embargo, esto solo explica el tipo concreto y no permite predicados adicionales, por lo que puede no ser adecuado para casos más complejos. [Nota al margen: si observa el resultado del compilador de C #, con frecuencia convierte las declaraciones de cambio en tablas de salto basadas en diccionario, por lo que no parece haber una buena razón por la que no podría soportar el cambio de tipos]


1
En realidad, la versión que tengo hace cortocircuito en las versiones de delegado y expresión. La versión de la expresión compila a un condicional compuesto; la versión delegada es simplemente un conjunto de predicados y funciones / acciones; una vez que tiene una coincidencia, se detiene.
Marc Gravell

Interesante: desde un punto de vista superficial, supuse que tendría que realizar al menos una verificación básica de cada condición, ya que parecía una cadena de métodos, pero ahora me doy cuenta de que los métodos en realidad están encadenando una instancia de objeto para construirla para que pueda hacer esto. Editaré mi respuesta para eliminar esa declaración.
Greg Beech

22

No creo que este tipo de bibliotecas (que actúan como extensiones de lenguaje) tengan una gran aceptación, pero es divertido jugar con ellas y pueden ser realmente útiles para pequeños equipos 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 cosas, puedo ver cómo sería útil.

No tengo idea si es probable que esto sea una característica del 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"

suponiendo que haya definido una jerarquía de clases 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

2
Gracias por la versión F #. Supongo que me gusta la forma en que F # maneja esto, pero no estoy seguro de que (en general) F # sea la opción correcta en este momento, así que tengo que caminar por ese punto medio ...
Marc Gravell

13

Para responder a su pregunta, sí, creo que las construcciones sintácticas de coincidencia de patrones son útiles. Por mi parte, me gustaría ver soporte sintáctico en C # para ello.

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

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> 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<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(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));
        }
    }

9

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

No hay nada malo con el diseño de lenguaje de paradigmas múltiples, por el contrario, es muy bueno tener lambdas en C #, y Haskell puede hacer cosas imprescindibles para, por ejemplo, IO. Pero no es una solución muy elegante, no a la moda de Haskell.

Pero dado que los lenguajes secuenciales de programación procesal pueden entenderse en términos de cálculo lambda, y C # se ajusta bien a los parámetros de un lenguaje procesal secuencial, es un buen ajuste. Pero, tomar algo del contexto funcional puro de decir Haskell, y luego poner esa característica en un lenguaje que no sea puro, bueno, hacer eso, no garantizará un mejor resultado.

Mi punto es este, lo que hace que la marca de coincidencia de patrones esté vinculada al diseño del lenguaje y al modelo de datos. Dicho esto, no creo que la coincidencia de patrones sea una característica útil de C # porque no resuelve problemas típicos de C # ni encaja bien dentro del paradigma de programación imperativo.


1
Tal vez. De hecho, me gustaría pensar en un argumento convincente de "asesino" de por qué sería necesario (en lugar de "quizás agradable en algunos casos extremos a costa de hacer que el lenguaje sea más complejo").
Marc Gravell

5

En mi humilde opinión, la forma OO de hacer tales cosas es el patrón Visitante. Sus métodos de miembro visitante simplemente actúan como construcciones de casos y deja que el lenguaje mismo maneje el despacho apropiado sin tener que "mirar" los tipos.


4

Aunque no es muy 'C-sharpey' para cambiar el tipo, sé que la construcción sería bastante útil en el uso general: tengo al menos un proyecto personal que podría usarlo (aunque es un cajero automático manejable). ¿Hay algún problema de rendimiento de compilación con la reescritura del árbol de expresión?


No si almacena en caché el objeto para su reutilización (que es en gran medida cómo funcionan las expresiones lambda de C #, excepto que el compilador oculta el código). La reescritura definitivamente mejora el rendimiento compilado; sin embargo, para uso regular (en lugar de LINQ-to-Something) espero que la versión delegada sea más útil.
Marc Gravell

Tenga en cuenta también que no es necesariamente un cambio de tipo, también podría usarse como un condicional compuesto (incluso a través de LINQ), pero sin una desordenada x => Prueba? Resultado1: (Prueba2? Resultado2: (Prueba3? Resultado 3: Resultado4))
Marc Gravell

Es bueno saberlo, aunque me refería al rendimiento de la compilación real : cuánto tiempo lleva csc.exe: no estoy lo suficientemente familiarizado con C # para saber si eso realmente es un problema, pero es un gran problema para C ++.
Simon Buchan

csc no parpadeará ante esto: es muy similar a cómo funciona LINQ, y el compilador C # 3.0 es bastante bueno en LINQ / métodos de extensión, etc.
Marc Gravell

3

Creo que esto parece realmente interesante (+1), pero una cosa a tener en cuenta: el compilador de C # es bastante bueno para optimizar las declaraciones de cambio. No solo para cortocircuitos: obtienes una IL completamente diferente dependiendo de cuántos casos tengas, etc.

Su ejemplo específico hace algo que me parece muy útil: no hay una sintaxis equivalente a mayúsculas y minúsculas, como (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 estar fácilmente impulsada por datos, dando una ejecución de estilo 'motor de reglas'.


0

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

La principal ventaja sobre switch(y if, y exceptions as control flow) es que se trata de tiempo de compilación seguro - no hay ningún controlador predeterminado o caer a través

   OneOf<Motorcycle, Bicycle, Car> 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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.