Co oznacza „abstrakcja”?

95

Często w literaturze Scali spotykam się z wyrażeniem „abstrakcja”, ale nie rozumiem intencji. Na przykład pisze Martin Odersky

Możesz przekazać metody (lub „funkcje”) jako parametry lub możesz je abstrahować . Możesz określić typy jako parametry lub abstrakcyjne .

Jako inny przykład w artykule „Deprecating the Observer Pattern” ,

Konsekwencją tego, że nasze strumienie zdarzeń są wartościami pierwszej klasy, jest to, że możemy nad nimi abstrakcyjnie .

Czytałem, że typy generyczne pierwszego rzędu „abstrakcyjne nad typami”, podczas gdy monady „abstrakcyjne względem konstruktorów typów”. Takie zwroty widzimy również w papierze Cake Pattern . Cytując jeden z wielu takich przykładów:

Elementy członkowskie typu abstrakcyjnego zapewniają elastyczny sposób abstrahowania od konkretnych typów komponentów.

Nawet istotne pytania dotyczące przepełnienia stosu używają tej terminologii. „nie można egzystencjalnie abstrakcyjnie nad typem sparametryzowanym ...”

Więc ... co właściwie oznacza „abstrakcja”?

Morgan Creighton
źródło

Odpowiedzi:

124

W algebrze, podobnie jak w codziennym tworzeniu pojęć, abstrakcje są tworzone przez grupowanie rzeczy według pewnych zasadniczych cech i pomijanie ich specyficznych innych cech. Abstrakcja jest ujednolicona pod jednym symbolem lub słowem oznaczającym podobieństwa. Mówimy, że abstrahujemy od różnic, ale to naprawdę oznacza, że integrujemy przez podobieństwa.

Rozważmy na przykład program, który bierze sumę liczb 1, 2oraz 3:

val sumOfOneTwoThree = 1 + 2 + 3

Ten program nie jest zbyt interesujący, ponieważ nie jest zbyt abstrakcyjny. Możemy wyabstrahować liczby, które sumujemy, integrując wszystkie listy liczb pod jednym symbolem ns:

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

I nie obchodzi nas też, że jest to lista. Lista jest konstruktorem określonego typu (pobiera typ i zwraca typ), ale możemy abstrahować od konstruktora typu, określając, jaką podstawową cechę chcemy (że można ją złożyć):

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)

Możemy mieć niejawne Foldableinstancje Listi każdą inną rzecz, którą możemy spasować.

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

Co więcej, możemy abstrahować zarówno od operacji, jak i typu operandów:

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)

Teraz mamy coś całkiem ogólnego. Metoda mapReducezwinie wszystkie F[A]dane, które możemy udowodnić, że Fsą składane i Asą monoidalne lub można je zmapować na jeden. Na przykład:

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)

Mamy wydobywane przez monoids i foldables.

Apocalisp
źródło
@coubeatczech Kod działa na grzywnie REPL. Jakiej wersji Scali używasz i jaki błąd dostałeś?
Daniel C. Sobral,
1
@Apocalisp Byłoby interesujące, gdybyś zrobił jeden z dwóch ostatnich przykładów a Setlub inny typ składany. Przykład z Stringkonkatenacją i również byłby całkiem fajny.
Daniel C. Sobral,
1
Piękna odpowiedź, Runar. Dzięki! Postępowałem zgodnie z sugestią Daniela i stworzyłem implicit setFoldable i concatMonoid, bez zmiany mapReduce w ogóle. Jestem na dobrej drodze, żeby to rozwikłać.
Morgan Creighton
6
Zajęło mi chwilę, zanim w ostatnich dwóch wierszach wykorzystałeś fakt, że obiekty towarzyszące Sum i Product, ponieważ definiują apply (Int), są traktowane przez Scala jako Int => Sum i Int => Product kompilator. Bardzo dobrze!
Kris Nuttycombe
Niezły post :)! W twoim ostatnim przykładzie niejawna logika Monoid wydaje się niepotrzebna. To jest prostsze: gist.github.com/cvogt/9716490
cvogt
11

W pierwszym przybliżeniu, możliwość „abstrahowania” czegoś oznacza, że ​​zamiast używać tego czegoś bezpośrednio, można utworzyć z tego parametr lub w inny sposób używać go „anonimowo”.

Scala umożliwia abstrakcję nad typami, zezwalając klasom, metodom i wartościom na parametry typu, a wartościom na typy abstrakcyjne (lub anonimowe).

Scala pozwala abstrahować od działań, zezwalając metodom na posiadanie parametrów funkcji.

Scala umożliwia abstrakcję funkcji, umożliwiając strukturalne definiowanie typów.

Scala umożliwia abstrahowanie od parametrów typu, zezwalając na parametry typu wyższego rzędu.

Scala umożliwia abstrahowanie od wzorców dostępu do danych, umożliwiając tworzenie ekstraktorów.

Scala pozwala abstrahować od „rzeczy, które mogą być użyte jako coś innego”, zezwalając na niejawne konwersje jako parametry. Haskell postępuje podobnie z klasami typów.

Scala nie pozwala (jeszcze) na abstrakcje nad klasami. Nie możesz przekazać do czegoś klasy, a następnie użyć tej klasy do tworzenia nowych obiektów. Inne języki pozwalają na abstrakcję w stosunku do klas.

(„Monady abstrakcyjne w stosunku do konstruktorów typów” są prawdziwe tylko w bardzo restrykcyjny sposób. Nie rozłączaj się, dopóki nie masz swojego momentu „Aha! Rozumiem monady !!”).

Zdolność do abstrahowania od pewnych aspektów obliczeń jest w zasadzie tym, co umożliwia ponowne użycie kodu i umożliwia tworzenie bibliotek funkcjonalności. Scala pozwala na abstrakcje o wiele więcej rodzajów rzeczy niż bardziej popularne języki, a biblioteki w Scali mogą być odpowiednio potężniejsze.

Dave Griffith
źródło
1
Możesz przekazać a Manifest, a nawet a Class, i użyć odbicia do utworzenia wystąpienia nowych obiektów tej klasy.
Daniel C. Sobral,
6

Abstrakcja to rodzaj uogólnienia.

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

Nie tylko w Scali, ale w wielu językach istnieje potrzeba posiadania takich mechanizmów, aby zmniejszyć złożoność (lub przynajmniej stworzyć hierarchię, która dzieli informacje na łatwiejsze do zrozumienia fragmenty).

Klasa jest abstrakcją w stosunku do prostego typu danych. To trochę jak typ podstawowy, ale w rzeczywistości je uogólnia. Klasa jest więc czymś więcej niż zwykłym typem danych, ale ma z nią wiele wspólnego.

Kiedy mówi „abstrahować”, ma na myśli proces, w którym uogólniasz. Więc jeśli wyabstrahujesz metody jako parametry, uogólniasz proces robienia tego. np. zamiast przekazywania metod do funkcji możesz stworzyć pewien rodzaj uogólnionego sposobu obsługi tego (na przykład nieprzekazywanie metod w ogóle, ale zbudowanie specjalnego systemu do obsługi tego).

W tym przypadku konkretnie ma na myśli proces wyodrębniania problemu i tworzenia rozwiązania problemu podobnego do oop. C ma bardzo małą zdolność do abstrakcji (możesz to zrobić, ale szybko robi się bałagan, a język nie obsługuje tego bezpośrednio). Jeśli napisałeś to w C ++, możesz użyć koncepcji oop, aby zmniejszyć złożoność problemu (cóż, jest to ta sama złożoność, ale konceptualizacja jest ogólnie łatwiejsza (przynajmniej gdy nauczysz się myśleć w kategoriach abstrakcji)).

np., gdybym potrzebował specjalnego typu danych, który byłby podobny do int, ale powiedzmy ograniczony, mógłbym go wyabstrahować, tworząc nowy typ, który mógłby być używany jak int, ale miałby te właściwości, których potrzebowałem. Proces, którego użyłbym do zrobienia czegoś takiego, nazwanoby „abstrakcją”.

StreszczenieDissonance
źródło
5

Oto moje wąskie przedstawienie i interpretacja. Jest to oczywiste i działa w 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
źródło
1
prawie „abstrakcyjny” pomysł w kodzie - potężny, ale krótki, wypróbuje ten język +1
user44298
2

Inne odpowiedzi dają już dobre wyobrażenie o tym, jakie rodzaje abstrakcji istnieją. Przejrzyjmy po kolei cytaty i podajmy przykład:

Możesz przekazać metody (lub „funkcje”) jako parametry lub możesz je abstrahować. Możesz określić typy jako parametry lub abstrakcyjne.

Przekaż funkcję jako parametr: w tym miejscu jest List(1,-2,3).map(math.abs(x))wyraźnie absprzekazywany jako parametr. mapsama abstrahuje od funkcji, która wykonuje określoną rzecz z każdym elementem listy. val list = List[String]()określa parametr typu (String). Można napisać typ kolekcji, który używa abstrakcyjnych elementów typu zamiast: val buffer = Buffer{ type Elem=String }. Jedyną różnicą jest to, że musisz pisać def f(lis:List[String])...ale def f(buffer:Buffer)..., więc typ elementu jest trochę „ukryty” w drugiej metodzie.

Konsekwencją tego, że nasze strumienie zdarzeń są wartościami pierwszej klasy, jest to, że możemy nad nimi abstrakcyjnie.

W Swingu wydarzenie po prostu „dzieje się” nieoczekiwanie i musisz sobie z nim radzić tu i teraz. Strumienie zdarzeń umożliwiają wykonanie wszystkich prac hydraulicznych i okablowania w bardziej deklaratywny sposób. Np. Kiedy chcesz zmienić odpowiedzialnego słuchacza w Swing, musisz wyrejestrować starego i zarejestrować nowego oraz poznać wszystkie krwawe szczegóły (np. Problemy z wątkami). W przypadku strumieni zdarzeń źródło zdarzeń staje się rzeczą, którą można po prostu przekazać, dzięki czemu nie różni się zbytnio od strumienia bajtów lub znaków, stąd koncepcja bardziej „abstrakcyjna”.

Elementy członkowskie typu abstrakcyjnego zapewniają elastyczny sposób abstrahowania od konkretnych typów komponentów.

Powyższa klasa Buffer jest już tego przykładem.

Landei
źródło
1

Powyższe odpowiedzi stanowią doskonałe wyjaśnienie, ale podsumowując to w jednym zdaniu, powiedziałbym:

Abstrahowanie nad czymś jest tym samym, co zaniedbywanie tego, gdy nie ma to znaczenia .

Michał Kaczanowicz
źródło