Parece que o código F # geralmente corresponde a padrões em relação aos tipos. Certamente
match opt with
| Some val -> Something(val)
| None -> Different()
parece comum.
Mas, do ponto de vista da OOP, isso se parece muito com o fluxo de controle com base em uma verificação do tipo de tempo de execução, que normalmente seria desaprovada. Para explicar, no POO você provavelmente preferiria usar sobrecarga:
type T =
abstract member Route : unit -> unit
type Foo() =
interface T with
member this.Route() = printfn "Go left"
type Bar() =
interface T with
member this.Route() = printfn "Go right"
Este é certamente mais código. OTOH, parece-me ter vantagens estruturais:
- a extensão para uma nova forma de
T
é fácil; - Não preciso me preocupar em encontrar duplicação do fluxo de controle de escolha de rota; e
- A escolha da rota é imutável, no sentido em que, uma vez que tenho uma
Foo
mão na mão, nunca mais preciso me preocupar comBar.Route()
a implementação da
Existem vantagens na correspondência de padrões com tipos que não estou vendo? É considerado idiomático ou é um recurso que não é comumente usado?
object-oriented
functional-programming
f#
pattern-matching
Larry OBrien
fonte
fonte
But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.
parece muito dogmático. Às vezes, você deseja separar seus ops da hierarquia: talvez 1) você não possa adicionar um op a uma hierarquia porque não é o proprietário da hierarquia; 2) as classes que você deseja que o op não correspondam à sua hierarquia; 3) você pode adicionar o op à sua hierarquia, mas não quer b / c, não quer desorganizar a API da sua hierarquia com um monte de porcaria que a maioria dos clientes não usa.Some
eNone
não são tipos. Ambos são construtores cujos tipos sãoforall a. a -> option a
eforall a. option a
(desculpe, não sei qual é a sintaxe das anotações de tipo em F #).Respostas:
Você está certo de que as hierarquias de classe OOP estão muito relacionadas a uniões discriminadas em F # e que a correspondência de padrões está intimamente relacionada a testes de tipo dinâmico. De fato, é assim que o F # compila uniões discriminadas no .NET!
Em relação à extensibilidade, existem dois lados do problema:
Dito isto, o F # emitirá um aviso quando você perder um caso na correspondência de padrões, portanto, adicionar novos casos de união não é tão ruim assim.
Em relação à localização de duplicações na escolha da raiz - o F # emitirá um aviso quando houver uma correspondência duplicada, por exemplo:
O fato de "a escolha da rota ser imutável" também pode ser problemático. Por exemplo, se você deseja compartilhar a implementação de uma função entre
Foo
eBar
casos, mas faz outra coisa para oZoo
caso, é possível codificar isso facilmente usando a correspondência de padrões:Em geral, o FP está mais focado no primeiro design dos tipos e depois na adição de funções. Portanto, ele realmente se beneficia do fato de poder ajustar seus tipos (modelo de domínio) em algumas linhas em um único arquivo e adicionar facilmente as funções que operam no modelo de domínio.
As duas abordagens - OO e FP são bastante complementares e têm vantagens e desvantagens. O difícil (vindo da perspectiva do OO) é que o F # geralmente usa o estilo FP como padrão. Mas se realmente houver mais necessidade de adicionar novas subclasses, você sempre poderá usar interfaces. Mas na maioria dos sistemas, você também precisa adicionar tipos e funções, para que a escolha realmente não importe tanto - e usar uniões discriminadas no F # é melhor.
Eu recomendaria esta excelente série de blogs para obter mais informações.
fonte
Você observou corretamente que a correspondência de padrões (essencialmente uma instrução de comutação sobrecarregada) e o envio dinâmico têm semelhanças. Eles também coexistem em alguns idiomas, com um resultado muito agradável. No entanto, existem pequenas diferenças.
Eu poderia usar o sistema de tipos para definir um tipo que só pode ter um número fixo de subtipos:
Haverá nunca mais ser outro subtipo de
Bool
ouOption
, então subclasses não parece ser útil (algumas línguas como o Scala tem uma noção de subclassificação que pode lidar com isso - uma classe pode ser marcado como fora “final” da unidade de compilação atual, mas subtipos pode ser definido dentro desta unidade de compilação).Como os subtipos de um tipo como
Option
agora são estaticamente conhecidos , o compilador pode avisar se esquecermos de lidar com um caso em nossa correspondência de padrões. Isso significa que uma correspondência de padrão é mais como um downcast especial que nos força a lidar com todas as opções.Além disso, o envio de método dinâmico (necessário para OOP) também implica uma verificação do tipo de tempo de execução, mas de um tipo diferente. Portanto, é irrelevante se fizermos esse tipo de verificação explicitamente através de uma correspondência de padrões ou implicitamente através de uma chamada de método.
fonte
A correspondência de padrões F # geralmente é feita com uma união discriminada, e não com classes (e, portanto, não é tecnicamente uma verificação de tipo). Isso permite que o compilador avise quando você não contabilizar casos em uma correspondência de padrão.
Outro aspecto a ser observado é que, em um estilo funcional, você organiza as coisas por funcionalidade, e não por dados; portanto, as correspondências de padrões permitem reunir as diferentes funcionalidades em um único local, em vez de espalhadas pelas classes. Isso também tem a vantagem de você ver como outros casos são tratados exatamente ao lado de onde você precisa fazer suas alterações.
A adição de uma nova opção se parece com:
fonte
Parcialmente, você o vê com mais frequência na programação funcional porque usa tipos para tomar decisões com mais frequência. Sei que você provavelmente acabou de escolher exemplos mais ou menos aleatoriamente, mas o OOP equivalente ao seu exemplo de correspondência de padrões se pareceria com mais frequência:
Em outras palavras, é relativamente raro usar o polimorfismo para evitar coisas rotineiras, como verificações nulas no OOP. Assim como um programador OO não cria um objeto nulo em todas as situações, um programador funcional nem sempre sobrecarrega uma função, especialmente quando você sabe que sua lista de padrões é exaustiva. Se você usar o sistema de tipos em mais situações, verá que ele é usado de maneiras às quais não está acostumado.
Por outro lado, a programação funcional idiomática equivalente ao seu exemplo de POO provavelmente não usaria correspondência de padrões, mas teria
fooRoute
ebarRoute
funções que seriam passadas como argumentos para o código de chamada. Se alguém usasse a correspondência de padrões nessa situação, geralmente seria considerado errado, assim como alguém que ativasse tipos seria considerado errado no OOP.Então, quando a correspondência de padrões é considerada um bom código de programação funcional? Quando você está fazendo mais do que apenas olhar para os tipos e ao estender os requisitos, não é necessário adicionar mais casos. Por exemplo,
Some val
não apenas verifica seopt
há tipoSome
, mas também se vinculaval
ao tipo subjacente para uso com segurança de tipo no outro lado do->
. Você sabe que provavelmente nunca precisará de um terceiro caso, por isso é um bom uso.A correspondência de padrões pode se assemelhar superficialmente a uma instrução de chave orientada a objetos, mas há muito mais acontecendo, especialmente com padrões mais longos ou aninhados. Certifique-se de levar tudo em consideração antes de declarar o equivalente a algum código OOP mal projetado. Muitas vezes, está lidando sucintamente com uma situação que não pode ser representada de maneira limpa em uma hierarquia de herança.
fonte
Some
eNone
não são tipos, para que não são padrão de correspondência em tipos. Você faz a correspondência de padrões em construtores do mesmo tipo . Não é como perguntar "instanceof".