ideia de correspondência de comutador / padrão

151

Eu estive analisando o F # recentemente e, embora não seja provável que pule a barreira tão cedo, ele definitivamente destaca algumas áreas em que o C # (ou o suporte de biblioteca) poderia facilitar a vida.

Em particular, estou pensando no recurso de correspondência de padrões do F #, que permite uma sintaxe muito rica - muito mais expressiva do que os atuais comutadores / equivalentes em C # condicionais. Não tentarei dar um exemplo direto (meu F # não depende dele), mas, em suma, ele permite:

  • corresponder por tipo (com verificação de cobertura total para uniões discriminadas) [observe que isso também infere o tipo da variável vinculada, fornecendo acesso a membros etc.]
  • combinar por predicado
  • combinações dos itens acima (e possivelmente de outros cenários dos quais não conheço)

Embora seja adorável o C # eventualmente emprestar [ahem] parte dessa riqueza, nesse meio tempo, eu estive analisando o que pode ser feito em tempo de execução - por exemplo, é bastante fácil reunir alguns 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

onde getRentPrice é um Func <Vehicle, int>.

[nota - talvez Switch / Case tenha os termos errados ... mas mostra a ideia]

Para mim, isso é muito mais claro que o equivalente, usando if / else repetido ou uma condicional ternária composta (que fica muito confusa para expressões não triviais - colchetes em abundância). Ele também evita muitas transmissões e permite uma extensão simples (diretamente ou por meio de métodos de extensão) para correspondências mais específicas, por exemplo, uma correspondência InRange (...) comparável à VB Select ... Case "x To y "uso.

Estou apenas tentando avaliar se as pessoas pensam que há muitos benefícios em construções como as mencionadas acima (na ausência de suporte ao idioma)?

Observe também que eu tenho jogado com 3 variantes do acima:

  • uma versão Func <TSource, TValue> para avaliação - comparável a declarações condicionais ternárias compostas
  • uma versão Action <TSource> - comparável a if / else if / else if / else if / else
  • uma versão Expression <Func <TSource, TValue >> - como a primeira, mas utilizável por provedores arbitrários de LINQ

Além disso, o uso da versão baseada em expressão permite reescrever a árvore de expressões, essencialmente incorporando todas as ramificações em uma única expressão condicional composta, em vez de usar a chamada repetida. Não verifiquei recentemente, mas em algumas versões anteriores do Entity Framework, lembro que isso era necessário, pois não gostava muito de InvocationExpression. Ele também permite um uso mais eficiente com o LINQ-to-Objects, uma vez que evita repetidas invocações de delegados - os testes mostram uma correspondência como a acima (usando o formulário Expressão) com a mesma velocidade [marginalmente mais rápida, na verdade] em comparação com o equivalente em C # declaração condicional composta. Para ser completo, a versão baseada em Func demorou quatro vezes mais que a instrução condicional C #, mas ainda é muito rápida e dificilmente será um grande gargalo na maioria dos casos de uso.

Congratulo-me com quaisquer pensamentos / sugestões / críticas / etc sobre o que foi dito acima (ou sobre as possibilidades de um suporte mais avançado à linguagem C # ... aqui está a esperança ;-p).

Marc Gravell
fonte
"Estou apenas tentando avaliar se as pessoas pensam que há muitos benefícios em construções como as mencionadas acima (na ausência de suporte ao idioma)?" IMHO, sim. Já não existe algo semelhante? Caso contrário, sinta-se incentivado a escrever uma biblioteca leve.
Konrad Rudolph
10
Você pode usar o VB .NET que suporta isso em sua instrução case case. Eek!
Jim Burger
Além disso, vou toot meu próprio chifre e adicionar um link para a minha biblioteca: funcional-dotnet
Alexey Romanov
1
Gosto dessa idéia e cria uma forma muito agradável e muito mais flexível de um caso de switch; no entanto, essa não é realmente uma maneira embelezada de usar a sintaxe semelhante ao Linq como um invólucro if-then? Eu desencorajaria alguém de usar isso no lugar do negócio real, ou seja, uma switch-casedeclaração. Não me interpretem mal, acho que tem o seu lugar e provavelmente procurarei uma maneira de implementar.
iAbstract
2
Embora essa pergunta tenha mais de dois anos, é pertinente mencionar que o C # 7 será lançado em breve (ish) com recursos de correspondência de padrões.
Abion47

Respostas:

22

Eu sei que é um tópico antigo, mas no c # 7 você pode fazer:

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));
}
Marcus Pierce
fonte
A diferença notável aqui entre C # e F # é a integridade da correspondência de padrões. Que a correspondência de padrões abrange todos os casos possíveis disponíveis, totalmente descritos, avisos do compilador, caso contrário. Embora você possa argumentar com razão que o caso padrão faz isso, na prática também é uma exceção em tempo de execução.
VoronoiPotato
37

Depois de tentar fazer essas coisas "funcionais" em C # (e até tentar um livro), cheguei à conclusão de que não, com algumas exceções, essas coisas não ajudam muito.

O principal motivo é que idiomas como o F # obtêm muito de seu poder ao realmente oferecer suporte a esses recursos. Não "você pode fazer isso", mas "é simples, é claro, é esperado".

Por exemplo, na correspondência de padrões, o compilador informa se há uma correspondência incompleta ou quando outra correspondência nunca será atingida. Isso é menos útil com tipos abertos, mas ao combinar uma união ou tupla discriminada, é muito bacana. No F #, você espera que as pessoas correspondam aos padrões e isso faz sentido instantaneamente.

O "problema" é que, depois que você começa a usar alguns conceitos funcionais, é natural querer continuar. No entanto, alavancar tuplas, funções, aplicação de método parcial e currying, correspondência de padrões, funções aninhadas, genéricos, suporte a mônada etc. no C # fica muito feio, muito rapidamente. É divertido, e algumas pessoas muito inteligentes fizeram coisas muito legais em C #, mas na verdade usá- lo parece pesado.

O que acabei usando frequentemente (em todos os projetos) em C #:

  • Funções de sequência, através de métodos de extensão para IEnumerable. Coisas como ForEach ou Process ("Apply"? - executam uma ação em um item de sequência, conforme ele é enumerado), porque a sintaxe do C # suporta isso bem.
  • Abstraindo padrões de declaração comuns. Blocos de tentativa / captura / finalmente complicados ou outros blocos de código envolvidos (geralmente muito genéricos). A extensão do LINQ-SQL também se encaixa aqui.
  • Tuplas, até certo ponto.

** Mas observe: a falta de generalização automática e inferência de tipo realmente atrapalha o uso desses recursos. **

Tudo isso dito, como outra pessoa mencionou, em uma equipe pequena, com um objetivo específico, sim, talvez eles possam ajudar se você estiver preso ao C #. Mas, na minha experiência, eles geralmente pareciam mais problemas do que valiam - YMMV.

Alguns outros links:

MichaelGG
fonte
25

Indiscutivelmente, o motivo pelo qual o C # não simplifica a ativação de tipos é porque é principalmente uma linguagem orientada a objetos, e a maneira 'correta' de fazer isso em termos orientados a objetos seria definir um método GetRentPrice em Vehicle and substituí-lo em classes derivadas.

Dito isso, passei um pouco de tempo brincando com linguagens multiparadigma e funcionais, como F # e Haskell, que têm esse tipo de capacidade, e me deparei com vários lugares em que isso seria útil antes (por exemplo, quando você não estão escrevendo os tipos que você precisa ativar, para que você não possa implementar um método virtual neles) e é algo que eu gostaria de receber no idioma junto com uniões discriminadas.

[Editar: Parte removida sobre desempenho, como Marc indicou que poderia estar em curto-circuito]

Outro problema em potencial é o de usabilidade - fica claro na chamada final o que acontece se a partida não atender a alguma condição, mas qual é o comportamento se ela corresponder a duas ou mais condições? Deveria lançar uma exceção? Deve retornar a primeira ou a última partida?

Uma maneira que costumo usar para resolver esse tipo de problema é usar um campo de dicionário com o tipo como chave e o lambda como valor, que é bastante conciso para construir usando a sintaxe do inicializador de objetos; no entanto, isso explica apenas o tipo concreto e não permite predicados adicionais; portanto, pode não ser adequado para casos mais complexos. [Observação: se você observar a saída do compilador C #, ele freqüentemente converterá instruções de chave em tabelas de salto baseadas em dicionário; portanto, não parece haver um bom motivo para não suportar a ativação de tipos]

Greg Beech
fonte
1
Na verdade - a versão que eu tenho faz um curto-circuito nas versões de delegado e de expressão. A versão da expressão é compilada em uma condicional composta; a versão delegada é simplesmente um conjunto de predicados e funções / ações - uma vez que a correspondência é interrompida.
Marc Gravell
Interessante - a partir de uma aparência superficial, presumi que teria que realizar pelo menos uma verificação básica de cada condição, pois parecia uma cadeia de métodos, mas agora percebo que os métodos estão realmente encadeando uma instância de objeto para construí-la para que você possa fazer isso. Vou editar minha resposta para remover essa declaração.
Greg Beech
22

Eu não acho que esses tipos de bibliotecas (que funcionam como extensões de idioma) provavelmente recebam ampla aceitação, mas são divertidos de brincar e podem ser realmente úteis para equipes pequenas que trabalham em domínios específicos onde isso é útil. Por exemplo, se você estiver escrevendo toneladas de 'regras / lógica de negócios' que fazem testes de tipo arbitrário como este e outros enfeites, posso ver como seria útil.

Não faço idéia se é provável que esse seja um recurso da linguagem C # (parece duvidoso, mas quem pode ver o futuro?).

Para referência, o F # correspondente é 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"

assumindo que você definiu uma hierarquia de classes ao longo das linhas 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
Brian
fonte
2
Obrigado pela versão do F #. Acho que gosto da maneira como o F # lida com isso, mas não tenho certeza de que o F # (em geral) seja a escolha certa no momento, então estou tendo que caminhar nesse meio termo ...
Marc Gravell
13

Para responder sua pergunta, sim, acho que as construções sintáticas de correspondência de padrões são úteis. Eu, pelo menos, gostaria de ver suporte sintático em C # para ele.

Aqui está minha implementação de uma classe que fornece (quase) a mesma sintaxe que você descreve

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");
    }
}

Aqui está um código de teste:

    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));
        }
    }
cdiggins
fonte
9

Correspondência de padrões (conforme descrito aqui ), seu objetivo é desconstruir valores de acordo com a especificação de tipo. No entanto, o conceito de uma classe (ou tipo) em C # não concorda com você.

Não há nada de errado com o design de linguagem de múltiplos paradigmas; pelo contrário, é muito bom ter lambdas em C #, e Haskell pode fazer coisas imperativas como, por exemplo, E / S. Mas não é uma solução muito elegante, não da maneira Haskell.

Porém, como as linguagens de programação procedural sequencial podem ser entendidas em termos de cálculo lambda, e o C # se encaixa bem nos parâmetros de uma linguagem processual sequencial, é uma boa opção. Mas, pegar algo do contexto funcional puro de dizer Haskell e colocar esse recurso em uma linguagem que não é pura, bem, fazer exatamente isso, não garantirá um resultado melhor.

O que quero dizer é que o que faz com que a correspondência de padrões esteja ligada ao design da linguagem e ao modelo de dados. Dito isso, não acredito que a correspondência de padrões seja um recurso útil do C # porque ele não resolve problemas típicos de C # nem se encaixa bem no paradigma de programação imperativa.

John Leidegren
fonte
1
Talvez. Na verdade, eu lutaria para pensar em um argumento "matador" convincente sobre por que seria necessário (em oposição a "talvez bom em alguns casos extremos, ao custo de tornar a linguagem mais complexa").
Marc Gravell
5

IMHO a maneira OO de fazer essas coisas é o padrão Visitor. Os métodos de membros visitantes simplesmente agem como construções de maiúsculas e minúsculas e você deixa o próprio idioma manipular o envio apropriado sem precisar "espiar" os tipos.

bacila
fonte
4

Embora não seja muito "C-sharpey" ativar o tipo, eu sei que o construto seria bastante útil no uso geral - eu tenho pelo menos um projeto pessoal que poderia usá-lo (embora seja um ATM gerenciável). Existe muito problema de desempenho de compilação, com a reescrita da árvore de expressões?

Simon Buchan
fonte
Não se você armazenar em cache o objeto para reutilização (que é basicamente como as expressões lambda em C # funcionam, exceto que o compilador oculta o código). A reescrita definitivamente melhora o desempenho compilado - no entanto, para uso regular (em vez de LINQ-to-Something), espero que a versão do delegado seja mais útil.
Marc Gravell
Observe também - não é necessariamente um tipo de alternância - também pode ser usado como condicional composto (mesmo através do LINQ) - mas sem um x => Test? Resultado1: (Teste2? Resultado2: (Teste3? Resultado 3: Resultado4))
Marc Gravell
É bom saber, embora eu quisesse dizer o desempenho da compilação real : quanto tempo o csc.exe leva - não estou familiarizado o suficiente com C # para saber se isso realmente é um problema, mas é um grande problema para o C ++.
Simon Buchan
O csc não piscará com isso - é muito parecido com o funcionamento do LINQ, e o compilador C # 3.0 é muito bom nos métodos LINQ / extensão etc.
Marc Gravell
3

Eu acho que isso parece realmente interessante (+1), mas uma coisa a ter cuidado: o compilador C # é muito bom em otimizar as declarações de opção. Não apenas para curtos-circuitos - você obtém uma IL completamente diferente, dependendo de quantos casos você tem e assim por diante.

Seu exemplo específico faz algo que eu consideraria muito útil - não há sintaxe equivalente a caso por tipo, como (por exemplo) typeof(Motorcycle) não é uma constante.

Isso fica mais interessante em aplicativos dinâmicos - sua lógica aqui pode ser facilmente orientada por dados, fornecendo execução no estilo 'mecanismo de regras'.

Keith
fonte
0

Você pode conseguir o que está procurando usando uma biblioteca que escrevi, chamada OneOf

A principal vantagem sobre switch(e ife exceptions as control flow) é que é seguro em tempo de compilação - não há manipulador padrão ou falha

   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á em Nuget e tem como alvo net451 e netstandard1.6

mcintyre321
fonte