“ นามธรรมทับ” หมายความว่าอย่างไร


95

บ่อยครั้งในวรรณคดี Scala ฉันพบวลี "นามธรรมมากกว่า" แต่ฉันไม่เข้าใจเจตนา ตัวอย่างเช่น Martin Odersky เขียน

คุณสามารถส่งเมธอด (หรือ "ฟังก์ชั่น") เป็นพารามิเตอร์หรือคุณสามารถสรุปทับได้ คุณสามารถระบุประเภทเป็นพารามิเตอร์หรือคุณสามารถสรุปทับได้

ดังตัวอย่างอื่นในกระดาษ"Deprecating the Observer Pattern"

ผลที่ตามมาจากกระแสเหตุการณ์ของเราเป็นค่านิยมชั้นหนึ่งคือเราสามารถสรุปเหนือสิ่งเหล่านี้ได้

ฉันได้อ่านชื่อสามัญลำดับแรก "นามธรรมเหนือประเภท" ในขณะที่ monads "abstract over type constructors" และเรายังเห็นวลีเช่นนี้ในกระดาษแบบเค้ก หากต้องการอ้างอิงหนึ่งในหลายตัวอย่างดังกล่าว:

สมาชิกประเภทบทคัดย่อให้วิธีที่ยืดหยุ่นในการทำนามธรรมกับส่วนประกอบประเภทคอนกรีต

แม้แต่คำถามเกี่ยวกับสแต็กล้นที่เกี่ยวข้องก็ใช้คำศัพท์นี้ "ไม่สามารถเป็นนามธรรมที่มีอยู่จริงเหนือประเภทที่กำหนดพารามิเตอร์ ... "

แล้ว ... "นามธรรมทับ" ที่จริงหมายความว่าอย่างไร?

คำตอบ:


124

ในพีชคณิตเช่นเดียวกับการสร้างแนวคิดในชีวิตประจำวัน Abstractions เกิดจากการจัดกลุ่มสิ่งต่าง ๆ ตามลักษณะสำคัญบางประการและละเว้นลักษณะเฉพาะอื่น ๆ สิ่งที่เป็นนามธรรมรวมเป็นหนึ่งเดียวภายใต้สัญลักษณ์หรือคำเดียวที่แสดงถึงความคล้ายคลึงกัน เราบอกว่าเราเป็นนามธรรมเหนือความแตกต่าง แต่นี่หมายความว่าเรากำลังรวมเข้าด้วยกันโดยความคล้ายคลึงกัน

ตัวอย่างเช่นพิจารณาโปรแกรมที่ใช้ผลรวมของตัวเลขที่1, 2และ3:

val sumOfOneTwoThree = 1 + 2 + 3

โปรแกรมนี้ไม่น่าสนใจมากนักเนื่องจากมันไม่ได้เป็นนามธรรมมากนัก เราสามารถสรุปตัวเลขที่เรากำลังสรุปได้โดยการรวมรายการตัวเลขทั้งหมดไว้ในสัญลักษณ์เดียวns:

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

และเราไม่สนใจเป็นพิเศษว่าเป็น List ด้วย รายการเป็นตัวสร้างชนิดเฉพาะ (รับประเภทและส่งคืนประเภท) แต่เราสามารถสรุปเหนือตัวสร้างประเภทได้โดยระบุลักษณะสำคัญที่เราต้องการ (ซึ่งสามารถพับเก็บได้):

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)

และเราสามารถมีFoldableอินสแตนซ์โดยนัยสำหรับListและสิ่งอื่น ๆ ที่เราพับได้

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

ยิ่งไปกว่านั้นเราสามารถสรุปได้ทั้งการดำเนินการและประเภทของตัวถูกดำเนินการ:

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)

ตอนนี้เรามีบางสิ่งที่ค่อนข้างทั่วไป วิธีmapReduceนี้จะพับอะไรF[A]ก็ได้ที่เราสามารถพิสูจน์ได้ว่าFพับได้และนั่นAคือ monoid หรือสามารถแมปเป็นชิ้นเดียวได้ ตัวอย่างเช่น:

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)

เราได้แยกแยะเรื่องโมโนและพับเก็บได้


@coubeatczech รหัสทำงานบน REPL ได้ดี คุณใช้ Scala รุ่นใดและคุณได้รับข้อผิดพลาดอะไร
Daniel C. Sobral

1
@Apocalisp มันจะน่าสนใจถ้าคุณทำหนึ่งในสองตัวอย่างสุดท้ายSetหรือแบบพับได้อื่น ๆ ตัวอย่างที่มีการStringเชื่อมต่อและการเชื่อมต่อก็ดูดีเช่นกัน
Daniel C. Sobral

1
คำตอบที่สวยงาม Runar ขอบคุณ! ฉันทำตามคำแนะนำของ Daniel และสร้าง implicit setFoldable และ concatMonoid โดยไม่ต้องแก้ไข mapReduce เลย ฉันกำลังเดินไปหาสิ่งนี้
Morgan Creighton

6
ฉันใช้เวลาสักครู่เพื่อให้ได้สิ่งนั้นใน 2 บรรทัดสุดท้ายคุณใช้ประโยชน์จากความจริงที่ว่า Sum และ Product ที่แสดงร่วมกันเนื่องจากพวกเขากำหนด Apply (Int) จะถือว่าเป็น Int => Sum และ Int => Product โดย Scala คอมไพเลอร์ ดีมาก!
Kris Nuttycombe

โพสต์ที่ดี :)! ในตัวอย่างสุดท้ายของคุณตรรกะโดยนัย Monoid ดูเหมือนไม่จำเป็น ง่ายกว่านี้: gist.github.com/cvogt/9716490
cvogt

11

ในการประมาณค่าครั้งแรกความสามารถในการ "นามธรรมทับ" บางสิ่งหมายความว่าแทนที่จะใช้สิ่งนั้นโดยตรงคุณสามารถสร้างพารามิเตอร์หรือใช้ "แบบไม่ระบุตัวตน"

Scala ช่วยให้คุณสามารถแยกประเภทนามธรรมได้โดยอนุญาตให้คลาสเมธอดและค่ามีพารามิเตอร์ประเภทและค่าเป็นประเภทนามธรรม (หรือไม่ระบุชื่อ)

Scala ช่วยให้คุณสามารถสรุปการกระทำได้โดยอนุญาตให้เมธอดมีพารามิเตอร์ฟังก์ชัน

Scala ช่วยให้คุณสามารถสรุปคุณสมบัติต่างๆได้โดยอนุญาตให้กำหนดประเภทในเชิงโครงสร้าง

Scala ช่วยให้คุณสามารถนามธรรมเหนือพารามิเตอร์ประเภทโดยอนุญาตให้พารามิเตอร์ประเภทลำดับที่สูงขึ้น

Scala ช่วยให้คุณสามารถสรุปรูปแบบการเข้าถึงข้อมูลโดยอนุญาตให้คุณสร้างตัวแยก

Scala ช่วยให้คุณสามารถสรุป "สิ่งที่สามารถใช้เป็นอย่างอื่นได้" โดยให้การแปลงโดยนัยเป็นพารามิเตอร์ Haskell ทำในทำนองเดียวกันกับคลาสประเภท

Scala ไม่ (ยัง) อนุญาตให้คุณทำบทคัดย่อในชั้นเรียน คุณไม่สามารถส่งต่อคลาสไปยังบางสิ่งจากนั้นใช้คลาสนั้นเพื่อสร้างอ็อบเจ็กต์ใหม่ ภาษาอื่น ๆ อนุญาตให้มีนามธรรมเหนือชั้นเรียน

("Monads abstract over type constructors" เป็นจริงในวิธีที่ จำกัด มากเท่านั้นอย่าเพิ่งวางสายจนกว่าคุณจะมีช่วงเวลา "Aha! I understand monads !!")

ความสามารถในการนามธรรมในบางแง่มุมของการคำนวณโดยพื้นฐานแล้วเป็นสิ่งที่อนุญาตให้นำโค้ดมาใช้ซ้ำและเปิดใช้งานการสร้างไลบรารีของฟังก์ชันการทำงาน Scala ช่วยให้สามารถสรุปสิ่งต่างๆได้หลายประเภทมากกว่าภาษากระแสหลักและไลบรารีใน Scala ก็มีประสิทธิภาพมากขึ้นตามลำดับ


1
คุณสามารถส่งผ่าน a Manifestหรือแม้กระทั่งClassและใช้การสะท้อนเพื่อสร้างอินสแตนซ์วัตถุใหม่ของคลาสนั้น
Daniel C. Sobral

6

สิ่งที่เป็นนามธรรมคือลักษณะทั่วไป

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

ไม่เพียง แต่ใน Scala เท่านั้น แต่ยังมีหลายภาษาที่จำเป็นต้องมีกลไกดังกล่าวเพื่อลดความซับซ้อน (หรืออย่างน้อยก็สร้างลำดับชั้นที่แบ่งข้อมูลให้เป็นส่วนที่เข้าใจง่ายขึ้น)

คลาสเป็นสิ่งที่เป็นนามธรรมเหนือชนิดข้อมูลธรรมดา มันเป็นเหมือนประเภทพื้นฐาน แต่จริงๆแล้วสรุปพวกเขา คลาสจึงเป็นมากกว่าชนิดข้อมูลธรรมดา แต่มีหลายสิ่งที่เหมือนกัน

เมื่อเขาพูดว่า "ทำให้เป็นนามธรรมมากกว่า" เขาหมายถึงกระบวนการที่คุณสรุป ดังนั้นหากคุณกำลังสรุปวิธีการเป็นพารามิเตอร์คุณกำลังสรุปขั้นตอนการทำเช่นนั้น เช่นแทนที่จะส่งวิธีการไปยังฟังก์ชันคุณอาจสร้างวิธีทั่วไปบางประเภทเพื่อจัดการกับมัน (เช่นไม่ส่งผ่านวิธีการเลย แต่สร้างระบบพิเศษขึ้นมาเพื่อจัดการกับมัน)

ในกรณีนี้เขาหมายถึงขั้นตอนการทำให้ปัญหาเป็นนามธรรมและสร้างวิธีแก้ปัญหาแบบโอป C มีความสามารถในการนามธรรมน้อยมาก (คุณทำได้ แต่มันยุ่งเหยิงอย่างรวดเร็วจริง ๆ และภาษาไม่รองรับโดยตรง) หากคุณเขียนในภาษา C ++ คุณสามารถใช้แนวคิด oop เพื่อลดความซับซ้อนของปัญหาได้ (ซึ่งก็มีความซับซ้อนเหมือนกัน แต่โดยทั่วไปแล้วแนวคิดจะง่ายกว่า (อย่างน้อยหนึ่งครั้งที่คุณเรียนรู้ที่จะคิดในแง่ของนามธรรม))

เช่นถ้าฉันต้องการชนิดข้อมูลพิเศษที่เหมือนกับ int แต่สมมติว่า จำกัด ฉันสามารถสรุปได้โดยการสร้างประเภทใหม่ที่สามารถใช้เช่น int แต่มีคุณสมบัติเหล่านั้นที่ฉันต้องการ กระบวนการที่ฉันจะใช้ทำสิ่งนั้นจะเรียกว่า "นามธรรม"


5

นี่คือการแสดงที่แคบและบอกการตีความของฉัน มันอธิบายตัวเองและทำงานใน 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
ความคิด "นามธรรมมากกว่า" ในโค้ด - ทรงพลัง แต่สั้นมากจะลองใช้ภาษานี้ +1
user44298

2

คำตอบอื่น ๆ ให้ความคิดที่ดีอยู่แล้วว่ามีอะไรบ้างที่มีอยู่ มาดูคำพูดทีละรายการและให้ตัวอย่าง:

คุณสามารถส่งเมธอด (หรือ "ฟังก์ชั่น") เป็นพารามิเตอร์หรือคุณสามารถสรุปทับได้ คุณสามารถระบุประเภทเป็นพารามิเตอร์หรือคุณสามารถสรุปทับได้

ผ่านฟังก์ชั่นเป็นพารามิเตอร์: List(1,-2,3).map(math.abs(x))เห็นได้ชัดว่าabsเป็นพารามิเตอร์ที่นี่ mapตัวมันเองเป็นนามธรรมเหนือฟังก์ชันที่ทำสิ่งที่เชี่ยวชาญเฉพาะกับแต่ละองค์ประกอบรายการ val list = List[String]()ระบุชนิดพารามิเตอร์ (String) val buffer = Buffer{ type Elem=String }คุณสามารถเขียนประเภทคอลเลกชันที่ใช้สมาชิกประเภทนามธรรมแทน: ความแตกต่างอย่างหนึ่งคือคุณต้องเขียนdef f(lis:List[String])...แต่def f(buffer:Buffer)...ประเภทองค์ประกอบจึงเป็น "ซ่อน" ในวิธีที่สอง

ผลที่ตามมาจากกระแสเหตุการณ์ของเราเป็นค่านิยมชั้นหนึ่งคือเราสามารถสรุปเหนือสิ่งเหล่านี้ได้

ใน Swing เหตุการณ์เพียง "เกิดขึ้น" จากสีน้ำเงินและคุณต้องจัดการกับมันที่นี่และตอนนี้ สตรีมกิจกรรมช่วยให้คุณสามารถเดินสายท่อประปาทั้งหมดได้ด้วยวิธีที่เปิดเผยมากขึ้น เช่นเมื่อคุณต้องการเปลี่ยนผู้ฟังที่มีความรับผิดชอบใน Swing คุณต้องยกเลิกการลงทะเบียนเก่าและลงทะเบียนใหม่และเพื่อทราบรายละเอียดเกี่ยวกับเลือดทั้งหมด (เช่นปัญหาเกี่ยวกับเธรด) ด้วยกระแสเหตุการณ์แหล่งที่มาของเหตุการณ์จะกลายเป็นสิ่งที่คุณสามารถส่งผ่านไปมาได้โดยไม่แตกต่างจากไบต์หรือสตรีมถ่านมากนักจึงเป็นแนวคิดที่ "นามธรรม" มากขึ้น

สมาชิกประเภทบทคัดย่อให้วิธีที่ยืดหยุ่นในการทำนามธรรมกับส่วนประกอบประเภทคอนกรีต

คลาส Buffer ด้านบนเป็นตัวอย่างสำหรับสิ่งนี้อยู่แล้ว


1

คำตอบข้างต้นให้คำอธิบายที่ดีเยี่ยม แต่ถ้าจะสรุปเป็นประโยคเดียวฉันจะพูดว่า:

สรุปมากกว่าบางสิ่งบางอย่างจะเหมือนกันมากในขณะที่ละเลยมันที่ไม่เกี่ยวข้อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.