ความต่อเนื่องของ Scala คืออะไรและเหตุใดจึงต้องใช้


85

ฉันเพิ่งเสร็จสิ้นการเขียนโปรแกรมใน Scalaและฉันได้ตรวจสอบการเปลี่ยนแปลงระหว่าง Scala 2.7 และ 2.8 สิ่งที่น่าจะสำคัญที่สุดคือปลั๊กอินความต่อเนื่อง แต่ฉันไม่เข้าใจว่ามันมีประโยชน์อะไรหรือทำงานอย่างไร ฉันเห็นว่ามันดีสำหรับ I / O แบบอะซิงโครนัส แต่ฉันไม่สามารถหาสาเหตุได้ แหล่งข้อมูลยอดนิยมบางส่วนในหัวข้อนี้ ได้แก่ :

และคำถามนี้ใน Stack Overflow:

น่าเสียดายที่ไม่มีการอ้างอิงใด ๆ เหล่านี้ที่พยายามกำหนดว่าความต่อเนื่องมีไว้เพื่ออะไรหรือฟังก์ชัน shift / reset ควรทำอย่างไรและฉันไม่พบการอ้างอิงใด ๆ ที่ทำ ฉันไม่สามารถเดาได้ว่าตัวอย่างใดในบทความที่เชื่อมโยงนั้นทำงานอย่างไร (หรือสิ่งที่พวกเขาทำ) ดังนั้นวิธีหนึ่งที่จะช่วยฉันได้คือการไปทีละบรรทัดผ่านหนึ่งในตัวอย่างเหล่านั้น แม้แต่เรื่องง่ายๆจากบทความที่สาม:

reset {
    ...
    shift { k: (Int=>Int) =>  // The continuation k will be the '_ + 1' below.
        k(7)
    } + 1
}
// Result: 8

ทำไมผล 8? นั่นอาจจะช่วยฉันในการเริ่มต้น


คำตอบ:


38

ฉันบล็อกไม่อธิบายสิ่งที่resetและshiftทำดังนั้นคุณอาจต้องการที่จะอ่านอีกครั้ง

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

กระดาษเกี่ยวกับความต่อเนื่องที่ใช้ตัวคั่นซึ่งฉันลิงก์ไปยังในบล็อกของฉัน แต่ดูเหมือนว่าจะใช้งานไม่ได้มีตัวอย่างการใช้งานมากมาย

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

ตอนนี้คุณไม่เข้าใจแม้แต่ตัวอย่างง่ายๆบนหน้า Scala ดังนั้นไม่อ่านบล็อกของฉัน ในนั้นฉันแค่กังวลกับการอธิบายพื้นฐานเหล่านี้ว่าเหตุใดผลลัพธ์จึงเป็น8เช่นนั้น


ฉันอ่านรายการบล็อกของคุณอีกครั้งและคราวนี้ฉันติดอยู่ - ฉันคิดว่าฉันมีความคิดที่ดีขึ้นว่าเกิดอะไรขึ้น ฉันไม่ได้รับอะไรมากจากหน้า Wikipedia (ฉันรู้จักความต่อเนื่องของ Lisp อยู่แล้ว) แต่รูปแบบการรีเซ็ต / เปลี่ยนการเลื่อนหรืออะไรก็ตามที่เรียกว่าทำให้ฉันนิ่งงัน สำหรับคนใจร้อน (เช่นตัวฉันเอง) คำอธิบายของคุณก็โอเค แต่ผู้คนจะต้องแน่ใจว่าจะยึดติดกับ "ผลลัพธ์ของการรีเซ็ตเป็นผลมาจากโค้ดภายในกะ ย่อหน้า ... ฉันหลงทางอย่างสิ้นหวังจนถึงจุดนั้น แต่มันชัดเจนขึ้น! ฉันจะดู Swarm เพราะฉันยังอยากรู้ว่านี่คืออะไร ขอบคุณ!
Dave

ใช่ต้องใช้เวลาจนกว่าสิ่งต่างๆจะเริ่มสมเหตุสมผล ฉันไม่รู้สึกว่าฉันสามารถหลีกเลี่ยงการอธิบายได้เร็วขึ้น
Daniel C. Sobral

ทุกอย่างมารวมกันเมื่อฉันได้ตระหนักว่า "การรีเซ็ตเป็นการกำหนดขอบเขตของความต่อเนื่อง (เช่น: ตัวแปรและคำสั่งที่จะรวม)
JeffV

1
คำอธิบายของคุณเป็นข้อมูลที่ละเอียดและไม่เข้าสู่สาระสำคัญของความเข้าใจ ตัวอย่างมีความยาวฉันไม่เข้าใจเพียงพอในย่อหน้าแรกที่จะสร้างแรงบันดาลใจให้ฉันอ่านมันทั้งหมด ก็เลยโหวตลงไป ดังนั้นแสดงข้อความหลังจากที่ฉันลงคะแนนขอให้ฉันเพิ่มความคิดเห็นดังนั้นฉันจึงปฏิบัติตาม ขอโทษสำหรับความตรงไปตรงมาของฉัน
Shelby Moore III

1
ฉันเขียนบล็อกเกี่ยวกับเรื่องนี้โดยเน้นที่การทำความเข้าใจขั้นตอนการควบคุม (โดยไม่ได้พูดถึงรายละเอียดของการใช้งาน) wherenullpoints.com/2014/04/scala-continuate.html
Alexandros

31

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

เมื่อเรียกใช้ฟังก์ชันความต่อเนื่องcf:

  1. การดำเนินการจะข้ามไปยังส่วนที่เหลือของshiftบล็อกและเริ่มต้นอีกครั้งในตอนท้ายของบล็อก
    • พารามิเตอร์ที่ส่งไปcfคือสิ่งที่shiftบล็อก "ประเมิน" เป็นเมื่อการดำเนินการดำเนินต่อไป ซึ่งอาจแตกต่างกันสำหรับการโทรทุกครั้งcf
  2. การดำเนินการจะดำเนินต่อไปจนกว่าจะสิ้นสุดการresetบล็อก (หรือจนกว่าจะมีการเรียกใช้resetหากไม่มีการบล็อก)
    • ผลลัพธ์ของresetบล็อก (หรือพารามิเตอร์ถึงreset() ถ้าไม่มีบล็อก) คือสิ่งที่cfส่งกลับ
  3. การดำเนินการจะดำเนินต่อไปcfจนกว่าจะสิ้นสุดการshiftบล็อก
  4. การดำเนินการจะข้ามไปจนจบresetบล็อก (หรือเรียกให้รีเซ็ต?)

ดังนั้นในตัวอย่างนี้ให้ตามตัวอักษรจาก A ถึง Z

reset {
  // A
  shift { cf: (Int=>Int) =>
    // B
    val eleven = cf(10)
    // E
    println(eleven)
    val oneHundredOne = cf(100)
    // H
    println(oneHundredOne)
    oneHundredOne
  }
  // C execution continues here with the 10 as the context
  // F execution continues here with 100
  + 1
  // D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven
  // G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne
}
// I

สิ่งนี้พิมพ์:

11
101

2
ฉันได้รับข้อผิดพลาดที่แจ้งว่า "ไม่สามารถคำนวณประเภทสำหรับผลลัพธ์ของฟังก์ชันที่แปลง CPS" เมื่อฉันพยายามรวบรวม .. ฉันไม่แน่ใจว่ามันไม่ใช่วิธีแก้ไขอะไร
Fabio Veronez

@Fabio Veronez เพิ่มคำสั่งส่งคืนที่ส่วนท้ายของกะ: เปลี่ยนprintln(oneHundredOne) }เป็นพูดprintln(oneHundredOne); oneHundredOne }.
folone

คำอธิบายที่ดีสำหรับไวยากรณ์ที่น่ากลัว การประกาศฟังก์ชันการต่อเนื่องนั้นแยกออกจากร่างกายอย่างแปลกประหลาด ฉันไม่เต็มใจที่จะแบ่งปันรหัสแบบเกาหัวกับคนอื่น
joeytwiddle

เพื่อหลีกเลี่ยงcannot compute type for CPS-transformed function resultข้อผิดพลาด+1ให้ปฏิบัติตามทันทีหลังจากoneHundredOne}นั้น ความคิดเห็นที่อยู่ระหว่างพวกเขาในปัจจุบันทำลายไวยากรณ์อย่างใด
lcn

9

ด้วยตัวอย่างมาตรฐานจากเอกสารการวิจัยสำหรับความต่อเนื่องที่คั่นด้วยตัวคั่นของ Scala ซึ่งได้รับการแก้ไขเล็กน้อยเพื่อให้อินพุตฟังก์ชันshiftได้รับชื่อfดังนั้นจึงไม่มีการระบุชื่ออีกต่อไป

def f(k: Int => Int): Int = k(k(k(7)))
reset(
  shift(f) + 1   // replace from here down with `f(k)` and move to `k`
) * 2

สกาล่าปลั๊กอินแปลงตัวอย่างนี้เช่นว่าการคำนวณ (ภายในอาร์กิวเมนต์ใส่ของreset) เริ่มต้นจากแต่ละshiftที่จะอุทธรณ์ของ resetถูกแทนที่ด้วยฟังก์ชั่น (เช่นf) shiftป้อนข้อมูลไปยัง

การคำนวณแทนที่จะขยับตัว (เช่นย้าย) kลงในฟังก์ชั่น ฟังก์ชั่นfปัจจัยการผลิตฟังก์ชั่นkที่k มีแทนที่การคำนวณkปัจจัยการผลิตx: Intและการคำนวณในkแทนที่ด้วยshift(f)x

f(k) * 2
def k(x: Int): Int = x + 1

ซึ่งมีผลเช่นเดียวกับ:

k(k(k(7))) * 2
def k(x: Int): Int = x + 1

หมายเหตุประเภทIntของพารามิเตอร์อินพุตx(เช่นลายเซ็นประเภทของk) ถูกกำหนดโดยลายเซ็นประเภทของพารามิเตอร์อินพุตของf.

อีกตัวอย่างที่ยืมมาพร้อมกับนามธรรมที่เทียบเท่ากับแนวคิดนั่นreadคือการป้อนข้อมูลฟังก์ชันเพื่อshift:

def read(callback: Byte => Unit): Unit = myCallback = callback
reset {
  val byte = "byte"

  val byte1 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "1 = " + byte1)
  val byte2 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "2 = " + byte2)
}

ฉันเชื่อว่าสิ่งนี้จะถูกแปลให้เทียบเท่าตรรกะของ:

val byte = "byte"

read(callback)
def callback(x: Byte): Unit {
  val byte1 = x
  println(byte + "1 = " + byte1)
  read(callback2)
  def callback2(x: Byte): Unit {
    val byte2 = x
    println(byte + "2 = " + byte1)
  }
}

ฉันหวังว่าสิ่งนี้จะทำให้เข้าใจถึงความเป็นนามธรรมที่สอดคล้องกันซึ่งค่อนข้างสับสนเมื่อนำเสนอก่อนหน้านี้ของสองตัวอย่างนี้ ตัวอย่างเช่นตัวอย่างแรกที่ยอมรับได้ถูกนำเสนอในเอกสารการวิจัยเป็นฟังก์ชันที่ไม่ระบุตัวตนแทนที่จะเป็นชื่อของฉันfดังนั้นจึงไม่ชัดเจนสำหรับผู้อ่านบางคนในทันทีว่ามันมีความคล้ายคลึงเชิงนามธรรมกับreadในเอกสารที่ยืมมาตัวอย่างที่สองที่

ความต่อเนื่องที่คั่นด้วยเหตุนี้ทำให้เกิดภาพลวงตาของการควบคุมแบบผกผันจาก "คุณเรียกฉันจากภายนอกreset" เป็น "ฉันเรียกคุณว่าภายในreset"

หมายเหตุประเภทกลับของfแต่kจะไม่จำเป็นต้องเป็นเช่นเดียวกับประเภทกลับของresetคือfมีอิสระในการประกาศประเภทผลตอบแทนใด ๆkตราบใดที่ผลตอบแทนประเภทเดียวกับf resetDitto สำหรับreadและcapture(ดูENVด้านล่าง)


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

เราสามารถบรรลุฟังก์ชันบริสุทธิ์อย่างชัดเจนด้วยการต่อเนื่องที่คั่น

def aread(env: ENV): Tuple2[Byte,ENV] {
  def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback)
  shift(read)
}
def pure(val env: ENV): ENV {
  reset {
    val (byte1, env) = aread(env)
    val env = env.println("byte1 = " + byte1)
    val (byte2, env) = aread(env)
    val env = env.println("byte2 = " + byte2)
  }
}

ฉันเชื่อว่าสิ่งนี้จะถูกแปลให้เทียบเท่าตรรกะของ:

def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV =
  env.myCallback(callback)
def pure(val env: ENV): ENV {
  read(callback,env)
  def callback(x: Tuple2[Byte,ENV]): ENV {
    val (byte1, env) = x
    val env = env.println("byte1 = " + byte1)
    read(callback2,env)
    def callback2(x: Tuple2[Byte,ENV]): ENV {
      val (byte2, env) = x
      val env = env.println("byte2 = " + byte2)
    }
  }
}

เสียงดังเนื่องจากสภาพแวดล้อมที่ไม่เหมาะสม

โปรดทราบว่า Scala ไม่มีการอนุมานประเภททั่วโลกของ Haskell และเท่าที่ฉันรู้ไม่สามารถรองรับการยกโดยนัยไปยังหน่วยงานของรัฐได้unit(เป็นกลยุทธ์ที่เป็นไปได้อย่างหนึ่งในการซ่อนสภาพแวดล้อมที่ชัดเจน) เนื่องจากการอนุมานประเภททั่วโลกของ Haskell (Hindley-Milner) ขึ้นอยู่กับที่ไม่สนับสนุนเพชรมรดกหลายเสมือน


ฉันกำลังเสนอว่าreset/ shiftถูกเปลี่ยนไป/delimit replaceและโดยการประชุมที่fและreadเป็นwithและkและcallbackเป็นreplaced, captured, หรือcontinuation callback
Shelby Moore III

ด้วยเป็นคำหลัก ป.ล. การรีเซ็ตบางส่วนของคุณมี () ซึ่งควรเป็น {} การเขียนที่ดีมาก!
nafg

@nafg ขอบคุณดังนั้นฉันจะได้นำเสนอแทนreplacement withAfaik ()ได้รับอนุญาตด้วยหรือไม่? Afaik {}คือ"ไวยากรณ์ที่มีน้ำหนักเบาของ Scala สำหรับการปิด"ซึ่งซ่อนการเรียกฟังก์ชันพื้นฐาน ตัวอย่างเช่นดูว่าฉันเขียนของ Daniel ใหม่sequenceอย่างไร(โปรดทราบว่ารหัสไม่เคยรวบรวมหรือทดสอบดังนั้นโปรดอย่าลังเลที่จะแก้ไขฉัน
Shelby Moore III

1
บล็อก - นั่นคือนิพจน์ที่มีหลายคำสั่งต้องใช้วงเล็บปีกกา
nafg

@nafg ถูกต้อง. Afaik shift resetเป็นฟังก์ชันห้องสมุดไม่ใช่คำหลัก ดังนั้น{}หรือ()สามารถนำมาใช้เมื่อฟังก์ชั่นคาดว่าเพียงหนึ่งพารามิเตอร์ Scala มีพารามิเตอร์ By-name (ดูหัวข้อ "9.5 Control Abstractions" ของ Programming ใน Scala, 2nd ed. pg. 218) โดยที่ถ้าพารามิเตอร์เป็นประเภท() => ...ที่() =>สามารถกำจัดได้ ฉันคิดว่าUnitไม่ใช่ตามชื่อเพราะบล็อกควรประเมินก่อนresetถูกเรียกใช้ แต่ฉันต้องการ{}หลายคำสั่ง การใช้งานของฉันshiftถูกต้องเพราะเห็นได้ชัดว่ามีการป้อนประเภทฟังก์ชัน
Shelby Moore III

8

ความต่อเนื่องจับสถานะของการคำนวณเพื่อเรียกใช้ในภายหลัง

ลองนึกถึงการคำนวณระหว่างการออกจากนิพจน์ shift และปล่อยให้นิพจน์รีเซ็ตเป็นฟังก์ชัน ภายในนิพจน์ shift ฟังก์ชันนี้เรียกว่า k มันคือความต่อเนื่อง คุณสามารถผ่านไปรอบ ๆ เรียกใช้ในภายหลังมากกว่าหนึ่งครั้ง

ฉันคิดว่าค่าที่ส่งคืนโดยนิพจน์การรีเซ็ตคือค่าของนิพจน์ภายในนิพจน์ shift หลัง => แต่เกี่ยวกับสิ่งนี้ฉันไม่ค่อยแน่ใจ

ดังนั้นด้วยความต่อเนื่องคุณสามารถสรุปส่วนของโค้ดที่ไม่ได้กำหนดเองและไม่ใช่เฉพาะในฟังก์ชันได้ สิ่งนี้สามารถใช้เพื่อนำโฟลว์การควบคุมที่ไม่ได้มาตรฐานไปใช้เช่น coroutining หรือ backtracking

ดังนั้นควรใช้ความต่อเนื่องในระดับระบบ การโรยผ่านรหัสแอปพลิเคชันของคุณจะเป็นสูตรสำหรับฝันร้ายที่เลวร้ายยิ่งกว่ารหัสสปาเก็ตตี้ที่เลวร้ายที่สุดโดยใช้ goto

คำเตือน:ฉันไม่มีความเข้าใจเชิงลึกเกี่ยวกับความต่อเนื่องใน Scala ฉันเพิ่งสรุปได้จากการดูตัวอย่างและทราบความต่อเนื่องจาก Scheme


5

จากมุมมองของฉันคำอธิบายที่ดีที่สุดได้รับที่นี่: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuate.html

หนึ่งในตัวอย่าง:

หากต้องการดูขั้นตอนการควบคุมที่ชัดเจนขึ้นเล็กน้อยคุณสามารถเรียกใช้ข้อมูลโค้ดนี้ได้:

reset {
    println("A")
    shift { k1: (Unit=>Unit) =>
        println("B")
        k1()
        println("C")
    }
    println("D")
    shift { k2: (Unit=>Unit) =>
        println("E")
        k2()
        println("F")
    }
    println("G")
}

นี่คือผลลัพธ์ที่รหัสด้านบนสร้าง:

A
B
D
E
G
F
C

1

บทความอื่น (ล่าสุด - พฤษภาคม 2016) เกี่ยวกับความต่อเนื่องของ Scala คือ:
" การเดินทางในเวลา Scala: CPS ในสกาล่า (สกาล่าของความต่อเนื่อง) " โดย Shivansh Srivastava (shiv4nsh )
นอกจากนี้ยังหมายถึงจิม McBeath 's บทความที่กล่าวถึงในDmitry Bespalov ' s คำตอบ

แต่ก่อนหน้านั้นจะอธิบายความต่อเนื่องดังนี้:

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

เพื่ออธิบายเพิ่มเติมเราสามารถมีหนึ่งในตัวอย่างที่คลาสสิกที่สุด

สมมติว่าคุณอยู่ในครัวหน้าตู้เย็นนึกถึงแซนด์วิช คุณใช้ความต่อเนื่องตรงนั้นและติดไว้ในกระเป๋าของคุณ
จากนั้นคุณจะนำไก่งวงและขนมปังออกจากตู้เย็นและทำแซนวิชด้วยตัวเองซึ่งตอนนี้นั่งอยู่บนเคาน์เตอร์
คุณเรียกความต่อเนื่องในกระเป๋าของคุณและคุณพบว่าตัวเองยืนอยู่หน้าตู้เย็นอีกครั้งโดยคิดถึงแซนด์วิช แต่โชคดีที่มีแซนวิชวางอยู่บนเคาน์เตอร์และวัสดุที่ใช้ทำทั้งหมดก็หายไป ดังนั้นคุณกินมัน :-)

ในคำอธิบายนี้sandwichเป็นส่วนหนึ่งของข้อมูลโปรแกรม (เช่นอ็อบเจ็กต์บนฮีป) และแทนที่จะเรียกmake sandwichรูทีน "" แล้วส่งกลับบุคคลนั้นเรียกว่าmake sandwich with current continuationรูทีน "" ซึ่งสร้างแซนวิชแล้วดำเนินการต่อในที่ที่การดำเนินการ ทิ้งไว้

ดังที่ได้กล่าวไว้ในเดือนเมษายน 2014 สำหรับ Scala 2.11.0-RC1

เรากำลังมองหาผู้ดูแลจะใช้เวลามากกว่าโมดูลต่อไปนี้: สกาล่าแกว่ง , Scala-ต
2.12 จะไม่รวมถึงพวกเขาหากไม่มีผู้ดูแลใหม่พบ
เรามีแนวโน้มที่จะรักษาโมดูลอื่น ๆ ไว้ (scala-xml, scala-parser-combinators) แต่ความช่วยเหลือยังคงได้รับการชื่นชมอย่างมาก


0

ความต่อเนื่องของสกาล่าผ่านตัวอย่างที่มีความหมาย

ให้เรากำหนดfrom0to10ว่าเป็นการแสดงความคิดของการวนซ้ำจาก 0 ถึง 10:

def from0to10() = shift { (cont: Int => Unit) =>
   for ( i <- 0 to 10 ) {
     cont(i)
   }
}

ตอนนี้

reset {
  val x = from0to10()
  print(s"$x ")
}
println()

พิมพ์:

0 1 2 3 4 5 6 7 8 9 10 

ในความเป็นจริงเราไม่ต้องการx:

reset {
  print(s"${from0to10()} ")
}
println()

พิมพ์ผลลัพธ์เดียวกัน

และ

reset {
  print(s"(${from0to10()},${from0to10()}) ")
}
println()

พิมพ์คู่ทั้งหมด:

(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10) 

ตอนนี้ทำงานอย่างไร?

มีเป็นรหัสเรียกว่า , from0to10และรหัสโทร ในกรณีนี้เป็นบล็อกที่ตามresetมา หนึ่งในพารามิเตอร์ที่ส่งไปยังรหัสที่เรียกคือที่อยู่สำหรับส่งคืนที่แสดงว่าส่วนใดของรหัสการโทรที่ยังไม่ได้ดำเนินการ (**) ส่วนหนึ่งของรหัสเรียกว่าเป็นความต่อเนื่อง รหัสที่เรียกสามารถทำกับพารามิเตอร์นั้นได้ทุกอย่างที่ตัดสินใจ: ส่งการควบคุมไปที่มันหรือเพิกเฉยหรือเรียกมันหลาย ๆ ครั้ง ในที่นี้from0to10เรียกความต่อเนื่องนั้นสำหรับแต่ละจำนวนเต็มในช่วง 0..10

def from0to10() = shift { (cont: Int => Unit) =>
   for ( i <- 0 to 10 ) {
     cont(i) // call the continuation
   }
}

แต่ความต่อเนื่องสิ้นสุดลงที่ไหน? นี้เป็นสิ่งสำคัญเพราะที่ผ่านมาจากผลตอบแทนต่อเนื่องควบคุมรหัสเรียกว่า,return from0to10ใน Scala จะสิ้นสุดที่ไฟล์resetบล็อกสิ้นสุด (*)

cont: Int => Unitตอนนี้เราจะเห็นว่าความต่อเนื่องที่มีการประกาศให้เป็น ทำไม? เราเรียกใช้from0to10เป็นval x = from0to10()และIntเป็นประเภทของค่าที่ไปxเป็นชนิดของค่าที่จะไปUnitหมายความว่าบล็อกหลังresetต้องไม่ส่งคืนค่า (มิฉะนั้นจะมีข้อผิดพลาดประเภท) โดยทั่วไปมีลายเซ็น 4 ประเภท ได้แก่ อินพุตฟังก์ชันอินพุตความต่อเนื่องผลลัพธ์ความต่อเนื่องผลลัพธ์ของฟังก์ชัน ทั้งสี่ต้องตรงกับบริบทการร้องขอ

ด้านบนเราพิมพ์คู่ค่า ให้เราพิมพ์สูตรคูณ แต่เราจะเอาท์พุทอย่างไร\nหลังจากแต่ละแถวได้อย่างไร?

ฟังก์ชั่นbackช่วยให้เราระบุสิ่งที่ต้องทำเมื่อการควบคุมส่งกลับจากความต่อเนื่องไปจนถึงรหัสที่เรียกมัน

def back(action: => Unit) = shift { (cont: Unit => Unit) =>
  cont()
  action
}

backครั้งแรกเรียกความต่อเนื่องของมันแล้วดำเนินการดำเนินการ

reset {
  val i = from0to10()
  back { println() }
  val j = from0to10
  print(f"${i*j}%4d ") // printf-like formatted i*j
}

มันพิมพ์:

   0    0    0    0    0    0    0    0    0    0    0 
   0    1    2    3    4    5    6    7    8    9   10 
   0    2    4    6    8   10   12   14   16   18   20 
   0    3    6    9   12   15   18   21   24   27   30 
   0    4    8   12   16   20   24   28   32   36   40 
   0    5   10   15   20   25   30   35   40   45   50 
   0    6   12   18   24   30   36   42   48   54   60 
   0    7   14   21   28   35   42   49   56   63   70 
   0    8   16   24   32   40   48   56   64   72   80 
   0    9   18   27   36   45   54   63   72   81   90 
   0   10   20   30   40   50   60   70   80   90  100 

ตอนนี้ถึงเวลาแล้วที่สมองจะเปลี่ยนไป มีสองคำเรียกร้องของfrom0to10. อะไรคือความต่อเนื่องสำหรับครั้งแรกfrom0to10? เป็นไปตามการเรียกใช้from0to10ในรหัสไบนารีแต่ในซอร์สโค้ดจะมีคำสั่งมอบหมายval i =ด้วย มันจบลงที่resetบล็อกปลาย แต่ท้ายของบล็อกไม่ได้กลับการควบคุมไปก่อนreset from0to10การสิ้นสุดของresetบล็อกจะส่งคืนการควบคุมไปยังลำดับที่ 2 from0to10ซึ่งในที่สุดจะส่งคืนการควบคุมไปยังbackและbackส่งคืนการควบคุมไปยังการเรียกใช้ครั้งแรกของfrom0to10ผลตอบแทนควบคุมการภาวนาแรกของเมื่อครั้งแรก (ใช่! 1!) from0to10ออกทั้งหมดresetบล็อกจะถูกออก

เรียกวิธีการคืนการควบคุมกลับดังกล่าว ย้อนรอยซึ่งเป็นเทคนิคที่เก่าแก่มากซึ่งเป็นที่รู้จักอย่างน้อยก็ในสมัยของ Prolog และอนุพันธ์ Lisp ที่เน้น AI

ชื่อresetและการshiftเรียกชื่อผิด ชื่อเหล่านี้ควรถูกทิ้งไว้สำหรับการดำเนินการระดับบิต resetกำหนดขอบเขตความต่อเนื่องและshiftรับช่วงต่อจาก call stack

หมายเหตุ (s)

(*) ใน Scala ความต่อเนื่องจะสิ้นสุดลงเมื่อresetบล็อกสิ้นสุดลง อีกแนวทางหนึ่งที่เป็นไปได้คือปล่อยให้มันสิ้นสุดลงเมื่อฟังก์ชันสิ้นสุดลง

(**) หนึ่งในพารามิเตอร์ของรหัสที่เรียกคือที่อยู่สำหรับส่งคืนที่แสดงว่าส่วนใดของรหัสการโทรที่ยังไม่ได้ดำเนินการ ใน Scala จะใช้ลำดับของที่อยู่ผู้ส่งคืนสำหรับสิ่งนั้น เท่าไหร่? ที่อยู่สำหรับส่งคืนทั้งหมดที่วางไว้ในกลุ่มการโทรตั้งแต่เข้าสู่resetบล็อก


UPD ตอนที่ 2 การ ละทิ้งความต่อเนื่อง: การกรอง

def onEven(x:Int) = shift { (cont: Unit => Unit) =>
  if ((x&1)==0) {
    cont() // call continuation only for even numbers
  }
}
reset {
  back { println() }
  val x = from0to10()
  onEven(x)
  print(s"$x ")
}

สิ่งนี้พิมพ์:

0 2 4 6 8 10 

ให้เราแยกการดำเนินการที่สำคัญสองอย่างออกจากการดำเนินการต่อเนื่อง ( fail()) และส่งต่อการควบคุมไปยัง ( succ()):

// fail: just discard the continuation, force control to return back
def fail() = shift { (cont: Unit => Unit) => }
// succ: does nothing (well, passes control to the continuation), but has a funny signature
def succ():Unit @cpsParam[Unit,Unit] = { }
// def succ() = shift { (cont: Unit => Unit) => cont() }

ทั้งสองเวอร์ชันsucc()(ด้านบน) ใช้งานได้ ปรากฎว่าshiftมีลายเซ็นตลก ๆ และแม้ว่าจะsucc()ไม่ทำอะไรเลย แต่ก็ต้องมีลายเซ็นนั้นสำหรับความสมดุลของประเภท

reset {
  back { println() }
  val x = from0to10()
  if ((x&1)==0) {
    succ()
  } else {
    fail()
  }
  print(s"$x ")
}

ตามที่คาดไว้มันจะพิมพ์ออกมา

0 2 4 6 8 10

ภายในฟังก์ชันsucc()ไม่จำเป็น:

def onTrue(b:Boolean) = {
  if(!b) {
    fail()
  }
}
reset {
  back { println() }
  val x = from0to10()
  onTrue ((x&1)==0)
  print(s"$x ")
}

อีกครั้งมันพิมพ์

0 2 4 6 8 10

ตอนนี้ให้เรากำหนดonOdd()ผ่านonEven():

// negation: the hard way
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
  try {
    reset {
      onEven(x)
      throw new ControlTransferException() // return is not allowed here
    }
    cont()
  } catch {
    case e: ControlTransferException =>
    case t: Throwable => throw t
  }
}
reset {
  back { println() }
  val x = from0to10()
  onOdd(x)
  print(s"$x ")
}

ด้านบนถ้าxเป็นคู่ข้อยกเว้นจะถูกโยนทิ้งและไม่เรียกความต่อเนื่อง ถ้าxเป็นเลขคี่ข้อยกเว้นจะไม่ถูกโยนทิ้งและเรียกความต่อเนื่อง รหัสด้านบนพิมพ์:

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