Zgodnie z pierwotnymi komentarzami, idiomatyczny sposób na wcześniejsze wyjście z OCaml jest kontynuacją. W miejscu, w którym chcesz wrócić do wcześniejszego powrotu, tworzysz kontynuację i przekazujesz ją do kodu, który może wrócić wcześniej. Jest to bardziej ogólne niż etykiety dla pętli, ponieważ możesz wyjść z niemal wszystkiego, co ma dostęp do kontynuacji.
Ponadto, jak podano w komentarzach, należy zwrócić uwagę na użycie raise_notrace
dla wyjątków, których ślad nigdy nie powinien zostać wygenerowany w środowisku wykonawczym.
A „naiwny” Pierwsza próba:
module Continuation :
sig
(* This is the flaw with this approach: there is no good choice for
the result type. *)
type 'a cont = 'a -> unit
(* with_early_exit f passes a function "k" to f. If f calls k,
execution resumes as if with_early_exit completed
immediately. *)
val with_early_exit : ('a cont -> 'a) -> 'a
end =
struct
type 'a cont = 'a -> unit
(* Early return is implemented by throwing an exception. The ref
cell is used to store the value with which the continuation is
called - this is a way to avoid having to generate an exception
type that can store 'a for each 'a this module is used with. The
integer is supposed to be a unique identifier for distinguishing
returns to different nested contexts. *)
type 'a context = 'a option ref * int64
exception Unwind of int64
let make_cont ((cell, id) : 'a context) =
fun result -> cell := Some result; raise_notrace (Unwind id)
let generate_id =
let last_id = ref 0L in
fun() -> last_id := Int64.add !last_id 1L; !last_id
let with_early_exit f =
let id = generate_id() in
let cell = ref None in
let cont : 'a cont = make_cont (cell, id) in
try
f cont
with Unwind i when i = id ->
match !cell with
| Some result -> result
(* This should never happen... *)
| None -> failwith "with_early_exit"
end
let _ =
let nested_function i k = k 15; i in
Continuation.with_early_exit (nested_function 42)
|> string_of_int
|> print_endline
Jak widać, powyższe narzędzia wczesne wyjście ukrywając wyjątek. Kontynuacja jest właściwie częściowo zastosowaną funkcją, która zna unikalny identyfikator kontekstu, dla którego została utworzona, i ma komórkę odniesienia do przechowywania wartości wyniku, podczas gdy wyjątek jest rzucany do tego kontekstu. Powyższy kod drukuje 15. Możesz kontynuować kontynuację k
tak głęboko, jak chcesz. Możesz również zdefiniować funkcję f
natychmiast w miejscu, w którym zostanie przekazana do with_early_exit
, dając efekt podobny do etykiety w pętli. Używam tego bardzo często.
Problem z powyższym jest wynikiem typu 'a cont
, który arbitralnie ustawiłem na unit
. W rzeczywistości funkcja typu 'a cont
nigdy nie wraca, więc chcemy, aby zachowywała się ona jak raise
- może być użyta tam, gdzie oczekuje się dowolnego typu. Jednak to nie działa od razu. Jeśli zrobisz coś takiego, jak type ('a, 'b) cont = 'a -> 'b
, i przekażesz to do funkcji zagnieżdżonej, sprawdzacz typów określi typ dla 'b
w jednym kontekście, a następnie zmusi Cię do wywołania kontynuacji tylko w kontekstach tego samego typu, tj. Nie będziesz w stanie robić takie rzeczy jak
(if ... then 3 else k 15)
...
(if ... then "s" else k 16)
ponieważ pierwsze siły ekspresji 'b
być int
, ale drugi wymaga 'b
być string
.
Aby rozwiązać ten problem, że trzeba zapewnić funkcje analogiczne do raise
wczesnego powrotu, to znaczy
(if ... then 3 else throw k 15)
...
(if ... then "s" else throw k 16)
Oznacza to, odsuwając się od czystych przedłużeń. Musimy un-częściowo stosuje make_cont
powyżej (i przemianował ją na throw
) i przekazać nagi kontekst wokół zamiast:
module BetterContinuation :
sig
type 'a context
val throw : 'a context -> 'a -> _
val with_early_exit : ('a context -> 'a) -> 'a
end =
struct
type 'a context = 'a option ref * int64
exception Unwind of int64
let throw ((cell, id) : 'a context) =
fun result -> cell := Some result; raise_notrace (Unwind id)
let generate_id = (* Same *)
let with_early_exit f =
let id = generate_id() in
let cell = ref None in
let context = (cell, id) in
try
f context
with Unwind i when i = id ->
match !cell with
| Some result -> result
| None -> failwith "with_early_exit"
end
let _ =
let nested_function i k = ignore (BetterContinuation.throw k 15); i in
BetterContinuation.with_early_exit (nested_function 42)
|> string_of_int
|> print_endline
Wyrażenie throw k v
może być stosowany w sytuacjach, gdzie wymagane są różne typy.
Używam tego podejścia wszechobecnie w niektórych dużych aplikacjach, nad którymi pracuję. Wolę to od zwykłych wyjątków. Mam bardziej skomplikowany wariant, w którym with_early_exit
posiada podpis mniej więcej tak:
val with_early_exit : ('a context -> 'b) -> ('a -> 'b) -> 'b
gdzie pierwsza funkcja stanowi próbę zrobienia czegoś, a druga reprezentuje obsługi błędów typu 'a
które mogą wyniknąć. Wraz z wariantami i odmianami polimorficznymi daje to wyraźniejsze podejście do obsługi wyjątków. Jest szczególnie silny w przypadku odmian polimorficznych, ponieważ zestaw wariantów błędów może być wywnioskowany przez kompilator.
Podejście Jane Street skutecznie działa tak samo, jak to tutaj opisano, aw rzeczywistości poprzednio miałem implementację, która generowała typy wyjątków z pierwszorzędnymi modułami. Nie jestem pewien, dlaczego ja już ostatecznie wybrał ten - mogą być subtelne różnice :)
To nie jest odpowiedź na to pytanie, ale cokolwiek skończyć do wykonania, ponieważ OCaml 4.02, powinieneś użyć 'raise_notrace' dla wyjątków kontrolnego przepływu, aby upewnić się, że ślad nie jest tworzony, nawet jeśli debugowanie jest włączone: http://caml.inria.fr/pub/docs/manual-ocaml /libref/Pervasives.html. Dla twojej informacji używam kontynuacji, aby rozwiązać większość problemów, które opisujesz. – antron
Dla jasności "idiomatyczny" sposób na uzyskanie efektów wczesnego wyjścia, takich jak etykiety Java, ma na celu stworzenie kontynuacji awarii, które spowodują "powrót" do części programu.Następnie przekazujesz kontynuacje awarii, a inny kod może wywoływać je z wartością, aby natychmiast "wyjść" do dowolnego z tych punktów. Jest znacznie bardziej ogólny niż wyjście z pętli, ponieważ możesz "wyjść" z wszystkiego, co dostaje jedną z tych kontynuacji. Jest również bezpieczny dla typu, ponieważ musisz przekazać kontynuację typowi wartości oczekiwanej przez kontekst w punkcie wyjścia. Nie jestem pewien, czy tego właśnie szukasz. – antron
@antron prawie rozumiem co masz na myśli. czy możesz podać minimalny przykład z kodem jako odpowiedzią? Chciałbym upvote;) – user3240588