scala - para - ¿Las listas H no son más que una forma complicada de escribir tuplas?
poligono en r (4)
Estoy realmente interesado en descubrir dónde están las diferencias y, en términos más generales, identificar casos de uso canónico en los que las listas H no pueden usarse (o más bien, no producen ningún beneficio sobre las listas regulares).
(Soy consciente de que hay 22 (creo) TupleN
en Scala, mientras que uno solo necesita una sola HList, pero ese no es el tipo de diferencia conceptual que me interesa).
He marcado un par de preguntas en el siguiente texto. Puede que en realidad no sea necesario responderlas, están más destinadas a señalar cosas que no están claras para mí, y a guiar la discusión en ciertas direcciones.
Motivación
Recientemente, he visto un par de respuestas en SO en las que las personas sugirieron utilizar HLists (por ejemplo, tal como lo proporciona Shapeless ), incluida una respuesta eliminada a esta pregunta . Dio lugar a esta discusión , que a su vez provocó esta pregunta.
Introducción
Me parece que las listas solo son útiles cuando conoces la cantidad de elementos y sus tipos precisos estáticamente. El número en realidad no es crucial, pero parece poco probable que alguna vez necesite generar una lista con elementos de tipos variables pero conocidos de forma estática, pero que no sepa estáticamente su número. Pregunta 1: ¿Podrías incluso escribir un ejemplo como, por ejemplo, en un bucle? Mi intuición es que tener una lista h estadísticamente precisa con un número estáticamente desconocido de elementos arbitrarios (arbitrario en relación con una jerarquía de clases determinada) simplemente no es compatible.
HLists vs. Tuples
Si esto es cierto, es decir, usted sabe estáticamente el número y el tipo - Pregunta 2: ¿por qué no usar simplemente una n-tupla? Claro, puedes escribir fácilmente un mapa y doblar sobre una HList (que también puedes, pero no de forma segura, hacer más de una tupla con la ayuda de productIterator
), pero dado que el número y tipo de elementos son estáticamente conocidos, probablemente puedas acceder a la tupla elementos directamente y realizar las operaciones.
Por otro lado, si la función f
mapea sobre una hlist es tan genérica que acepta todos los elementos - Pregunta 3: ¿por qué no usarla a través de productIterator.map
? De acuerdo, una diferencia interesante podría provenir de la sobrecarga de métodos: si tuviéramos varias f
sobrecargadas, tener la información de tipo más fuerte provista por la lista (a diferencia del productIterator) podría permitir al compilador elegir una f
más específica. Sin embargo, no estoy seguro de si realmente funcionaría en Scala, ya que los métodos y las funciones no son lo mismo.
HListas y entrada del usuario
Partiendo de la misma suposición, es decir, que necesita conocer el número y los tipos de los elementos de forma estática. Pregunta 4: ¿ se pueden usar hlists en situaciones en las que los elementos dependen de cualquier tipo de interacción del usuario? Por ejemplo, imagina poblar una hlist con elementos dentro de un bucle; los elementos se leen desde algún lugar (UI, archivo de configuración, interacción del actor, red) hasta que se cumple una determinada condición. ¿Cuál sería el tipo de hlist? Similar para una especificación de interfaz getElements: HList [...] que debería funcionar con listas de longitud estáticamente desconocida, y que permite que el componente A en un sistema obtenga dicha lista de elementos arbitrarios del componente B.
Abordando las preguntas una a tres: una de las principales aplicaciones para HLists
es abstraer sobre arity. Arity es típicamente estáticamente conocido en cualquier sitio de uso dado de una abstracción, pero varía de un sitio a otro. Toma esto, de los examples amorfos,
def flatten[T <: Product, L <: HList](t : T)
(implicit hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =
flatten(hl(t))
val t1 = (1, ((2, 3), 4))
val f1 = flatten(t1) // Inferred type is Int :: Int :: Int :: Int :: HNil
val l1 = f1.toList // Inferred type is List[Int]
val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
// Inferred type of t2b is (Int, Boolean, Double, String, String, Int, Boolean)
Sin usar HLists
(o algo equivalente) para abstraer la ariadidad de los argumentos de tupla para flatten
sería imposible tener una sola implementación que pudiera aceptar argumentos de estas dos formas muy diferentes y transformarlas de una manera segura.
Es probable que la capacidad de abstraer sobre ariedad sea de interés en cualquier lugar donde estén involucradas las arites fijas: así como las tuplas, como se mencionó anteriormente, que incluye listas de parámetros de métodos / funciones y clases de casos. Vea here ejemplos de cómo podríamos abstraer sobre la cantidad de clases de casos arbitrarios para obtener instancias de clase de tipo casi automáticamente
// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)
// Publish their `HListIso`''s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)
// And now they''re monoids ...
implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))
implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))
No hay iteración de tiempo de ejecución aquí, pero hay duplicación , que el uso de HLists
(o estructuras equivalentes) puede eliminar. Por supuesto, si su tolerancia para la repetición repetitiva es alta, puede obtener el mismo resultado escribiendo múltiples implementaciones para cada forma que le interese.
En la pregunta tres, usted pregunta "... si la función f que mapea sobre una hlist es tan genérica que acepta todos los elementos ... ¿por qué no usarla a través de productIterator.map?". Si la función que mapea sobre una HList es realmente de la forma Any => T
, el mapeo sobre productIterator
le servirá perfectamente. Pero las funciones de la forma Any => T
no suelen ser tan interesantes (al menos, no lo son a menos que escriban el molde internamente). sin forma proporciona una forma de valor de función polimórfica que permite al compilador seleccionar casos específicos del tipo exactamente de la forma en que tiene dudas. Por ejemplo,
// size is a function from values of arbitrary type to a ''size'' which is
// defined via type specific cases
object size extends Poly1 {
implicit def default[T] = at[T](t => 1)
implicit def caseString = at[String](_.length)
implicit def caseList[T] = at[List[T]](_.length)
}
scala> val l = 23 :: "foo" :: List(''a'', ''b'') :: true :: HNil
l: Int :: String :: List[Char] :: Boolean :: HNil =
23 :: foo :: List(a, b) :: true :: HNil
scala> (l map size).toList
res1: List[Int] = List(1, 3, 2, 1)
Con respecto a su pregunta cuatro, sobre la entrada del usuario, hay dos casos a considerar. El primero es situaciones donde podemos establecer dinámicamente un contexto que garantice que se obtenga una condición estática conocida. En este tipo de escenarios, es perfectamente posible aplicar técnicas sin forma, pero claramente con la condición de que si la condición estática no se obtiene en tiempo de ejecución, entonces tenemos que seguir un camino alternativo. Como era de esperar, esto significa que los métodos que son sensibles a las condiciones dinámicas tienen que arrojar resultados opcionales. Aquí hay un ejemplo usando HList
s,
trait Fruit
case class Apple() extends Fruit
case class Pear() extends Fruit
type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil
val a : Apple = Apple()
val p : Pear = Pear()
val l = List(a, p, a, p) // Inferred type is List[Fruit]
El tipo de l
no captura la longitud de la lista ni los tipos precisos de sus elementos. Sin embargo, si esperamos que tenga una forma específica (es decir, si debe ajustarse a algún esquema conocido conocido), entonces podemos intentar establecer ese hecho y actuar en consecuencia.
scala> import Traversables._
import Traversables._
scala> val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]
res0: Option[Apple :: Pear :: Apple :: Pear :: HNil] =
Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)
scala> apap.map(_.tail.head)
res1: Option[Pear] = Some(Pear())
Hay otras situaciones en las que no nos importa la longitud real de una lista determinada, aparte de que tiene la misma longitud que otra lista. De nuevo, esto es algo que admite informes, tanto de manera estática como también en un contexto mixto / estático como el anterior. Vea here para un ejemplo extendido.
Es cierto, como usted observa, que todos estos mecanismos requieren información de tipo estático para estar disponible, al menos condicionalmente, y eso parecería excluir estas técnicas de ser utilizables en un entorno completamente dinámico, totalmente impulsado por datos sin tipo provistos externamente. Pero con el advenimiento de la compilación en tiempo de ejecución como un componente de la reflexión de Scala en 2.10, incluso esto ya no es un obstáculo insuperable ... podemos usar la compilación de tiempo de ejecución para proporcionar una forma de puesta en escena liviana y hacer nuestro tipado estático en tiempo de ejecución en respuesta a los datos dinámicos: extracto de la tabla anterior a continuación ... siga el enlace para ver el ejemplo completo,
val t1 : (Any, Any) = (23, "foo") // Specific element types erased
val t2 : (Any, Any) = (true, 2.0) // Specific element types erased
// Type class instances selected on static type at runtime!
val c1 = stagedConsumeTuple(t1) // Uses intString instance
assert(c1 == "23foo")
val c2 = stagedConsumeTuple(t2) // Uses booleanDouble instance
assert(c2 == "+2.0")
Estoy seguro de que @PLT_Borat tendrá algo que decir al respecto, dados sus sabios comentarios sobre los lenguajes de programación de @PLT_Borat dependientes ;-)
Hay muchas cosas que no puedes hacer (bien) con tuplas:
- escribir una función genérica preceder / agregar
- escribe una función inversa
- escribe una función concat
- ...
Puede hacer todo eso con tuplas, por supuesto, pero no en el caso general. Entonces, usar HLists hace que tu código sea más SECO.
Para que quede claro, una HList no es más que una pila de Tuple2
con azúcar ligeramente diferente en la parte superior.
def hcons[A,B](head : A, tail : B) = (a,b)
def hnil = Unit
hcons("foo", hcons(3, hnil)) : (String, (Int, Unit))
Así que su pregunta es esencialmente sobre las diferencias entre el uso de tuplas anidadas vs tuplas planas, pero las dos son isomórficas, así que al final no hay diferencia, excepto la conveniencia en que se pueden usar las funciones de la biblioteca y qué notación se puede usar.
Puedo explicar esto en un lenguaje súper simple:
La nomenclatura tuple vs list no es significativa. HLists podría llamarse como HTuples. La diferencia es que en Scala + Haskell, puedes hacer esto con una tupla (usando la sintaxis de Scala):
def append2[A,B,C](in: (A,B), v: C) : (A,B,C) = (in._1, in._2, v)
para tomar una tupla de entrada de exactamente dos elementos de cualquier tipo, agregar un tercer elemento y devolver una tupla completamente tipeada con exactamente tres elementos. Pero mientras esto es completamente genérico sobre los tipos, tiene que especificar explícitamente las longitudes de entrada / salida.
Lo que HList de estilo Haskell le permite hacer es ampliar este genérico, de modo que puede anexar a cualquier longitud de tupla / lista y obtener una tupla / lista totalmente estáticamente tipada. Este beneficio también se aplica a las colecciones de tipo homogéneo donde puede agregar un int a una lista de números exactamente y obtener una lista que está estáticamente estátificada para tener exactamente (n + 1) entradas sin especificar explícitamente n.