Cosa significa "abstract over"?


95

Spesso nella letteratura scaligera incontro la frase "abstract over", ma non ne capisco l'intento. Ad esempio , scrive Martin Odersky

È possibile passare metodi (o "funzioni") come parametri o astrarre su di essi. È possibile specificare i tipi come parametri o astrarli .

Come altro esempio, nel documento "Deprecating the Observer Pattern" ,

Una conseguenza del fatto che i nostri flussi di eventi sono valori di prima classe è che possiamo astrarre su di essi.

Ho letto che i generici del primo ordine "astratti sui tipi", mentre le monadi "astratti sui costruttori di tipi". E vediamo anche frasi come questa nel documento Cake Pattern . Per citare uno dei tanti esempi di questo tipo:

I membri di tipo astratto forniscono un modo flessibile per astrarre su tipi concreti di componenti.

Anche le domande di overflow dello stack pertinenti utilizzano questa terminologia. "non può esistenzialmente astratto sul tipo parametrizzato ..."

Allora ... cosa significa in realtà "abstract over"?

Risposte:


124

In algebra, come nella formazione quotidiana dei concetti, le astrazioni sono formate raggruppando le cose in base ad alcune caratteristiche essenziali e omettendo le loro altre caratteristiche specifiche. L'astrazione è unificata sotto un unico simbolo o parola che denota le somiglianze. Diciamo che astraggiamo sulle differenze, ma questo significa davvero che ci stiamo integrando in base alle somiglianze.

Ad esempio, si consideri un programma che prende la somma dei numeri 1, 2e 3:

val sumOfOneTwoThree = 1 + 2 + 3

Questo programma non è molto interessante, poiché non è molto astratto. Possiamo astrarre sui numeri che stiamo sommando, integrando tutti gli elenchi di numeri sotto un unico simbolo ns:

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

E non ci interessa particolarmente nemmeno che sia una lista. List è un costruttore di tipi specifico (prende un tipo e restituisce un tipo), ma possiamo astrarre sul costruttore di tipi specificando quale caratteristica essenziale vogliamo (che può essere piegata):

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 possiamo avere Foldableistanze implicite per Liste qualsiasi altra cosa per cui possiamo piegare.

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

Inoltre, possiamo astrarre sia l'operazione che il tipo di operandi:

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)

Ora abbiamo qualcosa di abbastanza generale. Il metodo mapReduceripiegherà qualsiasi F[A]dato che possiamo dimostrare che Fè pieghevole e che Aè un monoide o può essere mappato in uno. Per esempio:

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)

Abbiamo astratto su monoidi e pieghevoli.


@coubeatczech Il codice funziona bene su REPL. Quale versione di Scala stai usando e quale errore hai ricevuto?
Daniel C. Sobral

1
@Apocalisp Sarebbe interessante se facessi uno dei due esempi finali Setao qualche altro tipo pieghevole. Anche un esempio con Stringae concatenazione sarebbe piuttosto interessante.
Daniel C. Sobral

1
Bella risposta, Runar. Grazie! Ho seguito il suggerimento di Daniel e ho creato setFoldable e concatMonoid impliciti, senza alterare affatto mapReduce. Sto bene per farlo.
Morgan Creighton

6
Mi ci è voluto un momento per capire che nelle ultime 2 righe si approfitta del fatto che gli oggetti associati Sum e Product, poiché definiscono apply (Int), sono trattati come Int => Sum e Int => Product dalla Scala compilatore. Molto bella!
Kris Nuttycombe

Bel post :)! Nel tuo ultimo esempio, la logica implicita Monoid sembra non necessaria. Questo è più semplice: gist.github.com/cvogt/9716490
cvogt

11

In prima approssimazione, essere in grado di "astrarre su" qualcosa significa che invece di usare quel qualcosa direttamente, puoi farne un parametro, o altrimenti usarlo "in modo anonimo".

Scala ti consente di astrarre sui tipi, consentendo a classi, metodi e valori di avere parametri di tipo e valori di tipi astratti (o anonimi).

Scala consente di astrarre sulle azioni, consentendo ai metodi di avere parametri di funzione.

Scala consente di astrarre sulle caratteristiche, consentendo la definizione strutturale dei tipi.

Scala ti consente di astrarre i parametri di tipo, consentendo parametri di tipo di ordine superiore.

Scala ti consente di astrarre sui modelli di accesso ai dati, permettendoti di creare estrattori.

Scala ti permette di astrarre su "cose ​​che possono essere usate come qualcos'altro", consentendo conversioni implicite come parametri. Haskell fa lo stesso con le classi di tipo.

Scala non ti consente (ancora) di astrarre sulle classi. Non puoi passare una classe a qualcosa e poi usare quella classe per creare nuovi oggetti. Altre lingue consentono l'astrazione sulle classi.

("Monadi astratte sui costruttori di tipi" è vero solo in un modo molto restrittivo. Non rimanere bloccato finché non hai il tuo momento "Aha! Capisco le monadi !!".)

La capacità di astrarre su alcuni aspetti del calcolo è fondamentalmente ciò che consente il riutilizzo del codice e consente la creazione di librerie di funzionalità. Scala consente di astrarre molte più cose rispetto a linguaggi tradizionali e le librerie in Scala possono essere di conseguenza più potenti.


1
Puoi passare a Manifest, o anche a Class, e usare la reflection per istanziare nuovi oggetti di quella classe.
Daniel C. Sobral

6

Un'astrazione è una sorta di generalizzazione.

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

Non solo in Scala, ma in molti linguaggi è necessario disporre di tali meccanismi per ridurre la complessità (o almeno creare una gerarchia che suddivide le informazioni in pezzi più facili da capire).

Una classe è un'astrazione su un semplice tipo di dati. È una specie di tipo base, ma in realtà li generalizza. Quindi una classe è più di un semplice tipo di dati, ma ha molte cose in comune con essa.

Quando dice "astrazione su", intende il processo con cui generalizzi. Quindi, se astratti sui metodi come parametri, stai generalizzando il processo per farlo. ad esempio, invece di passare metodi alle funzioni, potresti creare qualche tipo di modo generalizzato per gestirlo (come non passare affatto metodi ma costruire un sistema speciale per gestirlo).

In questo caso si intende specificamente il processo di astrazione di un problema e la creazione di una soluzione simile al problema. Il C ha pochissime capacità di astrazione (puoi farlo ma diventa disordinato molto velocemente e il linguaggio non lo supporta direttamente). Se l'hai scritto in C ++ potresti usare concetti oop per ridurre la complessità del problema (beh, è ​​la stessa complessità ma la concettualizzazione è generalmente più semplice (almeno una volta che impari a pensare in termini di astrazioni)).

Ad esempio, se avessi bisogno di un tipo di dati speciale che fosse come un int ma, diciamo limitato, potrei astrarre su di esso creando un nuovo tipo che potrebbe essere usato come un int ma con quelle proprietà di cui avevo bisogno. Il processo che userei per fare una cosa del genere sarebbe chiamato "astrazione".


5

Ecco il mio spettacolo ristretto e racconta l'interpretazione. È autoesplicativo e viene eseguito nel 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 l'idea "astratta su" nel codice - potente ma breve, proverò questo linguaggio +1
user44298

2

Le altre risposte danno già una buona idea di quali tipi di astrazioni esistono. Esaminiamo le virgolette una per una e forniamo un esempio:

È possibile passare metodi (o "funzioni") come parametri o astrarre su di essi. È possibile specificare i tipi come parametri o astrarli.

Passa funzione come parametro: List(1,-2,3).map(math.abs(x))chiaramente absviene passato come parametro qui. mapsi astrae su una funzione che fa una certa cosa speciale con ogni elemento della lista. val list = List[String]()specifica un parametro di tipo (String). Si potrebbe scrivere un tipo di raccolta che utilizza componenti di tipo astratti invece: val buffer = Buffer{ type Elem=String }. Una differenza è che devi scrivere def f(lis:List[String])...ma def f(buffer:Buffer)..., quindi il tipo di elemento è un po '"nascosto" nel secondo metodo.

Una conseguenza del fatto che i nostri flussi di eventi sono valori di prima classe è che possiamo astrarre su di essi.

In Swing un evento "accade" di punto in bianco e devi affrontarlo qui e ora. I flussi di eventi ti consentono di eseguire tutto l'impianto idraulico e il cablaggio in modo più dichiarativo. Ad esempio, quando si desidera modificare l'ascoltatore responsabile in Swing, è necessario annullare la registrazione del vecchio e registrare quello nuovo e conoscere tutti i dettagli cruenti (ad es. Problemi di threading). Con i flussi di eventi, la sorgente degli eventi diventa qualcosa che puoi semplicemente passare in giro, rendendola non molto diversa da un flusso di byte o char, quindi un concetto più "astratto".

I membri di tipo astratto forniscono un modo flessibile per astrarre su tipi concreti di componenti.

La classe Buffer sopra è già un esempio per questo.


1

Le risposte sopra forniscono un'ottima spiegazione, ma per riassumerla in una sola frase, direi:

Astrarre su qualcosa è come trascurarlo quando è irrilevante .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.