Que signifie «abstrait sur»?

95

Souvent, dans la littérature Scala, je rencontre l'expression «abstract over», mais je ne comprends pas l'intention. Par exemple , Martin Odersky écrit

Vous pouvez passer des méthodes (ou «fonctions») en tant que paramètres, ou vous pouvez les résumer . Vous pouvez spécifier des types en tant que paramètres, ou vous pouvez les résumer .

Comme autre exemple, dans l'article «Deprecating the Observer Pattern» ,

Une conséquence du fait que nos flux d'événements sont des valeurs de première classe est que nous pouvons les résumer .

J'ai lu que les génériques du premier ordre "abstraits sur les types", tandis que les monades "abstraites sur les constructeurs de types". Et nous voyons également des phrases comme celle-ci dans le papier Cake Pattern . Pour citer l'un des nombreux exemples de ce type:

Les membres de type abstrait fournissent un moyen flexible d' abstraire des types concrets de composants.

Même les questions de dépassement de pile pertinentes utilisent cette terminologie. "impossible d'abstraire existentiellement sur un type paramétré ..."

Alors ... que signifie réellement «abstrait sur»?

Morgan Creighton
la source

Réponses:

124

En algèbre, comme dans la formation quotidienne des concepts, les abstractions sont formées en regroupant les choses selon certaines caractéristiques essentielles et en omettant leurs autres caractéristiques spécifiques. L'abstraction est unifiée sous un seul symbole ou mot dénotant les similitudes. Nous disons que nous faisons abstraction des différences, mais cela signifie vraiment que nous intégrons par les similitudes.

Par exemple, envisager un programme qui prend la somme des nombres 1, 2et 3:

val sumOfOneTwoThree = 1 + 2 + 3

Ce programme n'est pas très intéressant, car il n'est pas très abstrait. Nous pouvons faire abstraction des nombres que nous additionnons, en intégrant toutes les listes de nombres sous un seul symbole ns:

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

Et nous ne nous soucions pas particulièrement du fait que ce soit une liste non plus. List est un constructeur de type spécifique (prend un type et retourne un type), mais nous pouvons faire abstraction du constructeur de type en spécifiant la caractéristique essentielle que nous voulons (qu'il puisse être plié):

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)

Et nous pouvons avoir des Foldableinstances implicites pour Listet toute autre chose que nous pouvons replier.

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

De plus, nous pouvons faire abstraction à la fois de l'opération et du type des opérandes:

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)

Maintenant, nous avons quelque chose d'assez général. La méthode mapReducepliera tout élément F[A]que nous pouvons prouver qu'il Fest pliable et qui Aest un monoïde ou peut être mappé en un seul. Par exemple:

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)

Nous avons résumé les monoïdes et les pliables.

Apocalisp
la source
@coubeatczech Le code fonctionne bien sur REPL. Quelle version de Scala utilisez-vous et quelle erreur avez-vous obtenue?
Daniel C.Sobral
1
@Apocalisp Ce serait intéressant si vous faisiez l'un des deux derniers exemples Setou un autre type pliable. Un exemple avec une Stringconcaténation et serait également assez cool.
Daniel C.Sobral
1
Belle réponse, Runar. Merci! J'ai suivi la suggestion de Daniel, et j'ai créé setFoldable et concatMonoid implicites, sans modifier du tout mapReduce. Je suis sur la bonne voie pour faire ça.
Morgan Creighton
6
Il m'a fallu un moment pour comprendre que dans les 2 dernières lignes, vous profitez du fait que les objets compagnons Sum et Product, car ils définissent apply (Int), sont traités comme Int => Sum et Int => Product par la Scala compilateur. Très agréable!
Kris Nuttycombe
Belle publication :)! Dans votre dernier exemple, la logique implicite Monoid semble inutile. C'est plus simple: gist.github.com/cvogt/9716490
cvogt
11

En première approximation, être capable de «faire abstraction» de quelque chose signifie qu'au lieu d'utiliser ce quelque chose directement, vous pouvez en faire un paramètre, ou autrement l'utiliser «anonymement».

Scala vous permet d'abstraire des types, en autorisant les classes, les méthodes et les valeurs à avoir des paramètres de type et les valeurs à avoir des types abstraits (ou anonymes).

Scala vous permet d'abstraire des actions, en permettant aux méthodes d'avoir des paramètres de fonction.

Scala vous permet d'abstraire des entités, en autorisant la définition structurelle des types.

Scala vous permet d'abstraire des paramètres de type, en autorisant des paramètres de type d'ordre supérieur.

Scala vous permet d'abstraire des modèles d'accès aux données, en vous permettant de créer des extracteurs.

Scala vous permet d'abstraire "des choses qui peuvent être utilisées comme autre chose", en autorisant des conversions implicites comme paramètres. Haskell fait de même avec les classes de types.

Scala ne vous permet pas (encore) d'abstraire des classes. Vous ne pouvez pas passer une classe à quelque chose, puis utiliser cette classe pour créer de nouveaux objets. D'autres langages autorisent l'abstraction sur les classes.

("Monades abstraites sur les constructeurs de types" n'est vrai que de manière très restrictive. Ne restez pas accroché avant d'avoir votre moment "Aha! Je comprends les monades !!".)

La capacité d'abstraction de certains aspects du calcul est essentiellement ce qui permet la réutilisation du code et permet la création de bibliothèques de fonctionnalités. Scala permet d'abstraire beaucoup plus de choses que les langages traditionnels, et les bibliothèques de Scala peuvent être en conséquence plus puissantes.

Dave Griffith
la source
1
Vous pouvez passer un Manifest, voire un Class, et utiliser la réflexion pour instancier de nouveaux objets de cette classe.
Daniel C.Sobral
6

Une abstraction est une sorte de généralisation.

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

Non seulement dans Scala mais dans de nombreux langages, il est nécessaire d'avoir de tels mécanismes pour réduire la complexité (ou au moins créer une hiérarchie qui partitionne les informations en éléments plus faciles à comprendre).

Une classe est une abstraction sur un type de données simple. C'est un peu comme un type de base mais les généralise en fait. Une classe est donc plus qu'un simple type de données, mais a beaucoup de choses en commun avec elle.

Quand il dit «faire abstraction», il veut dire le processus par lequel vous généralisez. Donc, si vous faites abstraction de méthodes en tant que paramètres, vous généralisez le processus pour le faire. par exemple, au lieu de passer des méthodes aux fonctions, vous pouvez créer un type de manière généralisée de le gérer (comme ne pas passer du tout de méthodes mais construire un système spécial pour le gérer).

Dans ce cas, il désigne spécifiquement le processus d'abstraction d'un problème et de création d'une solution de type oop au problème. C a très peu de capacité d'abstraction (vous pouvez le faire mais cela devient très vite compliqué et le langage ne le prend pas directement en charge). Si vous l'avez écrit en C ++, vous pouvez utiliser des concepts oop pour réduire la complexité du problème (enfin, c'est la même complexité mais la conceptualisation est généralement plus facile (au moins une fois que vous apprenez à penser en termes d'abstractions)).

Par exemple, si j'avais besoin d'un type de données spécial qui ressemblait à un int mais, disons restreint, je pourrais faire abstraction dessus en créant un nouveau type qui pourrait être utilisé comme un int mais qui avait les propriétés dont j'avais besoin. Le processus que j'utiliserais pour faire une telle chose s'appellerait un "résumé".

RésuméDissonance
la source
5

Voici mon spectacle étroit et dire l'interprétation. Il est explicite et fonctionne dans le 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()
huynhjl
la source
1
à peu près l'idée "abstraite sur" dans le code - puissant mais court, essaiera cette langue +1
user44298
2

Les autres réponses donnent déjà une bonne idée des types d'abstractions qui existent. Passons en revue les citations une par une et donnons un exemple:

Vous pouvez passer des méthodes (ou «fonctions») en tant que paramètres, ou vous pouvez les résumer. Vous pouvez spécifier des types en tant que paramètres, ou vous pouvez les résumer.

Passer la fonction en tant que paramètre: est List(1,-2,3).map(math.abs(x))clairement abspassé en paramètre ici. maplui-même fait abstraction d'une fonction qui fait une certaine chose spécialisée avec chaque élément de liste. val list = List[String]()spécifie un paramètre de type (String). Vous pouvez écrire un type de collection qui utilise les membres de type abstrait à la place: val buffer = Buffer{ type Elem=String }. Une différence est que vous devez écrire def f(lis:List[String])...mais def f(buffer:Buffer)..., le type d'élément est donc "caché" dans la deuxième méthode.

Une conséquence du fait que nos flux d'événements sont des valeurs de première classe est que nous pouvons les résumer.

Dans Swing, un événement «arrive» à l'improviste, et vous devez le gérer ici et maintenant. Les flux d'événements vous permettent de faire toute la plomberie et le câblage d'une manière plus déclarative. Par exemple, lorsque vous voulez changer l'auditeur responsable dans Swing, vous devez désenregistrer l'ancien et enregistrer le nouveau, et connaître tous les détails sanglants (par exemple les problèmes de threading). Avec les flux d'événements, la source des événements devient une chose que vous pouvez simplement transmettre, ce qui la rend pas très différente d'un flux d'octets ou de caractères, d'où un concept plus "abstrait".

Les membres de type abstrait fournissent un moyen flexible d'abstraire des types concrets de composants.

La classe Buffer ci-dessus en est déjà un exemple.

Landei
la source
0

Les réponses ci-dessus fournissent une excellente explication, mais pour la résumer en une seule phrase, je dirais:

Abstraire quelque chose revient exactement à le négliger là où il n'est pas pertinent .

Michał Kaczanowicz
la source