Hacker News new | ask | show | jobs
by Jtsummers 1432 days ago
You shouldn't need to raise exceptions for early returns. You can usually restructure your program to provide for the proper return without needing to use a return or comparable form when using an expression oriented language. If you had this C snippet, for instance:

  int foo(...) {
    if (condition) return an_expression;
    ...
    return another_expression;
  }
You'd put everything after that if into the else part instead of dropping it (like you would in C):

  (defun foo (...)
    (if condition
      an-expression
      (progn
        ...
        another-expression))) ;; and this /progn/ form would possibly be extracted to a new function if it was too long
Or more likely a cond if using Common Lisp and there were more than two cases:

  (defun foo (...)
    (cond (condition an-expression)
          ...
          (t
             ...
             another-expression)))
And in Common Lisp (and some others) you can still get a return form, using return-from in particular, from functions if you want to keep something more like the C-ish form:

  (defun foo (...)
    (when condition (return-from foo an-expression))
    ...
    another-expression))
1 comments

Thanks, learned something new today with return-from! I’d be curious how different cl implementations implement it.

The first few examples are what I was more familiar with, and it is precisely your note “and this /progn/ form would possibly be extracted to a new function if it was too long” which makes me want to just return early in some cases. Sometimes a case is common enough that it warrants a separate function, but having to define functions for incredibly specific cases just to avoid excessive indentation is not ideal for me.

> Thanks, learned something new today with return-from! I’d be curious how different cl implementations implement it.

At least in SBCL, with it compiling the function, it turns into exactly the kind of assembly code you'd expect. Using disassemble:

    CL-USER> (defun foo (n)
               (when (< n 0) (return-from foo 20))
               (* 20 n))

    CL-USER> (disassemble #'foo)
    ; disassembly for FOO
    ; Size: 54 bytes. Origin: #x53642F41                          ; FOO
    ; 41:       498B4510         MOV RAX, [R13+16]                ; thread.binding-stack-pointer
    ; 45:       488945F8         MOV [RBP-8], RAX
    ; 49:       488B55F0         MOV RDX, [RBP-16]
    ; 4D:       31FF             XOR EDI, EDI
    ; 4F:       FF14252001A052   CALL QWORD PTR [#x52A00120]      ; SB-VM::GENERIC-<
    ; 56:       7C16             JL L1
    ; 58:       488B55F0         MOV RDX, [RBP-16]
    ; 5C:       BF28000000       MOV EDI, 40
    ; 61:       FF14251001A052   CALL QWORD PTR [#x52A00110]      ; SB-VM::GENERIC-*
    ; 68: L0:   488BE5           MOV RSP, RBP
    ; 6B:       F8               CLC
    ; 6C:       5D               POP RBP
    ; 6D:       C3               RET
    ; 6E: L1:   BA28000000       MOV EDX, 40
    ; 73:       EBF3             JMP L0
    ; 75:       CC10             INT3 16                          ; Invalid argument count trap
This sets up the call for the comparison, conditionally jumps based on the result (JL), and then returns one of the two results. Note that the "40" in 5C and 6E are really "20", this is a consequence of how many CL implementations handle integers, the low-order bit is used as a tag so not really part of the value. If you wrote it in C it would be very similar.

https://godbolt.org/z/cYPhdW3qd

The biggest difference is that the C version doesn't need a call for the comparison.