Liên tục Scala là gì và tại sao lại sử dụng chúng?


85

Tôi vừa hoàn thành Lập trình trong Scala và tôi đang xem xét những thay đổi giữa Scala 2.7 và 2.8. Cái có vẻ quan trọng nhất là plugin liên tục, nhưng tôi không hiểu nó hữu ích cho việc gì hoặc nó hoạt động như thế nào. Tôi đã thấy rằng nó tốt cho I / O không đồng bộ, nhưng tôi không thể tìm ra lý do. Một số tài nguyên phổ biến hơn về chủ đề này là:

Và câu hỏi này trên Stack Overflow:

Thật không may, không có tham chiếu nào trong số các tham chiếu này cố gắng xác định tính liên tục là gì hoặc các chức năng shift / reset phải làm gì và tôi không tìm thấy bất kỳ tham chiếu nào làm được. Tôi không thể đoán bất kỳ ví dụ nào trong các bài viết được liên kết hoạt động (hoặc chúng hoạt động như thế nào), vì vậy một cách để giúp tôi có thể xem từng dòng một trong những mẫu đó. Ngay cả điều đơn giản này từ bài báo thứ ba:

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

Tại sao kết quả lại là 8? Điều đó có thể sẽ giúp tôi bắt đầu.


Câu trả lời:


38

Blog của tôi giải thích những gì resetshiftlàm, vì vậy bạn có thể muốn đọc lại.

Một nguồn tốt khác, mà tôi cũng chỉ ra trong blog của mình, là mục nhập Wikipedia về kiểu chuyển tiếp tiếp tục . Đó là, cho đến nay, là rõ ràng nhất về chủ đề, mặc dù nó không sử dụng cú pháp Scala và phần tiếp theo được thông qua một cách rõ ràng.

Bài báo về sự liên tục được phân định, mà tôi liên kết đến trong blog của mình nhưng dường như đã bị hỏng, đưa ra nhiều ví dụ về cách sử dụng.

Nhưng tôi nghĩ ví dụ tốt nhất về khái niệm liên tục được phân định là Scala Swarm. Trong đó, thư viện dừng việc thực thi mã của bạn tại một thời điểm, và phần tính toán còn lại trở thành phần tiếp theo. Sau đó, thư viện làm một việc gì đó - trong trường hợp này là chuyển tính toán sang máy chủ khác và trả về kết quả (giá trị của biến đã được truy cập) cho phép tính đã bị dừng.

Bây giờ, bạn không hiểu ngay cả ví dụ đơn giản trên trang Scala, hãy đọc blog của tôi. Trong đó, tôi chỉ quan tâm đến việc giải thích những điều cơ bản này, tại sao lại có kết quả 8.


Tôi đã đọc lại mục blog của bạn và lần này tôi mắc kẹt với nó - tôi nghĩ rằng tôi có ý tưởng tốt hơn về những gì đang xảy ra. Tôi không nhận được nhiều từ trang Wikipedia (tôi đã biết liên tục Lisp) nhưng phong cách đặt lại / thay đổi trì hoãn hoặc bất cứ điều gì được gọi là nó đã khiến tôi gặp khó khăn. Đối với những người thiếu kiên nhẫn (tức là bản thân tôi), mô tả của bạn vẫn ổn nhưng mọi người sẽ phải đảm bảo gắn bó với nó cho đến "Kết quả của việc đặt lại là kết quả của mã bên trong dịch chuyển." đoạn ... Tôi đã mất hy vọng cho đến thời điểm đó nhưng nó trở nên rõ ràng hơn! Tôi sẽ xem qua Swarm vì tôi vẫn tò mò không biết cái này dùng để làm gì. Cám ơn!
Dave

Vâng, phải mất thời gian cho đến khi mọi thứ bắt đầu có ý nghĩa. Tôi không cảm thấy mình có thể giải thích nhanh hơn.
Daniel C. Sobral

Mọi chuyện đến với nhau cho tôi khi tôi đi đến nhận thức rằng "reset delimits phạm vi của việc tiếp tục (ví dụ: các biến và báo cáo để được bao gồm.).
JeffV

1
Lời giải thích của bạn dài dòng và không đi đến bản chất của sự hiểu biết. Các ví dụ dài, tôi không hiểu đủ trong các đoạn đầu tiên để truyền cảm hứng cho tôi để đọc hết. Vì vậy, tôi đã bỏ phiếu nó xuống. SO hiển thị một tin nhắn sau khi tôi bỏ phiếu, yêu cầu tôi thêm nhận xét, vì vậy tôi đang tuân thủ. Xin lỗi vì sự thẳng thắn của tôi.
Shelby Moore III,

1
Tôi đã viết blog về điều này với trọng tâm là tìm hiểu luồng kiểm soát (không thảo luận về chi tiết của việc triển khai). wherenullpoints.com/2014/04/scala-continutions.html
Alexandros

31

Tôi thấy những giải thích hiện có kém hiệu quả trong việc giải thích khái niệm hơn tôi mong đợi. Tôi hy vọng điều này rõ ràng (và chính xác.) Tôi vẫn chưa sử dụng các phép liên tục.

Khi một hàm tiếp tục cfđược gọi:

  1. Việc thực thi bỏ qua phần còn lại của shiftkhối và bắt đầu lại ở cuối khối
    • tham số được truyền tới cflà thứ mà shiftkhối "đánh giá" khi quá trình thực thi tiếp tục. điều này có thể khác đối với mọi cuộc gọi đếncf
  2. Việc thực thi tiếp tục cho đến khi kết thúc resetkhối (hoặc cho đến khi có lệnh gọi đến resetnếu không có khối)
    • kết quả của resetkhối (hoặc tham số tới reset() nếu không có khối) là kết cfquả trả về
  3. Việc thực thi tiếp tục cfcho đến khi kết thúc shiftkhối
  4. Quá trình thực thi sẽ bỏ qua cho đến khi kết thúc resetkhối (hoặc lệnh gọi đặt lại?)

Vì vậy, trong ví dụ này, hãy làm theo các chữ cái từ A đến 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

Bản in này:

11
101

2
Tôi đã có một lỗi nói rằng "không thể loại tính toán cho kết quả chức năng CPS-chuyển" khi tôi đã cố gắng để biên dịch nó .. Tôi không chắc chắn những gì nó không phải là làm thế nào để sửa chữa nó
Fabio Veronez

@Fabio Veronez Thêm câu lệnh trả lại vào cuối ca làm việc: thay đổi println(oneHundredOne) }thành, nói println(oneHundredOne); oneHundredOne },.
folone

Giải thích tốt cho một cú pháp khủng khiếp. Phần khai báo của hàm tiếp tục được tách ra khỏi phần thân của nó một cách kỳ lạ. Tôi sẽ miễn cưỡng chia sẻ mã đầu như vậy với người khác.
joeytwiddle

Để tránh cannot compute type for CPS-transformed function resultlỗi, +1hãy làm theo ngay sau đây oneHundredOne}. Các bình luận hiện đang cư trú giữa chúng phá vỡ ngữ pháp bằng cách nào đó.
lcn

9

Đưa ra ví dụ chính tắc từ bài báo nghiên cứu cho các phép liên tục được phân tách của Scala, được sửa đổi một chút để đầu vào hàm shiftđược đặt tên fvà do đó không còn ẩn danh nữa.

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

Plugin Scala biến đổi ví dụ này sao cho phép tính (trong đối số đầu vào của reset) bắt đầu từ từng shiftđến lệnh gọi của resetđược thay thế bằng hàm (ví dụ f) đầu vào shift.

Phép tính được thay thế được chuyển (tức là được di chuyển) thành một hàm k. Hàm fnhập hàm k, trong đó k chứa phép tính được thay thế, các kđầu vào x: Intvà phép tính trong kthay thế shift(f)bằng x.

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

Có tác dụng tương tự như:

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

Lưu ý rằng kiểu Intcủa tham số đầu vào x(tức là kiểu chữ ký của k) được cung cấp bởi chữ ký kiểu của tham số đầu vào của f.

Một ví dụ mượn khác với khái niệm trừu tượng tương đương về mặt khái niệm, tức readlà đầu vào của hàm cho 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)
}

Tôi tin rằng điều này sẽ được dịch theo logic tương đương với:

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

Tôi hy vọng điều này sẽ làm sáng tỏ sự trừu tượng chung mạch lạc mà phần nào đã bị xáo trộn bởi phần trình bày trước của hai ví dụ này. Ví dụ, ví dụ đầu tiên kinh điển được thể hiện trong báo cáo nghiên cứu như một chức năng ẩn danh, thay vì tên của tôi f, do đó nó không được ngay lập tức rõ ràng đối với một số độc giả rằng đó là một cách trừu tượng tương tự như readtrong mượn ví dụ thứ hai.

Do đó, sự liên tục được phân định tạo ra ảo giác về sự đảo ngược-kiểm soát từ "bạn gọi tôi từ bên ngoài reset" đến "tôi gọi bạn bên trong reset".

Lưu ý rằng kiểu trả về flà, nhưng kkhông, bắt buộc phải giống với kiểu trả về reset, tức là fcó quyền tự do khai báo bất kỳ kiểu trả về nào kmiễn là ftrả về cùng kiểu reset. Ditto cho readcapture(xem thêm ENVbên dưới).


Các phép liên tục được phân định không hoàn toàn đảo ngược việc kiểm soát trạng thái, ví dụ readcallbackkhông phải là các chức năng thuần túy. Do đó, người gọi không thể tạo các biểu thức trong suốt tham chiếu và do đó không có quyền kiểm soát khai báo (còn gọi là trong suốt) đối với ngữ nghĩa mệnh lệnh dự kiến .

Chúng ta có thể đạt được một cách rõ ràng các hàm thuần túy với các phép liên tục được phân định.

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

Tôi tin rằng điều này sẽ được dịch theo logic tương đương với:

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

Điều này đang trở nên ồn ào, bởi vì môi trường rõ ràng.

Lưu ý tiếp theo, Scala không có suy luận kiểu toàn cục của Haskell và do đó theo tôi biết không thể hỗ trợ việc nâng ngầm lên đơn nguyên trạng thái unit(như một chiến lược khả thi để ẩn môi trường rõ ràng), vì suy luận kiểu toàn cục (Hindley-Milner) của Haskell phụ thuộc vào việc không hỗ trợ đa thừa kế ảo kim cương .


Tôi đang đề xuất rằng reset/ shiftđược thay đổi thành delimit/ replace. Và theo quy ước, đó freadđược with, và kcallbackđược replaced, captured, continuation, hoặc callback.
Shelby Moore III,

với là một từ khóa. PS Một số thiết lập lại của bạn có () nên là {} Dù sao thì bản ghi rất tuyệt vời!
nafg

@nafg cảm ơn bạn, vì vậy tôi sẽ cầu hôn replacementthay vì with. Afaik, ()cũng được phép? Afaik, {}"cú pháp nhẹ của Scala cho các bao đóng" , ẩn một lệnh gọi hàm cơ bản. Ví dụ: hãy xem cách tôi đã viết lại bài viết của Danielsequence (lưu ý rằng mã chưa bao giờ được biên dịch hoặc thử nghiệm, vì vậy vui lòng sửa cho tôi).
Shelby Moore III

1
Một khối - nghĩa là một biểu thức chứa nhiều câu lệnh - yêu cầu dấu ngoặc nhọn.
nafg

@nafg, chính xác. Afaik shift resetlà các hàm thư viện, không phải từ khóa. Do đó {}hoặc ()có thể được sử dụng khi hàm chỉ mong đợi một tham số . Scala có các tham số By-name (xem phần "9.5 Tóm tắt điều khiển" của Lập trình trong Scala, ấn bản thứ 2 trang 218), trong đó nếu tham số là loại () => ...thì () =>có thể bị loại bỏ. Tôi giả sử Unitchứ không phải theo tên vì khối sẽ đánh giá trước khi resetđược gọi, nhưng tôi cần {}nhiều câu lệnh. Cách sử dụng của tôi cho shiftlà chính xác, bởi vì nó rõ ràng là đầu vào một loại hàm.
Shelby Moore III,

8

Tiếp tục ghi lại trạng thái của một tính toán, sẽ được gọi sau này.

Hãy nghĩ về phép tính giữa việc rời khỏi biểu thức shift và để lại biểu thức đặt lại dưới dạng một hàm. Bên trong biểu thức shift, hàm này được gọi là k, nó là phần tiếp diễn. Bạn có thể chuyển nó xung quanh, gọi nó sau, thậm chí nhiều hơn một lần.

Tôi nghĩ giá trị được trả về bởi biểu thức đặt lại là giá trị của biểu thức bên trong biểu thức shift sau dấu =>, nhưng về điều này tôi không chắc lắm.

Vì vậy, với sự liên tục, bạn có thể gói một đoạn mã khá tùy ý và không cục bộ trong một hàm. Điều này có thể được sử dụng để thực hiện quy trình kiểm soát phi tiêu chuẩn, chẳng hạn như điều tra hoặc bẻ khóa ngược.

Vì vậy, sự liên tục nên được sử dụng ở mức hệ thống. Rắc chúng qua mã ứng dụng của bạn sẽ là một công thức chắc chắn cho những cơn ác mộng, tệ hơn nhiều so với mã spaghetti tồi tệ nhất sử dụng goto từng có.

Tuyên bố từ chối trách nhiệm: Tôi không có hiểu biết sâu về sự liên tục trong Scala, tôi chỉ suy ra nó khi xem các ví dụ và biết sự liên tục từ Scheme.


5

Theo quan điểm của tôi, lời giải thích tốt nhất đã được đưa ra ở đây: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continutions.html

Một trong những ví dụ:

Để xem quy trình điều khiển rõ ràng hơn một chút, bạn có thể thực thi đoạn mã này:

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

Đây là kết quả mà đoạn mã trên tạo ra:

A
B
D
E
G
F
C

1

Một bài báo khác (gần đây hơn - tháng 5 năm 2016) về sự liên tục của Scala là:
" Du hành thời gian ở Scala: CPS trong Scala (sự tiếp tục của scala) " của Shivansh Srivastava ( shiv4nsh) .
Nó cũng đề cập đến bài báo của Jim McBeath được đề cập trong câu trả lời của Dmitry Bespalov .

Nhưng trước đó, nó mô tả Tiếp tục như vậy:

Phần tiếp theo là một biểu diễn trừu tượng của trạng thái điều khiển của một chương trình máy tính .
Vì vậy, những gì nó thực sự có nghĩa là nó là một cấu trúc dữ liệu đại diện cho quá trình tính toán tại một điểm nhất định trong quá trình thực hiện của quá trình; cấu trúc dữ liệu đã tạo có thể được truy cập bằng ngôn ngữ lập trình, thay vì bị ẩn trong môi trường thời gian chạy.

Để giải thích thêm, chúng ta có thể có một trong những ví dụ cổ điển nhất,

Giả sử bạn đang ở trong bếp trước tủ lạnh và nghĩ về một chiếc bánh sandwich. Bạn lấy một đoạn tiếp theo ngay tại đó và bỏ vào túi.
Sau đó, bạn lấy một ít gà tây và bánh mì trong tủ lạnh và làm cho mình một chiếc bánh sandwich, bây giờ đang ngồi trên quầy.
Bạn gọi tiếp tục trong túi, và bạn lại thấy mình đứng trước tủ lạnh, nghĩ về một chiếc bánh sandwich. Nhưng may mắn thay, có một chiếc bánh sandwich trên quầy, và tất cả các nguyên liệu dùng để làm nó đã hết sạch. Vì vậy, bạn ăn nó. :-)

Trong mô tả này, phần sandwichnày là một phần của dữ liệu chương trình (ví dụ: một đối tượng trên heap) và thay vì gọi một make sandwichquy trình “” rồi quay lại, người ta gọi một make sandwich with current continuationquy trình “”, tạo ra bánh sandwich và sau đó tiếp tục nơi thực thi rời khỏi.

Điều đó đang được nói, như được công bố vào tháng 4 năm 2014 cho Scala 2.11.0-RC1

Chúng tôi đang tìm kiếm người bảo trì để tiếp quản các mô-đun sau: scala-swing , scala-continue .
2.12 sẽ không bao gồm chúng nếu không tìm thấy người bảo trì mới .
Chúng tôi có thể sẽ tiếp tục duy trì các mô-đun khác (scala-xml, scala-parser-combinators), nhưng sự trợ giúp vẫn được đánh giá cao.


0

Tiếp tục Scala qua các ví dụ có ý nghĩa

Hãy để chúng tôi xác định from0to10thể hiện ý tưởng lặp lại từ 0 đến 10:

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

Hiện nay,

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

bản in:

0 1 2 3 4 5 6 7 8 9 10 

Trên thực tế, chúng ta không cần x:

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

in ra cùng một kết quả.

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

in tất cả các cặp:

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

Bây giờ, nó hoạt động như thế nào?

Có các mã được gọi là , from0to10mã gọi . Trong trường hợp này, nó là khối theo sau reset. Một trong các tham số được truyền cho mã được gọi là địa chỉ trả về cho biết phần nào của mã gọi vẫn chưa được thực thi (**). Đó là một phần của mã gọi là sự tiếp nối . Mã được gọi có thể làm với tham số đó bất cứ điều gì nó quyết định: chuyển quyền điều khiển cho nó, hoặc bỏ qua hoặc gọi nó nhiều lần. Ở đây from0to10gọi sự tiếp tục đó cho mỗi số nguyên trong phạm vi 0..10.

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

Nhưng sự tiếp tục kết thúc ở đâu? Điều này rất quan trọng vì cuối cùng returntừ phần tiếp diễn trả lại quyền điều khiển cho mã được gọi from0to10,. Trong Scala, nó kết thúc khi resetkhối kết thúc (*).

Bây giờ, chúng ta thấy rằng phần tiếp diễn được khai báo là cont: Int => Unit. Tại sao? Chúng tôi gọi from0to10val x = from0to10(), và Intlà loại giá trị đi đến x. Unitcó nghĩa là khối sau resetphải trả về không có giá trị nào (nếu không sẽ có lỗi kiểu). Nói chung, có 4 loại chữ ký: đầu vào hàm, đầu vào tiếp tục, kết quả tiếp tục, kết quả hàm. Cả bốn phải phù hợp với ngữ cảnh gọi.

Ở trên, chúng tôi đã in các cặp giá trị. Hãy để chúng tôi in bảng cửu chương. Nhưng làm thế nào để chúng ta xuất ra \nsau mỗi hàng?

Hàm backcho phép chúng tôi chỉ định những gì phải thực hiện khi điều khiển quay trở lại, từ phần tiếp tục đến mã đã gọi nó.

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

backđầu tiên gọi sự tiếp tục của nó và sau đó thực hiện hành động .

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

Nó in:

   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 

Chà, bây giờ là lúc cho một số người điên đầu. Có hai cách gọi from0to10. Phần tiếp theo cho phần đầu tiên là from0to10gì? Nó tuân theo lệnh gọi from0to10trong mã nhị phân , nhưng trong mã nguồn nó cũng bao gồm câu lệnh gán val i =. Nó kết thúc khi resetkhối kết thúc, nhưng phần cuối của resetkhối không trả lại quyền kiểm soát cho khối đầu tiên from0to10. Phần cuối của resetkhối trả lại quyền kiểm soát cho khối thứ 2 from0to10, đến lượt nó cuối cùng trả lại quyền kiểm soát backvà nó backtrả lại quyền kiểm soát cho lệnh gọi đầu tiên from0to10. Khi khối đầu tiên (có! 1!) from0to10Thoát ra, toàn bộ resetkhối sẽ được thoát.

Phương pháp trả lại quyền điều khiển như vậy được gọi là backtracking , nó là một kỹ thuật rất cũ, được biết đến ít nhất là từ thời Prolog và các dẫn xuất Lisp hướng AI.

Tên resetshiftlà những người viết sai. Những tên này tốt hơn nên được để lại cho các hoạt động bitwise. resetxác định ranh giới tiếp tục và shifttiếp tục từ ngăn xếp cuộc gọi.

Ghi chú

(*) Trong Scala, phần tiếp diễn kết thúc khi resetkhối kết thúc. Một cách tiếp cận khác có thể là để nó kết thúc khi hàm kết thúc.

(**) Một trong các tham số của mã được gọi là địa chỉ trả về cho biết phần nào của mã gọi chưa được thực thi. Vâng, trong Scala, một chuỗi các địa chỉ trả về được sử dụng cho việc đó. Bao nhiêu? Tất cả các địa chỉ trả về được đặt trên ngăn xếp cuộc gọi kể từ khi nhập resetkhối.


UPD Phần 2 Loại bỏ tiếp tục: Lọc

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

Bản in này:

0 2 4 6 8 10 

Hãy để chúng tôi xác định hai hoạt động quan trọng: loại bỏ tiếp tục ( fail()) và chuyển quyền kiểm soát cho nó ( 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() }

Cả hai phiên bản của succ()(ở trên) đều hoạt động. Nó chỉ ra rằng shiftcó một chữ ký vui nhộn, và mặc dù succ()không có gì, nó phải có chữ ký đó để cân bằng loại.

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

như mong đợi, nó in

0 2 4 6 8 10

Trong một hàm, succ()không cần thiết:

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

một lần nữa, nó in

0 2 4 6 8 10

Bây giờ, chúng ta hãy xác định onOdd()thông qua 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 ")
}

Ở trên, nếu xlà chẵn, một ngoại lệ được ném ra và phần tiếp tục không được gọi; nếu xlà số lẻ, ngoại lệ không được ném ra và phần tiếp tục được gọi. Đoạn mã trên in ra:

1 3 5 7 9 
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.