Returning from a function inside when statement
Asked Answered
A

4

7

All I'm trying to do is use a when statement to return a value :( I want the functionality of:

if(x)
    return y

And I'm trying to use:

(when (x) y)

But the when statement is not evaluating in a way that exits the function and return y. It just happily carries on to the next line. Is there a way to do this without making an extremely ugly looking if-else block? mzscheme/racket does not allow 1-armed ifs.

Afterbirth answered 29/4, 2015 at 3:54 Comment(2)
What is the code supposed to do when x evaluates to #f?Photocathode
I think this is an XY-problem. If you want to exit the function when x is true and continue otherwise you have the very definition of what an if is.Gev
H
5

You tagged this as both Common Lisp and Racket, which are two completely different languages. If you're using Racket or Scheme and want to return from a function early, you can do it using a continuation:

(define (my-function x y)
  (call-with-current-continuation
    (lambda (return)
      (when x (return y))
      ;; Rest of code not evaluated if X is true
      )))

In some Scheme implementations including Racket, call-with-current-continuation is also bound to call/cc, but call-with-current-continuation is the only portable way to use continuations.

The above is even uglier than using a cond statement. If you want to get rid of all that extra crap, you can define a macro that creates an alternate version of define that automatically creates the continuation and binds it to return:

(define-syntax define/return
   (syntax-rules ()
      ((_ (name . args) . body)
       (define (name . args)
         (capture-vars (return)
            (call/cc (lambda (return) . body)))))))

This requires you to have my capture-vars macro, which you can find in this answer.

EDIT: Leppie provided the following implementation of define/return which is much simpler since it doesn't require my capture-vars macro:

(define-syntax define/return
  (lambda (x)
    (syntax-case x ()
      [(_ (name . args) . body)
        (with-syntax
          ([return (datum->syntax #'name 'return)])
         #'(define (name . args)
             (call/cc (lambda (return) . body))))])))

EDIT 2: However, it's easy to accidentally un-capture the definition of return doing it this way, if you incorporate a define/return in another macro.

Then return will behave as you'd expect and not be syntactically repugnant:

(define/return (my-function x y)
    (when x (return y))
    ;;; more code...
)

However, if you're using Common Lisp, the situation is different. In Common Lisp, (return y) will only compile when a block named nil is defined. Certain forms implicitly define a block named nil, such as the loop macro. Without a block named nil, you can still use return-from to return from a named block. If you're in a function defined with defun, the name of that function is also the name of a block that wraps that function, so this would work:

(defun my-function (x y)
   (when x (return-from my-function y))
   ;;; more code
   )
Hemp answered 29/4, 2015 at 4:19 Comment(8)
Why do you go through all those hoops and bounds to capture the return identifier? Something like gist.github.com/leppie/887a278e4db0db99b031 works fine in R6RS. Am I missing something?Cindacindee
@Cindacindee Nope. You didn't miss a thing. I actually didn't know you could do that.Hemp
In Racket, the appropriate way to give special keywords meaning inside of a macro is with syntax parameters. (define-syntax-parameter return #f) (define-syntax-rule (define/return header body ...+) (define header (let/ec return-continuation (syntax-parameterize ([return (make-rename-transformer #'return-continuation)]) body ...)))) This avoids breaking hygiene or the need to use syntax-case / syntax-parse instead of the much simpler define-syntax-ruleConsignee
@Consignee syntax parameters can be shadowed by local variables, making them no better than globals. I went into detail about it here.Hemp
@ThrowawayAccount3Million Why on earth is the ability to shadow syntax parameters a bad thing? It means they respect lexical scope. If I did (splicing-let ([return something-else]) (define/return ...)) I would expect return to refer to the closest definition of return in scope. Syntax parameters aren't used to tie a macro to a certain global identifier, they're used to make it explicit that a macro depends on some contextual binding. And lexically scoped bindings are the easiest bindings to reason about.Consignee
@ThrowawayAccount3Million You can't get your return keyword shadowed by another library. Either you've defined return in the module you're using it in, in which case lexical scope dictates it shadows any provided return binding from anything you've required, or you're requiring your define/return macro from a library that provides return in which case it can't get shadowed by a return binding provided from any other module because a module requiring two libraries that provide the same bindings is a syntax error. As long as you're respecting lexical scope and using hygienic macros..Consignee
...the case you're worried about is literally not possible. That's the whole advantage of hygiene and lexical scoping - easier to reason about, better static analysis, and fewer "gotchas".Consignee
@Consignee I had written a Racket version of CL's LOOP macro where the = operator didn't work because my macro was written in Swindle, but most of my programs were written in Racket, which defines a completely different = and therefore the = in my Racket code didn't pattern-match with the = in the macro. I had no idea what was going on. The other keywords worked fine. Therefore, I see syntax shadowing as a bug and not a feature.Hemp
P
10

Okay,I'm going to be "that guy"; there's a good reason for using the "extremely ugly" solution of putting the rest of the function inside of the "else" of your conditional; it makes the code easier to read and understand. When I'm trying to understand what a function does, I don't want to have to scan through all the code hunting for hidden returns and strange control flow. A straightforward 'if' or 'cond' makes it very clear under what circumstances each piece of code will be used.

If you think hard about why you like the "when+return" solution, I suspect that at some level, you want to take that guard and "get it out of the way" of your cognitive processes, allowing the rest of the function to be the focal point. This (I claim) is a recipe for subtle bugs.

Search your feelings; you know it to be true!

EDIT: an ant just crawled across my laptop. This is a sign that I'm speaking the truth.

Podium answered 29/4, 2015 at 16:49 Comment(0)
A
9

A simple way in Racket:

(define (foo x)
  (let/ec return
    (when (= x 0)
       (return 'zero))
    'not-zero))

Here ec stands for escape continuations, which are cheaper than full continuations.

Arnst answered 29/4, 2015 at 11:50 Comment(0)
H
5

You tagged this as both Common Lisp and Racket, which are two completely different languages. If you're using Racket or Scheme and want to return from a function early, you can do it using a continuation:

(define (my-function x y)
  (call-with-current-continuation
    (lambda (return)
      (when x (return y))
      ;; Rest of code not evaluated if X is true
      )))

In some Scheme implementations including Racket, call-with-current-continuation is also bound to call/cc, but call-with-current-continuation is the only portable way to use continuations.

The above is even uglier than using a cond statement. If you want to get rid of all that extra crap, you can define a macro that creates an alternate version of define that automatically creates the continuation and binds it to return:

(define-syntax define/return
   (syntax-rules ()
      ((_ (name . args) . body)
       (define (name . args)
         (capture-vars (return)
            (call/cc (lambda (return) . body)))))))

This requires you to have my capture-vars macro, which you can find in this answer.

EDIT: Leppie provided the following implementation of define/return which is much simpler since it doesn't require my capture-vars macro:

(define-syntax define/return
  (lambda (x)
    (syntax-case x ()
      [(_ (name . args) . body)
        (with-syntax
          ([return (datum->syntax #'name 'return)])
         #'(define (name . args)
             (call/cc (lambda (return) . body))))])))

EDIT 2: However, it's easy to accidentally un-capture the definition of return doing it this way, if you incorporate a define/return in another macro.

Then return will behave as you'd expect and not be syntactically repugnant:

(define/return (my-function x y)
    (when x (return y))
    ;;; more code...
)

However, if you're using Common Lisp, the situation is different. In Common Lisp, (return y) will only compile when a block named nil is defined. Certain forms implicitly define a block named nil, such as the loop macro. Without a block named nil, you can still use return-from to return from a named block. If you're in a function defined with defun, the name of that function is also the name of a block that wraps that function, so this would work:

(defun my-function (x y)
   (when x (return-from my-function y))
   ;;; more code
   )
Hemp answered 29/4, 2015 at 4:19 Comment(8)
Why do you go through all those hoops and bounds to capture the return identifier? Something like gist.github.com/leppie/887a278e4db0db99b031 works fine in R6RS. Am I missing something?Cindacindee
@Cindacindee Nope. You didn't miss a thing. I actually didn't know you could do that.Hemp
In Racket, the appropriate way to give special keywords meaning inside of a macro is with syntax parameters. (define-syntax-parameter return #f) (define-syntax-rule (define/return header body ...+) (define header (let/ec return-continuation (syntax-parameterize ([return (make-rename-transformer #'return-continuation)]) body ...)))) This avoids breaking hygiene or the need to use syntax-case / syntax-parse instead of the much simpler define-syntax-ruleConsignee
@Consignee syntax parameters can be shadowed by local variables, making them no better than globals. I went into detail about it here.Hemp
@ThrowawayAccount3Million Why on earth is the ability to shadow syntax parameters a bad thing? It means they respect lexical scope. If I did (splicing-let ([return something-else]) (define/return ...)) I would expect return to refer to the closest definition of return in scope. Syntax parameters aren't used to tie a macro to a certain global identifier, they're used to make it explicit that a macro depends on some contextual binding. And lexically scoped bindings are the easiest bindings to reason about.Consignee
@ThrowawayAccount3Million You can't get your return keyword shadowed by another library. Either you've defined return in the module you're using it in, in which case lexical scope dictates it shadows any provided return binding from anything you've required, or you're requiring your define/return macro from a library that provides return in which case it can't get shadowed by a return binding provided from any other module because a module requiring two libraries that provide the same bindings is a syntax error. As long as you're respecting lexical scope and using hygienic macros..Consignee
...the case you're worried about is literally not possible. That's the whole advantage of hygiene and lexical scoping - easier to reason about, better static analysis, and fewer "gotchas".Consignee
@Consignee I had written a Racket version of CL's LOOP macro where the = operator didn't work because my macro was written in Swindle, but most of my programs were written in Racket, which defines a completely different = and therefore the = in my Racket code didn't pattern-match with the = in the macro. I had no idea what was going on. The other keywords worked fine. Therefore, I see syntax shadowing as a bug and not a feature.Hemp
S
0

Simple:

(and (x) y)

The problem is that "when" doesn't return a value. "and" does.

Starnes answered 15/8, 2019 at 5:34 Comment(1)
no, this is incorrect, unfortunately. Both do the same thing, in the context of this question - both "happily carry on to the next [code] line" and do not immediately return from the function.Fete

© 2022 - 2024 — McMap. All rights reserved.