3 ms·
The things that really kept me (for the way I think about and write code) in CL (the sbcl variant) over clojure are, in no in no particular order: - User reade
by triclops200 2y ago
The things that really kept me (for the way I think about and write code) in CL (the sbcl variant) over clojure are, in no in no particular order:
- User reader macros are basically gone in clojure
- The JVM is too heavy and too slow to start up for smaller programs
- Tail recursion
- Mutual tail recursion without trampoline
- The JVM makes it impossible to have error restarts which are the coolest
- CL is multiparadigm instead of only functional
- tombert 2y agoThat's all fair, but I dispute a bit here: - Mutual tail recursion can be achieved with the trampoline function, which comes standard in Clojure. - It's not too hard to do OOP-like in Clojure, and there's even clever macro stuff to get logic programming and the like. I'll agree it's very much a functional-first language though. - While there's no explicit tail recursion, the Clojure loops are basically just tail recursive. You have to be a lot more explicit about it, and I will acknowledge that's not as pretty. The JVM startup time is a real issue, but GraalVM really does help with that; I got around 3ms of startup time last time I tried it. I might need to play with SCBL variant still. CLOS really fascinates me.
- lispm 2y ago"self tail recursion" is only a simple case of the idea of tail calls (-> tail call optimization, TCO). In a Lisp variant/implementation with TCO (generally Scheme implementations and also a bunch of CL compilers, like SBCL), ANY tail call is automatically optimized to a jump, without growing the stacks, without the need to use special construct like Clojure's loops or trampolines. for example SBCL on ARM64 CL-USER> (defun example (a b) (if (> a b) (sin a) (cos b))) EXAMPLE In above CL function, the calls to SIN and COS are tail calls. Let's look at the machine code, which is automatically and by default created by above definition: CL-USER> (disassemble #'example) ; disassembly for EXAMPLE ; Size: 100 bytes. Origin: #x7006948100 ; EXAMPLE ; 00: AA0A40F9 LDR R0, [THREAD, #16] ; binding-stack-pointer ; 04: 4A0B00F9 STR R0, [CFP, #16] ; 08: 7A0300F9 STR CFP, [CSP] ; 0C: EA030EAA MOV R0, R4 ; 10: EB030DAA MOV R1, R3 ; 14: FA031BAA MOV CFP, CSP ; 18: 29BE80D2 MOVZ TMP, #1521 ; 1C: BE6B69F8 LDR LR, [NULL, TMP] ; SB-KERNEL:TWO-ARG-> ; 20: C0033FD6 BLR LR ; 24: 4EB741A9 LDP R4, R3, [CFP, #24] ; 28: 5F011DEB CMP R0, NULL ; 2C: E1000054 BNE L0 ; 30: EA030DAA MOV R0, R3 ; 34: F6FBFF58 LDR LEXENV, #x70069480B0 ; #<SB-KERNEL:FDEFN COS> ; 38: 570080D2 MOVZ NARGS, #2 ; 3C: DE9240F8 LDR LR, [LEXENV, #9] ; 40: DE130091 ADD LR, LR, #4 ; 44: C0031FD6 BR LR ; 48: L0: EA030EAA MOV R0, R4 ; 4C: 76FBFF58 LDR LEXENV, #x70069480B8 ; #<SB-KERNEL:FDEFN SIN> ; 50: 570080D2 MOVZ NARGS, #2 ; 54: DE9240F8 LDR LR, [LEXENV, #9] ; 58: DE130091 ADD LR, LR, #4 ; 5C: C0031FD6 BR LR ; 60: E00120D4 BRK #15 ; Invalid argument count trap One can see in the assembler, that the > function is called as a subroutine with BLR (-> it is not a tail call), while SIN and COS are called with a BR, basically a jump, since those are tail calls. Thus in any piece of code, the compiler recognizes the tail calls and will create the corresponding machine code jmp instruction (minus any restriction a compiler will further have). Generally Common Lisp doesn't require TCO, though many compilers provide it as an optimization. Required is a basic low-level lexical goto construct, which is used to implement various loop. > but GraalVM really does help with that In something like SBCL there is no special tool/implementation (restricting dynamism) needed to create an executable. It is a function call and one second execution time away. The resulting executable starts fast and is basically a full Lisp with all features, again.
- Capricorn2481 2y agoI would like all of those things, but I'm always turned off of CL for a few reasons - Emacs is hard to learn, and just doesn't have a lot of the things that IDEs have these days. Yes, I know it's a tired argument but it's a real one. The VSCode solution works fairly well but isn't as interactive, which is kind of the whole point of using CL. - There's a lot of weird quirks and badly named things in CommonLisp. You can definitely feel how old it is, but it's great that old libraries will still work. - The ecosystem is hard to navigate. I have bought a few CL books and some of them really suck because they reference libraries that flat out don't exist anymore (these are books in the last 10 years, which most CL books are). You will, of course, never find an API wrapper for anything new. Those things may not be hard to work on, but it's extra work. It's hard to move from Clojure, which has a strong (and well documented) ecosystem, a dead simple library, and good IDE support. It may not have the debugging support CL has, but between the REPL and immutable nature, it's not a huge loss. It's more debuggable than most languages. I would also say some Clojure libraries really stand out to me from EVERY ecosystem. Electric Clojure is really cool. Tempel is a very simple encryption library but you can do so much with it. Hiccup feels so natural. It doesn't get talked about a lot, but I feel bummed using other languages because they don't have such creative libraries.