O que significa “abstrair”?


95

Freqüentemente, na literatura do Scala, encontro a frase "abstrair", mas não entendo a intenção. Por exemplo , Martin Odersky escreve

Você pode passar métodos (ou "funções") como parâmetros ou pode abstraí- los. Você pode especificar tipos como parâmetros ou abstrair sobre eles.

Como outro exemplo, no artigo "Descontinuando o padrão do observador" ,

Uma consequência de nossos fluxos de eventos serem valores de primeira classe é que podemos abstraí- los.

Eu li que os genéricos de primeira ordem "abstraem os tipos", enquanto as mônadas "abstraem os construtores de tipo". E também vemos frases como essa no artigo do Cake Pattern . Para citar um de muitos exemplos:

Membros de tipo abstrato fornecem uma maneira flexível de abstrair sobre tipos concretos de componentes.

Mesmo as questões de estouro de pilha relevantes usam essa terminologia. "não pode abstrair existencialmente sobre o tipo parametrizado ..."

Então ... o que "abstrair" realmente significa?

Respostas:


124

Na álgebra, como na formação de conceitos cotidianos, as abstrações são formadas pelo agrupamento de coisas por algumas características essenciais e omissão de suas outras características específicas. A abstração é unificada sob um único símbolo ou palavra denotando as semelhanças. Dizemos que abstraímos as diferenças, mas isso realmente significa que estamos nos integrando pelas semelhanças.

Por exemplo, considere um programa que leva a soma dos números 1, 2e 3:

val sumOfOneTwoThree = 1 + 2 + 3

Este programa não é muito interessante, pois não é muito abstrato. Podemos abstrair os números que estamos somando, integrando todas as listas de números em um único símbolo ns:

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

E não nos importamos particularmente se é uma lista também. List é um construtor de tipo específico (pega um tipo e retorna um tipo), mas podemos abstrair o construtor de tipo especificando qual característica essencial queremos (que pode ser dobrada):

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)

E podemos ter Foldableinstâncias implícitas para Listqualquer outra coisa que possamos dobrar.

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))

Além do mais, podemos abstrair a operação e o 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)

Agora temos algo bastante geral. O método mapReducedobrará qualquer F[A]dado que possamos provar que Fé dobrável e que Aé um monóide ou pode ser mapeado em um. Por exemplo:

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)

Temos abstraída sobre monoids e foldables.


@coubeatczech O código roda bem em REPL. Qual versão do Scala você está usando e qual erro você obteve?
Daniel C. Sobral

1
@Apocalisp Seria interessante se você fizesse um dos dois exemplos finais um Setou algum outro tipo dobrável. Um exemplo com a Stringconcatenação e também seria muito legal.
Daniel C. Sobral

1
Bela resposta, Runar. Obrigado! Segui a sugestão de Daniel e criei setFoldable e concatMonoid implícitos, sem alterar mapReduce de forma alguma. Estou bem no meu caminho para grocar isso.
Morgan Creighton

6
Levei um momento para entender que nas últimas 2 linhas você tira vantagem do fato de que os objetos complementares Soma e Produto, porque eles definem aplicar (Int), são tratados como Int => Soma e Int => Produto pela Escala compilador. Muito agradável!
Kris Nuttycombe

Bela postagem :)! Em seu último exemplo, a lógica implícita do monóide parece desnecessária. É mais simples: gist.github.com/cvogt/9716490
cvogt

11

Para uma primeira aproximação, ser capaz de "abstrair" algo significa que em vez de usar esse algo diretamente, você pode fazer um parâmetro dele, ou então usá-lo "anonimamente".

Scala permite que você abstraia tipos, permitindo que classes, métodos e valores tenham parâmetros de tipo, e os valores tenham tipos abstratos (ou anônimos).

Scala permite que você abstraia ações, permitindo que os métodos tenham parâmetros de função.

Scala permite que você abstraia recursos, permitindo que os tipos sejam definidos estruturalmente.

Scala permite abstrair parâmetros de tipo, permitindo parâmetros de tipo de ordem superior.

Scala permite que você abstraia os padrões de acesso a dados, permitindo a criação de extratores.

Scala permite abstrair "coisas que podem ser usadas como outra coisa", permitindo conversões implícitas como parâmetros. Haskell faz o mesmo com classes de tipo.

Scala (ainda) não permite que você abstraia as classes. Você não pode passar uma classe para algo e, em seguida, usar essa classe para criar novos objetos. Outras linguagens permitem abstração sobre classes.

("Mônadas abstraem sobre construtores de tipo" só é verdadeiro de uma forma muito restritiva. Não se preocupe com isso até que você tenha seu momento "Aha! Eu entendo mônadas !!".)

A capacidade de abstrair algum aspecto da computação é basicamente o que permite a reutilização de código e permite a criação de bibliotecas de funcionalidade. O Scala permite que muito mais tipos de coisas sejam abstraídas do que as linguagens convencionais, e as bibliotecas no Scala podem ser correspondentemente mais poderosas.


1
Você pode passar um Manifest, ou mesmo um Class, e usar reflexão para instanciar novos objetos dessa classe.
Daniel C. Sobral

6

Uma abstração é uma espécie de generalização.

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

Não apenas no Scala, mas em muitas linguagens, é necessário ter tais mecanismos para reduzir a complexidade (ou pelo menos criar uma hierarquia que particione as informações em partes mais fáceis de entender).

Uma classe é uma abstração sobre um tipo de dados simples. É como um tipo básico, mas na verdade os generaliza. Portanto, uma classe é mais do que um tipo de dados simples, mas tem muitas coisas em comum com ela.

Quando ele diz "abstraindo", ele se refere ao processo pelo qual você generaliza. Portanto, se você abstrai os métodos como parâmetros, está generalizando o processo de fazer isso. por exemplo, em vez de passar métodos para funções, você pode criar algum tipo de maneira generalizada de lidar com isso (como não passar métodos, mas construir um sistema especial para lidar com isso).

Nesse caso, ele se refere especificamente ao processo de abstrair um problema e criar uma solução semelhante a oop para o problema. C tem muito pouca habilidade de abstrair (você pode fazer isso, mas fica confuso muito rápido e a linguagem não oferece suporte direto). Se você escreveu em C ++, você poderia usar conceitos oop para reduzir a complexidade do problema (bem, é a mesma complexidade, mas a conceituação é geralmente mais fácil (pelo menos depois que você aprender a pensar em termos de abstrações)).

por exemplo, se eu precisasse de um tipo de dado especial que fosse como um int, mas, digamos, restrito, eu poderia abstrair sobre ele criando um novo tipo que poderia ser usado como um int, mas tinha as propriedades de que eu precisava. O processo que eu usaria para fazer isso seria chamado de "abstração".


5

Aqui está o meu estreito show and tell interpretação. É autoexplicativo e roda no 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
praticamente a ideia "abstrata" no código - poderosa, mas curta, tentarei esta linguagem +1
user44298

2

As outras respostas já dão uma boa ideia dos tipos de abstrações existentes. Vamos repassar as citações uma por uma e dar um exemplo:

Você pode passar métodos (ou "funções") como parâmetros ou pode abstraí-los. Você pode especificar tipos como parâmetros ou abstrair sobre eles.

Passe a função como um parâmetro: List(1,-2,3).map(math.abs(x)) Claramente absé passado como parâmetro aqui. mapse abstrai sobre uma função que faz uma certa coisa especial com cada elemento da lista. val list = List[String]()especifica um parâmetro de tipo (String). Você poderia escrever um tipo de coleção que utiliza membros de tipo abstrato em vez disso: val buffer = Buffer{ type Elem=String }. Uma diferença é que você tem que escrever def f(lis:List[String])...mas def f(buffer:Buffer)..., então o tipo de elemento fica meio "escondido" no segundo método.

Uma consequência de nossos fluxos de eventos serem valores de primeira classe é que podemos abstraí-los.

No Swing, um evento simplesmente "acontece" do nada, e você tem que lidar com isso aqui e agora. Os fluxos de eventos permitem que você faça todo o encanamento e a fiação de uma forma mais declarativa. Por exemplo, quando você deseja alterar o ouvinte responsável no Swing, você deve cancelar o registro do antigo e registrar o novo, e saber todos os detalhes sangrentos (por exemplo, problemas de threading). Com os fluxos de eventos, a origem dos eventos torna-se algo que você simplesmente pode repassar, tornando-o não muito diferente de um fluxo de bytes ou caracteres, portanto, um conceito mais "abstrato".

Membros de tipo abstrato fornecem uma maneira flexível de abstrair sobre tipos concretos de componentes.

A classe Buffer acima já é um exemplo disso.


0

As respostas acima fornecem uma explicação excelente, mas para resumir em uma única frase, eu diria:

Abstrair sobre algo é o mesmo que negligenciá-lo onde for irrelevante .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.