Pergunta

Seguindo assistindo Apresentação de Nick Partidge na derivação escalaz, comecei a olhar para este exemplo, que é simplesmente incrível:

import scalaz._
import Scalaz._
def even(x: Int) : Validation[NonEmptyList[String], Int] 
    = if (x % 2 ==0) x.success else "not even: %d".format(x).wrapNel.fail

println( even(3) <|*|> even(5) ) //prints: Failure(NonEmptyList(not even: 3, not even: 5))

Eu estava tentando entender o que <|*|> método estava fazendo, aqui está o código fonte:

def <|*|>[B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[(A, B)] 
    = <**>(b, (_: A, _: B))

OK, isso é bastante confuso (!) - mas faz referência ao <**> método, que é declarado assim:

def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] 
    = a(t.fmap(value, z.curried), b)

Então eu tenho algumas perguntas:

  1. Como é que o método parece levar um tipo de tipo superior de um parâmetro de tipo (M[B]), mas pode passar por um Validation (que tem dois parâmetros de tipo)?
  2. A sintaxe (_: A, _: B) define a função (A, B) => Pair[A,B] que o segundo método espera: o que está acontecendo com Tuple2/Pair no caso de falha?Não há nenhuma tupla à vista!
Foi útil?

Solução

Construtores de tipo como parâmetros de tipo

M é um parâmetro de tipo para um dos principais cafetões do Scalaz, MA, que representa o Type Constructor (também conhecido como Higher Kinded Type) do valor modificado.Este construtor de tipo é usado para procurar as instâncias apropriadas de Functor e Apply, que são requisitos implícitos para o método <**>.

trait MA[M[_], A] {
   val value: M[A]
   def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] = ...
}

O que é um construtor de tipo?

Da referência da linguagem Scala:

Distinguimos entre tipos de primeira ordem e construtores de tipos, que pegam parâmetros de tipo e tipos de rendimento.Um subconjunto de tipos de primeira ordem chamados tipos de valor representa conjuntos de valores (de primeira classe).Os tipos de valor são concretos ou abstratos.Todo tipo de valor concreto pode ser representado como tipo de classe, ou seja,um designador de tipo (§3.2.3) que se refere a uma classe1 (§5.3), ou como um tipo de composto (§3.2.7), representando uma interseção de tipos, possivelmente com um refinamento (§3.2.7) que restringe ainda mais o tipos de seus membros.Os tipos de valor abstrato são introduzidos por parâmetros do tipo (§4.4) e ligações do tipo abstrato (§4.3).Parênteses nos tipos são usados ​​para agrupamento.Assumimos que objetos e pacotes também definem implicitamente uma classe (com o mesmo nome que o objeto ou o pacote, mas inacessíveis para os programas de usuário).

Tipos de não valor capturam propriedades de identificadores que não são valores (§3.3).Por exemplo, um construtor de tipo (§3.3.3) não especifica diretamente o tipo de valores.No entanto, quando um construtor de tipo é aplicado aos argumentos do tipo correto, ele produz um tipo de primeira ordem, que pode ser um tipo de valor.Os tipos de não valor são expressos indiretamente em Scala.Por exemplo, um tipo de método é descrito anotando uma assinatura de método, que por si só não é um tipo real, embora dê origem a um tipo de função correspondente (§3.3.1).Os construtores de tipo são outro exemplo, pois é possível escrever o tipo de troca [m [_, _], a, b] = m [b, a], mas não há sintaxe para gravar a função do tipo anônimo correspondente diretamente.

List é um construtor de tipo.Você pode aplicar o tipo Int para obter um tipo de valor, List[Int], que pode classificar um valor.Outros construtores de tipo utilizam mais de um parâmetro.

A característica scalaz.MA requer que seu primeiro parâmetro de tipo seja um construtor de tipo que usa um único tipo para retornar um tipo de valor, com a sintaxe trait MA[M[_], A] {}.A definição do parâmetro de tipo descreve a forma do construtor de tipo, que é chamado de Kind. List diz-se que tem o tipo '* -> *.

Aplicação Parcial de Tipos

Mas como pode MA envolver valores do tipo Validation[X, Y]?O tipo Validation tem um tipo (* *) -> *, e só poderia ser passado como um argumento de tipo para um parâmetro de tipo declarado como M[_, _].

Esta conversão implícita em objeto Scalaz converte um valor do tipo Validation[X, Y] para um MA:

object Scalaz {
    implicit def ValidationMA[A, E](v: Validation[E, A]): MA[PartialApply1Of2[Validation, E]#Apply, A] = ma[PartialApply1Of2[Validation, E]#Apply, A](v)
}

Que por sua vez usa um truque com um alias de tipo em Aplicação Parcial1Of2 para aplicar parcialmente o construtor de tipo Validation, corrigindo o tipo dos erros, mas deixando o tipo de sucesso não aplicado.

PartialApply1Of2[Validation, E]#Apply seria melhor escrito como [X] => Validation[E, X].Recentemente, propus adicionar essa sintaxe ao Scala, isso pode acontecer no 2.9.

Pense nisso como um nível de tipo equivalente a isto:

def validation[A, B](a: A, b: B) = ...
def partialApply1Of2[A, B C](f: (A, B) => C, a: A): (B => C) = (b: B) => f(a, b)

Isso permite combinar Validation[String, Int] com um Validation[String, Boolean], porque ambos compartilham o construtor de tipo [A] Validation[String, A].

Funtores Aplicativos

<**> exige o construtor do tipo M deve ter instâncias associadas de Aplicar e Funtor.Isso constitui um Functor Aplicativo, que, como uma Mônada, é uma forma de estruturar uma computação por meio de algum efeito.Neste caso, o efeito é que os subcálculos podem falhar (e quando isso acontece, acumulamos as falhas).

O contêiner Validation[NonEmptyList[String], A] pode envolver um valor puro do tipo A neste 'efeito'.O <**> O operador pega dois valores efetivos e uma função pura e os combina com a instância do Applicative Functor para esse contêiner.

Veja como funciona para o Option funtor aplicativo.O “efeito” aqui é a possibilidade de fracasso.

val os: Option[String] = Some("a")
val oi: Option[Int] = Some(2)

val result1 = (os <**> oi) { (s: String, i: Int) => s * i }
assert(result1 == Some("aa"))

val result2 = (os <**> (None: Option[Int])) { (s: String, i: Int) => s * i }
assert(result2 == None)

Em ambos os casos, existe uma função pura do tipo (String, Int) => String, sendo aplicado a argumentos eficazes.Observe que o resultado está envolvido no mesmo efeito (ou contêiner, se preferir) que os argumentos.

Você pode usar o mesmo padrão em vários contêineres que possuem um Applicative Functor associado.Todas as Mônadas são automaticamente Functores Aplicativos, mas existem ainda mais, como ZipStream.

Option e [A]Validation[X, A] são ambas Mônadas, então você também pode usar Bind (também conhecido como flatMap):

val result3 = oi flatMap { i => os map { s => s * i } }
val result4 = for {i <- oi; s <- os} yield s * i

Tupla com `<|**|>`

<|**|> é realmente semelhante a <**>, mas fornece a função pura para você simplesmente construir um Tuple2 a partir dos resultados. (_: A, _ B) é uma abreviatura para (a: A, b: B) => Tuple2(a, b)

E além

Aqui estão nossos exemplos agrupados para Aplicativo e Validação.Usei uma sintaxe um pouco diferente para usar o Applicative Functor, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => .... }

ATUALIZAR:Mas o que acontece no caso de falha?

o que está acontecendo com o Tuple2/Pair no caso de falha?

Se algum dos subcálculos falhar, a função fornecida nunca será executada.Ele só é executado se todos os subcálculos (neste caso, os dois argumentos passados ​​para <**>) são bem sucedidos.Se assim for, combina-os num Success.Onde está essa lógica?Isso define o Apply exemplo para [A] Validation[X, A].Exigimos que o tipo X tenha um Semigroup disponível, que é a estratégia para combinar os erros individuais, cada um do tipo X, em um erro agregado do mesmo tipo.Se você escolher String como seu tipo de erro, o Semigroup[String] concatena as strings;se você escolher NonEmptyList[String], os erros de cada etapa são concatenados em um erro mais longo NonEmptyList de erros.Esta concatenação acontece abaixo quando dois Failures são combinados, usando o operador (que se expande com implícitos para, por exemplo, Scalaz.IdentityTo(e1).⊹(e2)(Semigroup.NonEmptyListSemigroup(Semigroup.StringSemigroup)).

implicit def ValidationApply[X: Semigroup]: Apply[PartialApply1Of2[Validation, X]#Apply] = new Apply[PartialApply1Of2[Validation, X]#Apply] {
  def apply[A, B](f: Validation[X, A => B], a: Validation[X, A]) = (f, a) match {
    case (Success(f), Success(a)) => success(f(a))
    case (Success(_), Failure(e)) => failure(e)
    case (Failure(e), Success(_)) => failure(e)
    case (Failure(e1), Failure(e2)) => failure(e1 ⊹ e2)
  }
}

Mônada ou Aplicativo, como devo escolher?

Ainda está lendo?(Sim.Ed.)

Eu mostrei que subcomputações baseadas em Option ou [A] Validation[E, A] pode ser combinado com qualquer Apply ou com Bind.Quando você escolheria um em vez do outro?

Quando você usa Apply, a estrutura do cálculo é fixa.Todos os subcálculos serão executados;os resultados de um não podem influenciar os outros.Somente a função ‘pura’ tem uma visão geral do que aconteceu.Os cálculos monádicos, por outro lado, permitem que o primeiro subcomputamento influencie os posteriores.

Se utilizássemos uma estrutura de validação Monádica, a primeira falha causaria um curto-circuito em toda a validação, pois não haveria Success valor para alimentar a validação subsequente.Porém, ficamos felizes que as subvalidações sejam independentes, para que possamos combiná-las através do Aplicativo e coletar todas as falhas que encontrarmos.A fraqueza dos Applicative Functors tornou-se uma força!

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top