¿Qué significa "resumen sobre"?


95

A menudo, en la literatura de Scala, encuentro la frase "resumen sobre", pero no entiendo la intención. Por ejemplo , Martin Odersky escribe

Puede pasar métodos (o "funciones") como parámetros, o puede abstraerlos . Puede especificar tipos como parámetros o puede abstraerlos .

Como otro ejemplo, en el documento "Deprecating the Observer Pattern" ,

Una consecuencia de que nuestros flujos de eventos sean valores de primera clase es que podemos abstraerlos .

He leído que los genéricos de primer orden "abstraen sobre tipos", mientras que las mónadas "abstraen sobre constructores de tipos". Y también vemos frases como esta en el papel de Cake Pattern . Para citar uno de los muchos ejemplos:

Los miembros de tipo abstracto proporcionan una forma flexible de abstraer sobre tipos concretos de componentes.

Incluso las preguntas de desbordamiento de pila relevantes utilizan esta terminología. "no se puede abstraer existencialmente sobre el tipo parametrizado ..."

Entonces ... ¿qué significa realmente "resumen sobre"?

Respuestas:


124

En álgebra, como en la formación de conceptos cotidianos, las abstracciones se forman agrupando cosas por algunas características esenciales y omitiendo sus otras características específicas. La abstracción se unifica bajo un solo símbolo o palabra que denota las similitudes. Decimos que abstraemos las diferencias, pero esto realmente significa que nos estamos integrando por las similitudes.

Por ejemplo, considere un programa que toma la suma de los números 1, 2y 3:

val sumOfOneTwoThree = 1 + 2 + 3

Este programa no es muy interesante, ya que no es muy abstracto. Podemos abstraer los números que estamos sumando, integrando todas las listas de números bajo un solo símbolo ns:

def sumOf(ns: List[Int]) = ns.foldLeft(0)(_ + _)

Y tampoco nos importa especialmente que sea una Lista. List es un constructor de tipo específico (toma un tipo y devuelve un tipo), pero podemos abstraer el constructor de tipo especificando qué característica esencial queremos (que se puede plegar):

trait Foldable[F[_]] {
  def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
}

def sumOf[F[_]](ns: F[Int])(implicit ff: Foldable[F]) =
  ff.foldl(ns, 0, (x: Int, y: Int) => x + y)

Y podemos tener Foldableinstancias implícitas para Listy cualquier otra cosa que podamos plegar.

implicit val listFoldable = new Foldable[List] {
  def foldl[A, B](as: List[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}

val sumOfOneTwoThree = sumOf(List(1,2,3))

Además, podemos abstraer tanto la operación como el tipo de operandos:

trait Monoid[M] {
  def zero: M
  def add(m1: M, m2: M): M
}

trait Foldable[F[_]] {
  def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
  def foldMap[A, B](as: F[A], f: A => B)(implicit m: Monoid[B]): B =
    foldl(as, m.zero, (b: B, a: A) => m.add(b, f(a)))
}

def mapReduce[F[_], A, B](as: F[A], f: A => B)
                         (implicit ff: Foldable[F], m: Monoid[B]) =
  ff.foldMap(as, f)

Ahora tenemos algo bastante general. El método mapReducedoblará cualquier F[A]dado que podamos probar que Fes plegable y que Aes un monoide o que se puede mapear en uno. Por ejemplo:

case class Sum(value: Int)
case class Product(value: Int)

implicit val sumMonoid = new Monoid[Sum] {
  def zero = Sum(0)
  def add(a: Sum, b: Sum) = Sum(a.value + b.value)
}

implicit val productMonoid = new Monoid[Product] {
  def zero = Product(1)
  def add(a: Product, b: Product) = Product(a.value * b.value)
}

val sumOf123 = mapReduce(List(1,2,3), Sum)
val productOf456 = mapReduce(List(4,5,6), Product)

Hemos resumido sobre monoides y plegables.


@coubeatczech El código se ejecuta bien en REPL. ¿Qué versión de Scala está usando y qué error obtuvo?
Daniel C. Sobral

1
@Apocalisp Sería interesante si hiciera uno de los dos ejemplos finales ao Setalgún otro tipo plegable. Un ejemplo con una Stringy una concatenación también sería genial.
Daniel C. Sobral

1
Hermosa respuesta, Runar. ¡Gracias! Seguí la sugerencia de Daniel y creé implícitamente setFoldable y concatMonoid, sin alterar mapReduce en absoluto. Estoy en camino de asimilar esto.
Morgan Creighton

6
Me tomó un momento entender que en las últimas 2 líneas aprovechas el hecho de que los objetos complementarios Suma y Producto, debido a que definen aplicar (Int), son tratados como Int => Suma e Int => Producto por Scala compilador. ¡Muy agradable!
Kris Nuttycombe

Buen post :)! En su último ejemplo, la lógica implícita de Monoide parece innecesaria. Esto es más simple: gist.github.com/cvogt/9716490
cvogt

11

En una primera aproximación, ser capaz de "abstraer" algo significa que, en lugar de usar ese algo directamente, puedes convertirlo en un parámetro, o usarlo "anónimamente".

Scala le permite abstraer tipos, permitiendo que las clases, métodos y valores tengan parámetros de tipo y que los valores tengan tipos abstractos (o anónimos).

Scala le permite abstraer acciones, permitiendo que los métodos tengan parámetros de función.

Scala le permite abstraer características, al permitir que los tipos se definan estructuralmente.

Scala le permite abstraer parámetros de tipo, permitiendo parámetros de tipo de orden superior.

Scala le permite abstraer patrones de acceso a datos, permitiéndole crear extractores.

Scala le permite abstraer "cosas que se pueden usar como otra cosa", al permitir conversiones implícitas como parámetros. Haskell hace lo mismo con las clases de tipos.

Scala no le permite (todavía) abstraer clases. No puede pasar una clase a algo y luego usar esa clase para crear nuevos objetos. Otros lenguajes permiten la abstracción sobre clases.

("Mónadas abstractas sobre constructores de tipos" solo es cierto de una manera muy restrictiva. No se obsesione con eso hasta que tenga su momento "¡Ajá! ¡Entiendo las mónadas!").

La capacidad de abstraer algún aspecto de la computación es básicamente lo que permite la reutilización del código y permite la creación de bibliotecas de funcionalidad. Scala permite abstraer muchos más tipos de cosas que los lenguajes más convencionales, y las bibliotecas de Scala pueden ser, en consecuencia, más potentes.


1
Puede pasar a Manifest, o incluso a Class, y usar la reflexión para crear instancias de nuevos objetos de esa clase.
Daniel C. Sobral

6

Una abstracción es una especie de generalización.

http://en.wikipedia.org/wiki/Abstraction

No solo en Scala, sino en muchos lenguajes, existe la necesidad de tener tales mecanismos para reducir la complejidad (o al menos crear una jerarquía que divida la información en partes más fáciles de entender).

Una clase es una abstracción sobre un tipo de datos simple. Es como un tipo básico, pero en realidad los generaliza. Entonces, una clase es más que un simple tipo de datos, pero tiene muchas cosas en común.

Cuando dice "abstraer" se refiere al proceso mediante el cual se generaliza. Entonces, si está abstrayendo métodos como parámetros, está generalizando el proceso de hacerlo. por ejemplo, en lugar de pasar métodos a funciones, podría crear algún tipo de forma generalizada de manejarlo (como no pasar métodos en absoluto, pero construir un sistema especial para manejarlo).

En este caso, se refiere específicamente al proceso de abstraer un problema y crear una solución similar a la del problema. C tiene muy poca capacidad para abstraer (puedes hacerlo, pero se complica muy rápido y el lenguaje no lo admite directamente). Si lo escribió en C ++, podría usar conceptos de oop para reducir la complejidad del problema (bueno, es la misma complejidad pero la conceptualización es generalmente más fácil (al menos una vez que aprende a pensar en términos de abstracciones)).

por ejemplo, si necesitaba un tipo de datos especial que fuera como un int pero, digamos restringido, podría abstraerlo creando un nuevo tipo que podría usarse como un int pero que tuviera las propiedades que necesitaba. El proceso que usaría para hacer tal cosa se llamaría "abstracción".


5

Aquí está mi interpretación limitada de show and tell. Se explica por sí mismo y se ejecuta en REPL.

class Parameterized[T] { // type as a parameter
  def call(func: (Int) => Int) = func(1)  // function as a parameter
  def use(l: Long) { println(l) } // value as a parameter
}

val p = new Parameterized[String] // pass type String as a parameter
p.call((i:Int) => i + 1) // pass function increment as a parameter
p.use(1L) // pass value 1L as a parameter


abstract class Abstracted { 
  type T // abstract over a type
  def call(i: Int): Int // abstract over a function
  val l: Long // abstract over value
  def use() { println(l) }
}

class Concrete extends Abstracted { 
  type T = String // specialize type as String
  def call(i:Int): Int = i + 1 // specialize function as increment function
  val l = 1L // specialize value as 1L
}

val a: Abstracted = new Concrete
a.call(1)
a.use()

1
más o menos la idea de "resumen sobre" en el código, potente pero breve, probará este lenguaje +1
user44298

2

Las otras respuestas ya dan una buena idea de qué tipo de abstracciones existen. Repasemos las citas una por una y proporcionemos un ejemplo:

Puede pasar métodos (o "funciones") como parámetros, o puede abstraerlos. Puede especificar tipos como parámetros o puede abstraerlos.

Pasar función como parámetro: List(1,-2,3).map(math.abs(x))Claramente aquí absse pasa como parámetro. mapen sí mismo se abstrae de una función que hace algo especial con cada elemento de la lista. val list = List[String]()especifica un parámetro de tipo (String). Se podría escribir un tipo de colección que utiliza miembros de tipo abstracto en su lugar: val buffer = Buffer{ type Elem=String }. Una diferencia es que tienes que escribir def f(lis:List[String])...pero def f(buffer:Buffer)..., por lo que el tipo de elemento está "oculto" en el segundo método.

Una consecuencia de que nuestros flujos de eventos sean valores de primera clase es que podemos abstraerlos.

En Swing, un evento simplemente "ocurre" de la nada, y tienes que lidiar con él aquí y ahora. Los flujos de eventos le permiten realizar toda la instalación y el cableado de una manera más declarativa. Por ejemplo, cuando desee cambiar el oyente responsable en Swing, debe anular el registro del antiguo y registrar el nuevo, y conocer todos los detalles sangrientos (por ejemplo, problemas de subprocesos). Con los flujos de eventos, la fuente de los eventos se convierte en algo que puede simplemente pasar, por lo que no es muy diferente de un flujo de bytes o caracteres, por lo tanto, un concepto más "abstracto".

Los miembros de tipo abstracto proporcionan una forma flexible de abstraer sobre tipos concretos de componentes.

La clase Buffer anterior ya es un ejemplo de esto.


0

Las respuestas anteriores brindan una excelente explicación, pero para resumirla en una sola oración, diría:

Abstraer algo es lo mismo que descuidarlo cuando es irrelevante .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.