Como definir "disjunção de tipo" (tipos de união)?

181

Uma maneira sugerida para lidar com definições duplas de métodos sobrecarregados é substituir a sobrecarga pela correspondência de padrões:

object Bar {
   def foo(xs: Any*) = xs foreach { 
      case _:String => println("str")
      case _:Int => println("int")
      case _ => throw new UglyRuntimeException()
   }
}

Essa abordagem exige que entregemos a verificação de tipo estático nos argumentos para foo. Seria muito melhor poder escrever

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case _: String => println("str")
      case _: Int => println("int")
   }
}

Posso me aproximar Either, mas fica feio rapidamente com mais de dois tipos:

type or[L,R] = Either[L,R]

implicit def l2Or[L,R](l: L): L or R = Left(l)
implicit def r2Or[L,R](r: R): L or R = Right(r)

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case Left(l) => println("str")
      case Right(r) => println("int")
   }
}

Parece uma solução geral (elegante, eficiente) exigiria definir Either3, Either4, .... Alguém sabe de uma solução alternativa para atingir o mesmo fim? Que eu saiba, Scala não possui "disjunção de tipo" embutida. Além disso, as conversões implícitas definidas acima estão à espreita na biblioteca padrão em algum lugar para que eu possa importá-las?

Aaron Novstrup
fonte

Respostas:

142

Bem, no caso específico de Any*, este truque abaixo não funcionará, pois não aceitará tipos mistos. No entanto, como os tipos mistos também não funcionariam com sobrecarga, pode ser isso que você deseja.

Primeiro, declare uma classe com os tipos que você deseja aceitar como abaixo:

class StringOrInt[T]
object StringOrInt {
  implicit object IntWitness extends StringOrInt[Int]
  implicit object StringWitness extends StringOrInt[String]
}

Em seguida, declare fooassim:

object Bar {
  def foo[T: StringOrInt](x: T) = x match {
    case _: String => println("str")
    case _: Int => println("int")
  }
}

E é isso. Você pode ligar foo(5)ou foo("abc"), e funcionará, mas tente foo(true)e falhará. Isso pode ser evitado pelo código do cliente, criando uma StringOrInt[Boolean], a menos que, como observado por Randall abaixo, você faça StringOrIntuma sealedclasse.

Funciona porque T: StringOrIntsignifica que há um parâmetro implícito do tipo StringOrInt[T]e porque o Scala examina os objetos complementares de um tipo para ver se existem implícitos para fazer o código solicitar esse tipo de trabalho.

Daniel C. Sobral
fonte
14
Se class StringOrInt[T]for feito sealed, o "vazamento" ao qual você se referiu ("Claro, isso pode ser evitado pelo código do cliente criando um StringOrInt[Boolean]") é conectado, pelo menos se ele StringOrIntresidir em um arquivo próprio. Então os objetos testemunha devem ser definidos na mesma fonte que StringOrInt.
Randall Schulz
3
Tentei generalizar essa solução um pouco (postado como resposta abaixo). A principal desvantagem em comparação com a Eitherabordagem parece ser que perdemos muito suporte do compilador para verificar a correspondência.
Aaron Novstrup
belo truque! No entanto, mesmo com a classe selada, você ainda pode contorná-la no código do cliente, definindo um valor implícito b = new StringOrInt [Boolean] no escopo com foo ou chamando explicitamente foo (2.9) (new StringOrInt [Double]). Eu acho que você precisa tornar a classe abstrata também.
Paolo Falabella
2
Sim; provavelmente seria melhor usartrait StringOrInt ...
Mecânica caracol
7
Ps se você quiser subtipos de apoio simplesmente mudar StringOrInt[T]para StringOrInt[-T](ver stackoverflow.com/questions/24387701/... )
Eran Medan
178

Miles Sabin descreve uma maneira muito legal de obter o tipo de união em sua recente postagem no blog Tipos de união sem caixa em Scala através do isomorfismo Curry-Howard :

Ele primeiro define negação de tipos como

type ¬[A] = A => Nothing

usando a lei de De Morgan, isso permite que ele defina tipos de sindicatos

type[T, U] = ¬[¬[T] with ¬[U]]

Com as seguintes construções auxiliares

type ¬¬[A] = ¬[¬[A]]
type ||[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

você pode escrever tipos de união da seguinte maneira:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
fonte
13
Essa é uma das coisas mais impressionantes que já vi.
Submonoid 23/11
18
Aqui está minha implementação estendida da ideia de Miles: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - com exemplos: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ teste /…
Connor Doyle
6
O comentário acima deve ser uma resposta por si só. É apenas uma implementação da idéia de Miles, mas muito bem embrulhada em um pacote no Maven Central, e sem todos os símbolos unicode que podem (?) Representar um problema para algo em um processo de compilação em algum lugar.
Jim Pivarski
2
Esse personagem engraçado é uma negação booleana .
Michid #
1
Inicialmente, a ideia parecia complicada demais para mim. Lendo quase todos os links mencionados neste tópico, fiquei impressionado com a idéia e a beleza de sua implementação :-) ... mas ainda sinto que isso é algo complicado ... agora apenas porque ainda não está disponível diretamente longe de Scala. Como Miles diz: "Agora só precisamos incomodar Martin e Adriaan para torná-lo diretamente acessível".
Richard Gomes
44

Dotty , um novo compilador Scala experimental, suporta tipos de união (escritos A | B), para que você possa fazer exatamente o que deseja:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Samuel Gruetter
fonte
1
Um destes dias.
Michael Ahlers
5
A propósito, Dotty será o novo scala 3 (foi anunciado há alguns meses).
usar o seguinte código
1
e estará disponível em algum lugar no final de 2020
JulienD 14/08/19
31

Aqui está a maneira de Rex Kerr de codificar tipos de união. Direto e simples!

scala> def f[A](a: A)(implicit ev: (Int with String) <:< A) = a match {
     |   case i: Int => i + 1
     |   case s: String => s.length
     | }
f: [A](a: A)(implicit ev: <:<[Int with String,A])Int

scala> f(3)
res0: Int = 4

scala> f("hello")
res1: Int = 5

scala> f(9.2)
<console>:9: error: Cannot prove that Int with String <:< Double.
       f(9.2)
        ^

Fonte: Comentário nº 27 deste excelente post de Miles Sabin, que fornece outra maneira de codificar tipos de união no Scala.

desaparecido
fonte
6
Infelizmente, essa codificação pode ser derrotada: scala> f(9.2: AnyVal)passa no verificador de letras.
Kipton Barros
@Kipton: Isso é triste. A codificação de Miles Sabin também sofre com esse problema?
missingfaktor
9
Existe uma versão um pouco mais simples do código de Miles; como ele está realmente usando a implicação inversa do parâmetro contravariante da função, e não um estrito "não", você pode usar trait Contra[-A] {}no lugar de todas as funções para nada. Então você obtém coisas como type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }usadas como def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(sem unicode chique).
Rex Kerr
Isso pode resolver o problema de herança dos tipos de união? stackoverflow.com/questions/45255270/…
jhegedus
Hmm, eu tentei, eu não posso criar tipos de retorno com este codificações, de modo que não parece ser possível implementar subtipagem stackoverflow.com/questions/45255270/...
jhegedus
18

É possível generalizar a solução de Daniel da seguinte maneira:

sealed trait Or[A, B]

object Or {
   implicit def a2Or[A,B](a: A) = new Or[A, B] {}
   implicit def b2Or[A,B](b: B) = new Or[A, B] {}
}

object Bar {
   def foo[T <% String Or Int](x: T) = x match {
     case _: String => println("str")
     case _: Int => println("int")
   }
}

As principais desvantagens dessa abordagem são

  • Como Daniel apontou, ele não trata de coleções / varargs com tipos mistos
  • O compilador não emite um aviso se a correspondência não for exaustiva
  • O compilador não emite um erro se a correspondência incluir um caso impossível
  • Como a Eitherabordagem, ainda generalização exigiria a definição análoga Or3, Or4etc. traços. Obviamente, definir essas características seria muito mais simples do que definir as Eitherclasses correspondentes .

Atualizar:

Mitch Blevins demonstra uma abordagem muito semelhante e mostra como generalizá-la para mais de dois tipos, apelidando-a de "gagueira ou".

Aaron Novstrup
fonte
18

Eu meio que me deparei com uma implementação relativamente limpa de tipos de união n-ária, combinando a noção de listas de tipos com uma simplificação do trabalho de Miles Sabin nessa área , que alguém menciona em outra resposta.

Dado tipo ¬[-A]que é contrário A, por definição dada A <: B, podemos escrever ¬[B] <: ¬[A], invertendo a ordem dos tipos.

Tipos de dados A, Be X, queremos expressar X <: A || X <: B. Aplicando contravariância, conseguimos ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Este por sua vez pode ser expressa como ¬[A] with ¬[B] <: ¬[X]em que um dos Aou Bdeve ser um supertipo Xou Xem si (pensar em argumentos de função).

object Union {
  import scala.language.higherKinds

  sealed trait ¬[-A]

  sealed trait TSet {
    type Compound[A]
    type Map[F[_]] <: TSet
  }

  sealed traitextends TSet {
    type Compound[A] = A
    type Map[F[_]] =}

  // Note that this type is left-associative for the sake of concision.
  sealed trait[T <: TSet, H] extends TSet {
    // Given a type of the form `∅ ∨ A ∨ B ∨ ...` and parameter `X`, we want to produce the type
    // `¬[A] with ¬[B] with ... <:< ¬[X]`.
    type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

    // This could be generalized as a fold, but for concision we leave it as is.
    type Compound[A] = T#Compound[H with A]

    type Map[F[_]] = T#Map[F] ∨ F[H]
  }

  def foo[A : (∅ ∨ StringIntList[Int])#Member](a: A): String = a match {
    case s: String => "String"
    case i: Int => "Int"
    case l: List[_] => "List[Int]"
  }

  foo(42)
  foo("bar")
  foo(List(1, 2, 3))
  foo(42d) // error
  foo[Any](???) // error
}

Passei algum tempo tentando combinar essa idéia com um limite superior nos tipos de membros, como visto nos TLists de harrah / up , no entanto, a implementação dos Maplimites com tipo até agora se mostrou desafiadora.

J Cracknell
fonte
1
Isso é brilhante, obrigado! Eu tentei as abordagens anteriores, mas continuava tendo problemas usando isso com tipos genéricos como parte da união. Essa foi a única implementação que eu consegui trabalhar com tipos genéricos.
precisa
Infelizmente, mas provavelmente é de se esperar, quando tento usar um método Scala que utiliza um tipo de união do código Java, ele não funciona. Erro: (40, 29) java: o método setValue na classe Config não pode ser aplicado a determinados tipos; necessário: X, scala.Predef. $ less $ colon $ less <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> encontrado: java.lang.String motivo: não é possível inferir tipo-variável (s) X (lista de argumentos reais e formais diferem em comprimento)
Samer Adra
Ainda não está muito claro alguns detalhes nesta implementação. Por exemplo, o artigo original definiu negação como "tipo ¬ [A] = A => Nada", mas nesta versão se apenas tiver "característica selada ¬ [-A]" e a característica não for estendida em nenhum lugar. Como é que isso funciona?
precisa
@Samer Adra Funcionaria de qualquer maneira, o artigo usa Function1como um tipo contravariante existente. Você não precisa de uma implementação, tudo que você precisa é evidência de conformidade ( <:<).
J Cracknell
Alguma idéia de como ter um construtor que aceite um tipo de união?
Samer Adra
13

Uma solução de classe de tipo é provavelmente o melhor caminho a percorrer aqui, usando implícitos. Isso é semelhante à abordagem monóide mencionada no livro Odersky / Spoon / Venners:

abstract class NameOf[T] {
  def get : String
}

implicit object NameOfStr extends NameOf[String] {
  def get = "str"
}

implicit object NameOfInt extends NameOf[Int] {
 def get = "int"
}

def printNameOf[T](t:T)(implicit name : NameOf[T]) = println(name.get)

Se você executar isso no REPL:

scala> printNameOf(1)
int

scala> printNameOf("sss")
str

scala> printNameOf(2.0f)
<console>:10: error: could not find implicit value for parameter nameOf: NameOf[
Float]
       printNameOf(2.0f)

              ^
Kevin Wright
fonte
Eu posso estar errado, mas acho que não é isso que o OP estava procurando. O OP estava perguntando sobre um tipo de dados que poderia representar uma união desatualizada de tipos e, em seguida, faz uma análise de caso em tempo de execução para ver qual era o tipo real. As classes de tipo não resolverão esse problema, pois são uma construção puramente em tempo de compilação.
Tom Crockett
5
A verdadeira pergunta feita foi como expor comportamentos diferentes para tipos diferentes, mas sem sobrecarregar. Sem o conhecimento das classes de tipo (e talvez alguma exposição ao C / C ++), um tipo de união parece ser a única solução. O Eithertipo preexistente de Scala tende a reforçar essa crença. Usar classes de tipo através dos implícitos de Scala é uma solução melhor para o problema subjacente, mas é um conceito relativamente novo e ainda não é amplamente conhecido, e é por isso que o OP nem sabia considerá-las como uma possível alternativa a um tipo de união.
Kevin Wright
isso funciona com subtipagem? stackoverflow.com/questions/45255270/…
jhegedus
10

Gostaríamos de um operador de tipo Or[U,V]que possa ser usado para restringir os parâmetros de um tipo de Xmaneira que seja X <: Uou X <: V. Aqui está uma definição que chega o mais perto possível:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Aqui está como é usado:

// use

class A; class B extends A; class C extends B

def foo[X : (B Or String)#pf] = {}

foo[B]      // OK
foo[C]      // OK
foo[String] // OK
foo[A]      // ERROR!
foo[Number] // ERROR!

Isso usa alguns truques do tipo Scala. O principal é o uso de restrições de tipo generalizado . Dados os tipos Ue V, o compilador Scala fornece uma classe chamada U <:< V(e um objeto implícito dessa classe) se e somente se o compilador Scala puder provar que esse Ué um subtipo de V. Aqui está um exemplo mais simples usando restrições de tipo generalizadas que funcionam em alguns casos:

def foo[X](implicit ev : (B with String) <:< X) = {}

Este exemplo funciona quando Xuma instância da classe B, a String, ou tem um tipo que não é um supertipo nem um subtipo de Bou String. Nos dois primeiros casos, é verdade pela definição da withpalavra - chave que (B with String) <: Be (B with String) <: String, portanto, o Scala fornecerá um objeto implícito que será passado como ev: o compilador do Scala aceitará corretamente foo[B]e foo[String].

No último caso, estou confiando no fato de que se U with V <: X, então U <: Xou V <: X. Parece intuitivamente verdade, e estou simplesmente assumindo isso. Fica claro nessa suposição por que esse exemplo simples falha quando Xé um supertipo ou subtipo de um Bou outro String: por exemplo, no exemplo acima, foo[A]é aceito incorretamente e foo[C]é rejeitado incorretamente. Mais uma vez, o que queremos é algum tipo de tipo de expressão sobre as variáveis U, Ve Xisso é verdade exatamente quando X <: Uou X <: V.

A noção de contravariância de Scala pode ajudar aqui. Lembra da característica trait Inv[-X]? Porque é contravariante em seu parâmetro de tipo X, Inv[X] <: Inv[Y]se e somente se Y <: X. Isso significa que podemos substituir o exemplo acima por um que realmente funcione:

trait Inv[-X]
def foo[X](implicit ev : (Inv[B] with Inv[String]) <:< Inv[X]) = {}

Isso (Inv[U] with Inv[V]) <: Inv[X]ocorre porque a expressão é verdadeira, pela mesma suposição acima, exatamente quando Inv[U] <: Inv[X]ou Inv[V] <: Inv[X], e pela definição de contravariância, isso é verdade exatamente quando X <: Uou X <: V.

É possível tornar as coisas um pouco mais reutilizáveis, declarando um tipo parametrizável BOrString[X]e usando-o da seguinte maneira:

trait Inv[-X]
type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
def foo[X](implicit ev : BOrString[X]) = {}

Scala vai agora tentar construir o tipo BOrString[X]para cada Xque fooé chamado com eo tipo será construído precisamente quando Xé um subtipo de qualquer Bou String. Isso funciona e existe uma notação abreviada. A sintaxe abaixo é equivalente (exceto que evagora deve ser referenciada no corpo do método implicitly[BOrString[X]]e não simplesmente ev) e usada BOrStringcomo um contexto de tipo ligado :

def foo[X : BOrString] = {}

O que realmente queremos é uma maneira flexível de criar um contexto de tipo vinculado. Um contexto de tipo deve ser um tipo parametrizável e queremos uma maneira parametrizável de criar um. Parece que estamos tentando organizar funções em tipos, assim como organizamos funções em valores. Em outras palavras, gostaríamos de algo como o seguinte:

type Or[U,T][X] = (Inv[U] with Inv[T]) <:< Inv[X]

Isso não é diretamente possível no Scala, mas há um truque que podemos usar para chegar bem perto. Isso nos leva à definição Oracima:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Aqui usamos tipagem estrutural e o operador de libra de Scala para criar um tipo estrutural Or[U,T]que é garantido para ter um tipo interno. Este é um animal estranho. Para fornecer algum contexto, a função def bar[X <: { type Y = Int }](x : X) = {}deve ser chamada com subclasses AnyRefque possuem um tipo Ydefinido nelas:

bar(new AnyRef{ type Y = Int }) // works!

O uso do operador pound nos permite fazer referência ao tipo interno Or[B, String]#pfe, usando a notação infix para o operador type Or, chegamos à nossa definição original de foo:

def foo[X : (B Or String)#pf] = {}

Podemos usar o fato de que os tipos de função são contrários ao primeiro parâmetro de tipo, a fim de evitar a definição da característica Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
Josh
fonte
Isso pode resolver o A|B <: A|B|Cproblema? stackoverflow.com/questions/45255270/… não sei dizer.
22417 jhegedus
7

Você pode dar uma olhada no MetaScala , que tem algo chamado OneOf. Tenho a impressão de que isso não funciona bem com matchinstruções, mas que você pode simular a correspondência usando funções de ordem superior. Dê uma olhada neste trecho , por exemplo, mas observe que a parte "correspondência simulada" está comentada, talvez porque ainda não funcione.

Agora, para alguns editorialistas: eu não acho que exista algo de estranho em definir Either3, Either4, etc., como você descreve. Isso é essencialmente duplo para os 22 tipos de tupla padrão incorporados ao Scala. Certamente seria bom se Scala tivesse tipos disjuntivos embutidos, e talvez alguma sintaxe agradável para eles {x, y, z}.

Tom Crockett
fonte
6

Eu estou pensando que o tipo disjuntivo de primeira classe é um supertipo selado, com os subtipos alternativos e conversões implícitas de / para os tipos desejados da disjunção para esses subtipos alternativos.

Suponho que isso aborda os comentários 33 a 36 da solução de Miles Sabin, portanto, o tipo de primeira classe que pode ser empregado no site de uso, mas não o testei.

sealed trait IntOrString
case class IntOfIntOrString( v:Int ) extends IntOrString
case class StringOfIntOrString( v:String ) extends IntOrString
implicit def IntToIntOfIntOrString( v:Int ) = new IntOfIntOrString(v)
implicit def StringToStringOfIntOrString( v:String ) = new StringOfIntOrString(v)

object Int {
   def unapply( t : IntOrString ) : Option[Int] = t match {
      case v : IntOfIntOrString => Some( v.v )
      case _ => None
   }
}

object String {
   def unapply( t : IntOrString ) : Option[String] = t match {
      case v : StringOfIntOrString => Some( v.v )
      case _ => None
   }
}

def size( t : IntOrString ) = t match {
    case Int(i) => i
    case String(s) => s.length
}

scala> size("test")
res0: Int = 4
scala> size(2)
res1: Int = 2

Um problema é que o Scala não empregará no contexto de correspondência de caso, uma conversão implícita de IntOfIntOrStringpara Int(e StringOfIntOrStringpara String), portanto, deve definir extratores e usá- case Int(i)los em vez de case i : Int.


ADD: Respondi a Miles Sabin em seu blog da seguinte maneira. Talvez haja várias melhorias em relação a:

  1. Ele se estende a mais de 2 tipos, sem nenhum ruído adicional no site de uso ou definição.
  2. Os argumentos estão em caixas implicitamente, por exemplo, não precisa size(Left(2))ou size(Right("test")).
  3. A sintaxe da correspondência de padrões é implicitamente desmarcada.
  4. O boxe e o unboxing podem ser otimizados pelo hotspot da JVM.
  5. A sintaxe pode ser a adotada por um futuro tipo de união de primeira classe; portanto, a migração talvez seja perfeita? Talvez para o nome do tipo de união, seria melhor usar em Vvez de Or, por exemplo IntVString, ` Int |v| String`, ` Int or String` ou meu favorito ` Int|String`?

ATUALIZAÇÃO: A negação lógica da disjunção para o padrão acima segue e eu adicionei um padrão alternativo (e provavelmente mais útil) no blog de Miles Sabin .

sealed trait `Int or String`
sealed trait `not an Int or String`
sealed trait `Int|String`[T,E]
case class `IntOf(Int|String)`( v:Int ) extends `Int|String`[Int,`Int or String`]
case class `StringOf(Int|String)`( v:String ) extends `Int|String`[String,`Int or String`]
case class `NotAn(Int|String)`[T]( v:T ) extends `Int|String`[T,`not an Int or String`]
implicit def `IntTo(IntOf(Int|String))`( v:Int ) = new `IntOf(Int|String)`(v)
implicit def `StringTo(StringOf(Int|String))`( v:String ) = new `StringOf(Int|String)`(v)
implicit def `AnyTo(NotAn(Int|String))`[T]( v:T ) = new `NotAn(Int|String)`[T](v)
def disjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `Int or String`) = x
def negationOfDisjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `not an Int or String`) = x

scala> disjunction(5)
res0: Int|String[Int,Int or String] = IntOf(Int|String)(5)

scala> disjunction("")
res1: Int|String[String,Int or String] = StringOf(Int|String)()

scala> disjunction(5.0)
error: could not find implicit value for parameter ev: =:=[not an Int or String,Int or String]
       disjunction(5.0)
                  ^

scala> negationOfDisjunction(5)
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction(5)
                            ^

scala> negationOfDisjunction("")
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction("")
                            ^
scala> negationOfDisjunction(5.0)
res5: Int|String[Double,not an Int or String] = NotAn(Int|String)(5.0)

OUTRA ATUALIZAÇÃO: Em relação aos comentários 23 e 35 da solução da Mile Sabin , aqui está uma maneira de declarar um tipo de união no site de uso. Observe que ele está fora da caixa após o primeiro nível, ou seja, tem a vantagem de ser extensível a qualquer número de tipos na disjunção , ao passo que Eitherprecisa de box aninhado e o paradigma no meu comentário anterior 41 não era extensível. Em outras palavras, a D[Int ∨ String]é atribuível a (ou seja, é um subtipo de) a D[Int ∨ String ∨ Double].

type ¬[A] = (() => A) => A
type[T, U] = ¬[T] with ¬[U]
class D[-A](v: A) {
  def get[T](f: (() => T)) = v match {
    case x : ¬[T] => x(f)
  }
}
def size(t: D[IntString]) = t match {
  case x: D[¬[Int]] => x.get( () => 0 )
  case x: D[¬[String]] => x.get( () => "" )
  case x: D[¬[Double]] => x.get( () => 0.0 )
}
implicit def neg[A](x: A) = new D[¬[A]]( (f: (() => A)) => x )

scala> size(5)
res0: Any = 5

scala> size("")
error: type mismatch;
 found   : java.lang.String("")
 required: D[?[Int,String]]
       size("")
            ^

scala> size("hi" : D[¬[String]])
res2: Any = hi

scala> size(5.0 : D[¬[Double]])
error: type mismatch;
 found   : D[(() => Double) => Double]
 required: D[?[Int,String]]
       size(5.0 : D[?[Double]])
                ^

Aparentemente, o compilador Scala tem três erros.

  1. Ele não escolherá a função implícita correta para nenhum tipo após o primeiro tipo na disjunção de destino.
  2. Não exclui o D[¬[Double]]caso da partida.

3)

scala> class D[-A](v: A) {
  def get[T](f: (() => T))(implicit e: A <:< ¬[T]) = v match {
    case x : ¬[T] => x(f)
  }
}
error: contravariant type A occurs in covariant position in
       type <:<[A,(() => T) => T] of value e
         def get[T](f: (() => T))(implicit e: A <:< ?[T]) = v match {
                                           ^

O método get não é restrito corretamente no tipo de entrada, porque o compilador não permitirá Ana posição covariante. Pode-se argumentar que é um bug, porque tudo o que queremos é evidência, nunca acessamos a evidência na função. E fiz a escolha de não testar case _no getmétodo, para não ter que desmarcar um Optionno matchin size().


5 de março de 2012: a atualização anterior precisa de uma melhoria. A solução de Miles Sabin funcionou corretamente com subtipagem.

type ¬[A] = A => Nothing
type[T, U] = ¬[T] with ¬[U]
class Super
class Sub extends Super

scala> implicitly[(SuperString) <:< ¬[Super]]
res0: <:<[?[Super,String],(Super) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Sub]]
res2: <:<[?[Super,String],(Sub) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Any]]
error: could not find implicit value for parameter
       e: <:<[?[Super,String],(Any) => Nothing]
       implicitly[(Super ? String) <:< ?[Any]]
                 ^

A proposta da minha atualização anterior (para o tipo de união quase de primeira classe) quebrou a subtipagem.

 scala> implicitly[D[¬[Sub]] <:< D[(SuperString)]]
error: could not find implicit value for parameter
       e: <:<[D[(() => Sub) => Sub],D[?[Super,String]]]
       implicitly[D[?[Sub]] <:< D[(Super ? String)]]
                 ^

O problema é que Aem (() => A) => Aaparece em ambos os co-variante (tipo de retorno) e contravariante (entrada de função, ou, neste caso, um valor de retorno da função que é uma função de entrada) posições, assim substituições só pode ser invariante.

Observe que isso A => Nothingé necessário apenas porque queremos Ana posição contravariante, para que os supertipos de A não sejam subtipos de D[¬[A]]nem D[¬[A] with ¬[U]]( veja também ). Como precisamos apenas de contravariância dupla, podemos obter o equivalente à solução de Miles, mesmo que possamos descartar o ¬e .

trait D[-A]

scala> implicitly[D[D[Super]] <:< D[D[Super] with D[String]]]
res0: <:<[D[D[Super]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Sub]] <:< D[D[Super] with D[String]]]
res1: <:<[D[D[Sub]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
error: could not find implicit value for parameter
       e: <:<[D[D[Any]],D[D[Super] with D[String]]]
       implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
                 ^

Portanto, a correção completa é.

class D[-A] (v: A) {
  def get[T <: A] = v match {
    case x: T => x
  }
}

implicit def neg[A](x: A) = new D[D[A]]( new D[A](x) )

def size(t: D[D[Int] with D[String]]) = t match {
  case x: D[D[Int]] => x.get[D[Int]].get[Int]
  case x: D[D[String]] => x.get[D[String]].get[String]
  case x: D[D[Double]] => x.get[D[Double]].get[Double]
}

Observe que os 2 erros anteriores no Scala permanecem, mas o terceiro é evitado, pois Tagora está restrito ao subtipo de A.

Podemos confirmar os trabalhos de subtipagem.

def size(t: D[D[Super] with D[String]]) = t match {
  case x: D[D[Super]] => x.get[D[Super]].get[Super]
  case x: D[D[String]] => x.get[D[String]].get[String]
}

scala> size( new Super )
res7: Any = Super@1272e52

scala> size( new Sub )
res8: Any = Sub@1d941d7

Eu tenho pensado que os tipos de interseção de primeira classe são muito importantes, tanto pelas razões que o Ceilão os possui , como porque, em vez de se dedicar a Anyisso, unboxing com matchtipos esperados pode gerar um erro de tempo de execução, o unboxing de uma coleção heterogênea contendo a) a disjunção pode ser verificada do tipo (Scala precisa corrigir os bugs que observei). Os sindicatos são mais simples do que a complexidade do uso experimental hList de metascala para coleções heterogêneas.

Shelby Moore III
fonte
O item nº 3 acima não é um bug no compilador Scala . Note que eu originalmente não o havia numerado como um bug, então fiz uma edição descuidada hoje e o fiz (esquecendo minha razão original de não afirmar que era um bug). Não editei a postagem novamente, porque estou no limite de 7 edições.
Shelby Moore III
O bug nº 1 acima pode ser evitado com uma formulação diferente da sizefunção .
Shelby Moore III
O item # 2 não é um bug. Scala não pode expressar completamente um tipo de união . O documento vinculado fornece outra versão do código, para que sizenão aceite mais D[Any]como entrada.
Shelby Moore III
Eu não começ completamente esta resposta, é isso também uma resposta a esta pergunta: stackoverflow.com/questions/45255270/...
jhegedus
5

Existe outra maneira que é um pouco mais fácil de entender se você não gosta de Curry-Howard:

type v[A,B] = Either[Option[A], Option[B]]

private def L[A,B](a: A): v[A,B] = Left(Some(a))
private def R[A,B](b: B): v[A,B] = Right(Some(b))  
// TODO: for more use scala macro to generate this for up to 22 types?
implicit def a2[A,B](a: A): v[A,B] = L(a)
implicit def b2[A,B](b: B): v[A,B] = R(b)
implicit def a3[A,B,C](a: A): v[v[A,B],C] = L(a2(a))
implicit def b3[A,B,C](b: B): v[v[A,B],C] = L(b2(b))
implicit def a4[A,B,C,D](a: A): v[v[v[A,B],C],D] = L(a3(a))
implicit def b4[A,B,C,D](b: B): v[v[v[A,B],C],D] = L(b3(b))    
implicit def a5[A,B,C,D,E](a: A): v[v[v[v[A,B],C],D],E] = L(a4(a))
implicit def b5[A,B,C,D,E](b: B): v[v[v[v[A,B],C],D],E] = L(b4(b))

type JsonPrimtives = (String v Int v Double)
type ValidJsonPrimitive[A] = A => JsonPrimtives

def test[A : ValidJsonPrimitive](x: A): A = x 

test("hi")
test(9)
// test(true)   // does not compile

Eu uso técnica semelhante em dijon

pathikrit
fonte
Isso pode funcionar com subtipagem? Meu instinto: não, mas posso estar errado. stackoverflow.com/questions/45255270/…
jhegedus
1

Bem, isso é tudo muito inteligente, mas eu tenho certeza que você já sabe que as respostas para suas perguntas principais são várias variedades de "Não". O Scala lida com a sobrecarga de maneira diferente e, é preciso admitir, um pouco menos elegante do que você descreve. Parte disso é devido à interoperabilidade do Java, parte do fato de não querer atingir casos extremos do algoritmo de inferência de tipo, e parte do fato de simplesmente não ser Haskell.

Dave Griffith
fonte
5
Enquanto uso Scala há algum tempo, não sou tão experiente nem inteligente quanto você parece pensar. Neste exemplo, posso ver como uma biblioteca poderia fornecer a solução. Faz sentido então se perguntar se existe uma biblioteca (ou alguma alternativa).
Aaron Novstrup
1

Adicionando as ótimas respostas aqui. Aqui está uma essência que se baseia nos tipos de união de Miles Sabin (e nas idéias de Josh), mas também os define recursivamente, para que você possa ter> 2 tipos na união ( def foo[A : UNil Or Int Or String Or List[String])

https://gist.github.com/aishfenton/2bb3bfa12e0321acfc904a71dda9bfbb

NB: Devo acrescentar que, depois de brincar com o exposto acima, acabei voltando aos tipos de soma antiga simples (ou seja, característica selada com subclasses). Os tipos de união Miles Sabin são ótimos para restringir o parâmetro type, mas se você precisar retornar um tipo de união, ele não oferecerá muito.

Aish
fonte
Isso pode resolver o A|C <: A|B|Cproblema de subtipagem? stackoverflow.com/questions/45255270/… Meu instinto não, porque então isso significaria que A or Cprecisaria ser o subtipo de (A or B) or Cmas não contém o tipo, A or Cportanto não há esperança em criar A or Cum subtipo A or B or Ccom essa codificação pelo menos .. . O que você acha ?
Jhegedus
0

Dos documentos , com a adição de sealed:

sealed class Expr
case class Var   (x: String)          extends Expr
case class Apply (f: Expr, e: Expr)   extends Expr
case class Lambda(x: String, e: Expr) extends Expr

Em relação à sealedpeça:

É possível definir outras classes de casos que estendem o tipo Expr em outras partes do programa (...). Esta forma de extensibilidade pode ser excluída declarando a classe base Expr selada; nesse caso, todas as classes que estendem diretamente o Expr devem estar no mesmo arquivo de origem que o Expr.

Elazar
fonte