Re: Escape continuations for fussy code

73 views
Skip to first unread message

Hendrik Boom

unread,
Oct 1, 2021, 11:58:29 AM10/1/21
to Jesse Alama, Racket Users
On Fri, Oct 01, 2021 at 02:22:14PM +0000, Jesse Alama wrote:
> Hello,
>
> Have you ever wished you could do a C-style return in the middle
> of a block of Racket code? When you're in the heat of things with
> a complicated problem where input values need a fair amount of
> multi-stage extraction and validation, using cond quickly pulls
> code to the right. My procedure is:
>
> * cond/case. In each branch:
> * Define some new values safe in the knowledge of where you are
> (extract) and perhaps check them, if necessasry (validate)
> * Make sure you have an else branch.
> * return to 1 and repeat as many times as necessary.
>
> The result:
>
> (cond [(foo? x)
> (define y (bar x))
> (define z (jazz x y))
> (cond [(loopy? z)
> (define a (yowza z))
> (cond [(string? a)
> (define b (bonkers a))
> (cond [(number? (hoop x b))
> (define ...)]
> [else
> (error 'um)])]
> [else
> (error 'ugh)])]
> [else #f])]
> [else #f])

the Racket package parendown (see
https://docs.racket-lang.org/parendown/index.html ) can reduce this a
lot:

(if (not #/ foo? x) #f
#/ begin
(define y (bar z))
(define z (jazz x y))
#/if (not #/ loopy? z) #f
#/ begin
(define a (yowza z))
#/if (not (string a)) (error 'ugh)
#/begin
(define b (bonkers a))
#/if (not (number? (hoop x b))) (error 'um)
(define ...)
)

Or, using let instead of define

(if (not #/ foo? x) #f
#/ let ((y (bar z)))
#/ let ((z (jazz x y)))
#/ if (not #/ loopy? z) #f
#/ let ((a (yowza z)))
#/ if (not (string a)) (error 'ugh)
#/ let ((b (bonkers a)))
#/ if (not (number? (hoop x b))) (error 'um)
#/ let ((...))
-- and presumably you wanted to do someting inside all there define's
)

Is this clearar?

There remain
* the ugly extra parentheses aroung each let-pair,
which is an unnecessary tradition once you use parendown,
and
* the 'not's after the 'if's.
which could be resolved with a three-argument check operator:
(check requirement errormessage stufftodoifok) -- essentially
an argument-reversed if.

but this is enough to eliminate the push off the right side of the page.

A heuristic when programming this way:
When a function takes several arguments, define it so that the
argument that is likely to be textually longer is at the end.

-- hendrik

>
> That's an awful lot of whitespace. We're getting dragged to the
> right. Pretty soon we're dealing with lines that have three dozen
> spaces at the front or more.
>
> At times, it can even get a bit silly just how deeply nested code
> can get. It can even degrade program comprehension for you &
> others. It can even impede your own coding by requiring you to
> scroll up to mentally re-construct the state you're in.
>
> This isn't necessarily a problem with Racket. I think it reflects
> of the inherent fussiness of some problems. And certainly, you
> can reformat the code differently to make the problem somewhat
> less severe.
>
> Nonetheless, I certainly have found myself envying those working
> in other languages where they can just bail out of a complicated
> computation by just returning a value. I mean, sure, I could
> raise an exception and bail out that way, right? It turns out
> there's a way: escape continuations. They permit a kind of
> C-style return-y programming in Racket. I've got an article (
> https://click.convertkit-mail.com/92u52qdvngtnh535n9s9/3ohphkhq39xwevtr/aHR0cHM6Ly9saXNwLnNoL2VzY2FwZS1jb250aW51YXRpb25zLWZvci1mdXNzeS1jb2RlLw==
> ) up introducing escape continuations and give a simple
> real-world example from web programming.
>
> Happy hacking,
>
> Jesse
>
> Unsubscribe (
> https://unsubscribe.convertkit-mail.com/92u52qdvngtnh535n9s9 ) |
> Update your profile (
> https://preferences.convertkit-mail.com/92u52qdvngtnh535n9s9 ) |
> Moltkestrasse 3d, Mainz, RP 55118

David Storrs

unread,
Oct 1, 2021, 2:33:06 PM10/1/21
to Jesse Alama, Racket Users
I'm presuming that this code should either return #f, return a calculated value, or raise an exception.  If so, here's a version that runs in plain racket that I find pretty easy to read.  It has the advantage that the 'return #f' parts aren't way far away from what causes them.

(with-handlers ([false? identity] ; return #f
                [any/c  raise])   ; re-raise everything else
  (let* ([x (if (foo? x)
                x
                (raise #f))]
         [z (jazz x (bar x))]
         [a (if (loopy? z)
                (yowza z)
                (raise #f))]
         [b (if (string? a)
                (bonkers a)
                (error 'ugh))])
    (if (number? (hoop x b))
        'all-good
        (error 'um))))

If instead you want to return the exn that comes from error instead of re-raising it then you can do that by removing the false? clause from the with-handlers.  NOTE:  You should re-raise exn:break since otherwise the user cannot ^C the program.

(with-handlers ([exn:break?  raise]
                [any/c       identity])
  ...put the let* code here...)

Hendrik Boom

unread,
Oct 1, 2021, 3:53:25 PM10/1/21
to racket...@googlegroups.com
Yes. But different semantics if bar, yowza, and bonkers have side effects.
If they don't, they're quite equivalent.

-- hendrik

>
> If instead you want to return the exn that comes from error instead of
> re-raising it then you can do that by removing the false? clause from the
> with-handlers. NOTE: You should re-raise exn:break since otherwise the
> user cannot ^C the program.
>
> (with-handlers ([exn:break? raise]
> [any/c identity])
> ...put the let* code here...)
>
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAE8gKodxas7jtze%2BttcFA%2BG0ATKUFZD3rhK%2B%3Dn2U1md1zQPJSg%40mail.gmail.com.

jackh...@gmail.com

unread,
Oct 2, 2021, 4:09:03 PM10/2/21
to Racket Users
Here's my solution:

(define/guard (f x)
  (guard (foo? x) else
    #false)

  (define y (bar x))
  (define z (jazz x y))
  (guard (loopy? z) else
    #false)
  (define a (yowza z))
  (guard (string? a) else
    (error 'ugh))
  (define b (bonkers a))
  (guard (number? (hoop x b)) else
    (error 'um))
  (define ...))

It uses a `guard` macro I wrote and talked about in this thread.

Laurent

unread,
Oct 3, 2021, 10:45:28 AM10/3/21
to jackh...@gmail.com, Racket Users
Oh well, since everyone is at it, here's my version that no-one asked for. It's similar to parendown, but uses a more standard (but also specific) macro `cond/else` from https://github.com/Metaxal/bazaar/blob/master/cond-else.rkt :

(cond/else
 [(not (foo? x)) #f]
 #:else

 (define y (bar x))
 (define z (jazz x y))
 #:cond
 [(not (loopy? z)) #f]
 #:else
 (define a (yowza z))
 #:cond
 [(not (string? a))
  (error 'ugh)]
 #:else
 (define b (bonkers a))
 #:cond
 [(number? (hoop x b))
  (define ...)]
 #:else
 (error 'um))


I find the different coloration of the keywords helpful to parse the code too.

Now waiting for more original solutions to this problem :-)


Ryan Kramer

unread,
Oct 20, 2021, 12:44:42 PM10/20/21
to Racket Users
I guess I'll pile on too. My approach was `let++` which I rename to `let*` because (I think) it is backwards compatible. The pattern for early exit is `#:break (when test-expr result-expr)` so the previous example would look like this:

(let* (#:break (when (not (foo? x))
                 #f)
       [y (bar x)]
       [z (jazz x y)]
       #:break (when (not (loopy? z))
                 #f)
       [a (yowza z)]
       #:break (when (not (string? a))
                 'ugh)
       [b (bonkers a)]
       #:break (when (not (number? (hoop x b)))
                 'um))
  (list x y z a b))

In practice, this allowed me to rewrite a lot of functions that were highly nested into a single let++ form.

The other feature of let++ is that it also supports let-values. (Having to nest "let, then let-values, then let again" was another reason my code would get too indented for my taste.)

George Neuner

unread,
Oct 20, 2021, 8:54:37 PM10/20/21
to racket...@googlegroups.com
On Wed, 20 Oct 2021 09:44:42 -0700 (PDT), Ryan Kramer
<default...@gmail.com> wrote:

> :
>The other feature of let++ is that it also supports let-values. (Having to
>nest "let, then let-values, then let again" was another reason my code
>would get too indented for my taste.)
> :

Possibly a stupid question, but ...

What causes you to /have to/ 'nest "let, then let-values, then let
again"'? Assuming no intervening body code, a single let*-values
could cover all of it (with the same semantics).

Ryan Kramer

unread,
Oct 21, 2021, 10:39:18 AM10/21/21
to Racket Users
Good question. I know I have done exactly that in the past, but I guess I just forgot about that pattern in my more recent code. Other possible reasons include "because I don't like the unnecessary parens around a single id" and "because I like the indentation of let* (4 chars) much better than let*-values (11 chars) and want to delay introducing let*-values until it becomes necessary."
Reply all
Reply to author
Forward
0 new messages