Not Lisp again (2009)
funcall.blogspot.com
funcall.blogspot.com
[1]: https://www.youtube.com/watch?v=-J_xL4IGhJA&list=PLE18841CAB...
https://mitpress.mit.edu/sites/default/files/sicp/full-text/...
Texinfo version (recommended) and pretty PDF version:
$ mit-scheme
MIT/GNU Scheme running under GNU/Linux
Type `^C' (control-C) followed by `H' to obtain information about interrupts.
Copyright (C) 2014 Massachusetts Institute of Technology
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Image saved on Friday January 4, 2019 at 11:17:34 PM
Release 9.2 || Microcode 15.3 || Runtime 15.7 || SF 4.41 || LIAR/C 4.118 || Edwin 3.116
1 ]=>vs
https://docs.racket-lang.org/guide/scripts.html
MIT scheme does not love us as users. That's fine. That's entirely their decision of which this is one example. This aspect of the mit-scheme culture is a pretty significant hurdle to it gaining popularity. Probably one that was simply too big. I believe it has been replaced with python for 6.001 nowadays too.
Another example of the same mentality can be seen in the really great SICP video lectures. Jerry Sussman treats the audience like a compiler. He takes the view if he says it once correctly, successful communication of the idea is no longer his problem. That's a lot easier for the audience now we can rewind videos and listen to some small number of sentences again, and again until we get it. It's not exactly a fun way to learn things. It is also a massive contrast to the way say Larry Wall or Guido Van Rossum went about things. I've noticed since Perl ever single new language trumpets how friendly and /helpful/ the community is. I think that's probably pretty significant. This is not a criticism of Jerry, I don't know what his objectives with the language and SICP course were. Hard to get into is hard to get into whatever the reasons.
1 ]=> (edit)
;Loading "/usr/lib/x86_64-linux-gnu/mit-
scheme/lib/prx11.so"... doneI think my most common use case is for math (e.g. adding values that I extracted via some emacs keyboard macros).
There is also the #lang sicp for Racket that needs no modification from the code in the book, to my knowledge.
But if needed, the user can write:
(define (1+ x) (+ x 1))
(define (1- x) (- x 1))
Although, I think, add1 and sub1 are more common nowadays.https://news.ycombinator.com/item?id=18308721 (236 comments) https://news.ycombinator.com/item?id=14247269 (263 comments) https://news.ycombinator.com/item?id=504667 (39 comments)
CL-USER> (time (fact 5))
Evaluation took:
0.000 seconds of real time
0.000002 seconds of total run time (0.000001 user, 0.000001 system)
100.00% CPU
1,550 processor cycles
0 bytes consed
120
compared to second version of factorial CL-USER> (time (fact2 5))
Evaluation took:
0.000 seconds of real time
0.000027 seconds of total run time (0.000027 user, 0.000000 system)
100.00% CPU
64,240 processor cycles
0 bytes consed
120
How it works for other lisp implementation. However when I reach for bigger number things looks different. CL-USER> (time (fact 10000))
Evaluation took:
0.044 seconds of real time
0.045335 seconds of total run time (0.036745 user, 0.008590 system)
[ Run times consist of 0.021 seconds GC time, and 0.025 seconds non-GC time. ]
102.27% CPU
111,336,538 processor cycles
69,739,552 bytes consed
CL-USER> (time (fact2 10000))
Evaluation took:
0.021 seconds of real time
0.021083 seconds of total run time (0.020312 user, 0.000771 system)
[ Run times consist of 0.003 seconds GC time, and 0.019 seconds non-GC time. ]
100.00% CPU
51,983,719 processor cycles
78,751,760 bytes consed
I wonder some kind of compiler magic kicks in, for higher number. USER> (time (loop repeat 100000000 for f = (fact2 5) finally (return f)))
Evaluation took:
3.208 seconds of real time
3.208269 seconds of total run time (3.208269 user, 0.000000 system)
100.00% CPU
10,241,019,690 processor cycles
0 bytes consed
120 (7 bits, #x78, #o170, #b1111000)
USER> (time (loop repeat 100000000 for f = (fact 5) finally (return f)))
Evaluation took:
2.972 seconds of real time
2.971409 seconds of total run time (2.971409 user, 0.000000 system)
99.97% CPU
9,484,980,134 processor cycles
0 bytes consed
120 (7 bits, #x78, #o170, #b1111000)
I think the additional local functions might add a bit of overhead that is noticeable on small inputs.So I added (declare (inline i)) in fact2 (i is iter), recompiled it, and I had a warning:
note: *INLINE-EXPANSION-LIMIT* (50) was exceeded while inlining I
Then, the timing for fact2 was faster: USER> (time (loop repeat 100000000 for f = (fact2 5) finally (return f)))
Evaluation took:
1.900 seconds of real time
1.897450 seconds of total run time (1.897450 user, 0.000000 system)
99.84% CPU
6,056,874,280 processor cycles
0 bytes consed
And if I look at the disassembly for fact2, there is a lot of repetition, and it does look like it was inlined recursively.--- edit
Here is a loop fact3:
(defun fact3 (x)
(declare (type integer x) (optimize (speed 3)))
(loop
for n of-type integer = x then (- n 1)
for a of-type integer = 1 then (* n a)
while (> n 0)
finally (return a)))
USER> (time (loop repeat 100000000 for f = (fact3 5) finally (return f)))
Evaluation took:
1.636 seconds of real time
1.635138 seconds of total run time (1.635138 user, 0.000000 system)
99.94% CPU
5,219,556,544 processor cycles
0 bytes consed
It could be faster but here this supports big integers. Also, please not that this is a micro benchmark, there is a ridiculous amount of iteration needed to have 2-3 seconds of run time. Evaluation took:
2.628 seconds of real time
2.624228 seconds of total run time (2.624228 user, 0.000000 system)
99.85% CPU
8,395,768,934 processor cycles
0 bytes consed
Still less that fact2.But fact2 optimized and with types is:
Evaluation took:
1.416 seconds of real time
1.419171 seconds of total run time (1.419098 user, 0.000073 system)
100.21% CPU
4,530,124,009 processor cycles
0 bytes consed
For reference, here are the dissassembly for both optimized versions. ; disassembly for FACT2
; Size: 97 bytes. Origin: #x5380C48A ; FACT2
; 8A: 498B4D10 MOV RCX, [R13+16] ; thread.binding-stack-pointer
; 8E: 48894DF8 MOV [RBP-8], RCX
; 92: BB02000000 MOV EBX, 2
; 97: 660F1F840000000000 NOP
; A0: L0: 4885C0 TEST RAX, RAX
; A3: 743B JEQ L1
; A5: 48895DE8 MOV [RBP-24], RBX
; A9: 488945E0 MOV [RBP-32], RAX
; AD: BF02000000 MOV EDI, 2
; B2: 488BD0 MOV RDX, RAX
; B5: E846527FFE CALL #x52001700 ; GENERIC--
; BA: 488BF2 MOV RSI, RDX
; BD: 488B45E0 MOV RAX, [RBP-32]
; C1: 488B5DE8 MOV RBX, [RBP-24]
; C5: 488975F0 MOV [RBP-16], RSI
; C9: 488BD0 MOV RDX, RAX
; CC: 488BFB MOV RDI, RBX
; CF: E88C527FFE CALL #x52001760 ; GENERIC-*
; D4: 488B75F0 MOV RSI, [RBP-16]
; D8: 488BC6 MOV RAX, RSI
; DB: 488BDA MOV RBX, RDX
; DE: EBC0 JMP L0
; E0: L1: 488BD3 MOV RDX, RBX
; E3: 488BE5 MOV RSP, RBP
; E6: F8 CLC
; E7: 5D POP RBP
; E8: C3 RET
; E9: CC10 INT3 16 ; Invalid argument count trap
And ; disassembly for FACT3
; Size: 113 bytes. Origin: #x5380DA07 ; FACT3
; 07: 498B4510 MOV RAX, [R13+16] ; thread.binding-stack-pointer
; 0B: 488945F8 MOV [RBP-8], RAX
; 0F: 31F6 XOR ESI, ESI
; 11: 31C0 XOR EAX, EAX
; 13: 488BF2 MOV RSI, RDX
; 16: B802000000 MOV EAX, 2
; 1B: EB31 JMP L1
; 1D: 0F1F00 NOP
; 20: L0: 488945E8 MOV [RBP-24], RAX
; 24: BF02000000 MOV EDI, 2
; 29: 488BD6 MOV RDX, RSI
; 2C: E8CF3C7FFE CALL #x52001700 ; GENERIC--
; 31: 488BF2 MOV RSI, RDX
; 34: 488B45E8 MOV RAX, [RBP-24]
; 38: 488975F0 MOV [RBP-16], RSI
; 3C: 488BD6 MOV RDX, RSI
; 3F: 488BF8 MOV RDI, RAX
; 42: E8193D7FFE CALL #x52001760 ; GENERIC-*
; 47: 488B75F0 MOV RSI, [RBP-16]
; 4B: 488BC2 MOV RAX, RDX
; 4E: L1: 488BDE MOV RBX, RSI
; 51: F6C301 TEST BL, 1
; 54: 7507 JNE L2
; 56: 4885DB TEST RBX, RBX
; 59: 7FC5 JNLE L0
; 5B: EB10 JMP L3
; 5D: L2: 488B5EF1 MOV RBX, [RSI-15]
; 61: 48C1EB08 SHR RBX, 8
; 65: 48837CDEF100 CMP QWORD PTR [RSI+RBX*8-15], 0
; 6B: 7DB3 JNL L0
; 6D: L3: 488BD0 MOV RDX, RAX
; 70: 488BE5 MOV RSP, RBP
; 73: F8 CLC
; 74: 5D POP RBP
; 75: C3 RET
The code after expansion of the loop is a tagbody (labels and goto statements) which calls setq on local variables, whereas the tail-call recursive case is not expanded; the control-flow graph might be easier to optimize given that LABELS is a special form. dx = .0001
def deriv(f):
def f_prime(x):
return (f(x+dx) - f(x)) / dx
return f_prime def f(n):
if n <= 0:
return n
return f(n-1)
f(1000)
will blow out your stack. Meanwhile, scheme@(guile-user)> (define (f n) (if (<= n 0) n (f (- n 1))))
scheme@(guile-user)> (f 1000000)
$1 = 0
Also bear in mind that this was lesson 1 of an intro programming course that culminates in writing a Scheme interpreter in Scheme. There's something to be said about the simplicity / consistency of a language's syntax such that a first-year undergrad can implement it.This kind of approximation exercise is given to calculus students all the time ("Use the definition of derivative to approximate the derivative of f(x) at 3 by taking dx=.001," etc.).
edit: I misread! See reply below.
Granted Knuth's work also did symbolic rather early. A bit harder to work with in his.
Edit: To be fair, I could have worded that first post more clearly! :D
For common lisp, I thought it was just not required, but that it was common to do so?
As you can imagine, there are cases where the developer is counting on the optimization but the code isn't such that it can be made.
https://softwareengineering.stackexchange.com/questions/1576...
Looks like it's a scalac optimization that doesn't always work hence the need for the annotation to give the developer feedback.
There is also a link to a Closure discussion board exploring why Clojure doesn't have the annotation, by design. I didn't read it as I'm not that interested.
```quote:
When speaking about general TCO, we are not just talking about recursive self-calls, but also tail calls to other functions. Full TCO in the latter case is not possible on the JVM at present whilst preserving Java calling conventions (i.e without interpreting or inserting a trampoline etc).
While making self tail-calls into jumps would be easy (after all, that's what recur does), doing so implicitly would create the wrong expectations for those coming from, e.g. Scheme, which has full TCO. So, instead we have an explicit recur construct.
Essentially it boils down to the difference between a mere optimization and a semantic promise. Until I can make it a promise, I'd rather not have partial TCO.
Some people even prefer 'recur' to the redundant restatement of the function name. In addition, recur can enforce tail-call position.
```endquote
https://groups.google.com/g/clojure/c/4bSdsbperNE/m/tXdcmbiv...
Although, a more nuanced point: it may not be required by the Common Lisp spec, but that's not to say that it's not done.
clisp:
[1]> (defun f (n) (if (<= n 0) n (f (- n 1))))
F
[2]> (f 1000000)
*** - Lisp stack overflow. RESET
sbcl: * (defun f (n) (if (<= n 0) n (f (- n 1))))
F
* (f 1000000)
0
(Apparently clisp does do some tail call optimization, but I can't figure what it'd be used for other than an non-terminating loop such as a REPL, if it doesn't even handle this simple case properly.)Heck, even GCC can do tail call optimization for C, even though that's definitely not in the spec.
#include <stdio.h>
int f(int n) {
if (n <= 0) {
return n;
}
return f(n - 1);
}
int main(int argc, char **argv) {
printf("%d\n", f(10000000));
}
Try compiling that with gcc -O0 (wherein you'll get a segfault) vs -O2 (wherein it'll print 0 as hoped).You simply forgot to compile the code!
[1]> (defun f (n) (if (<= n 0) n (f (- n 1))))
F
[2]> (compile 'f)
F ;
NIL ;
NIL
[3]> (f 1000000)
0
The limited form of tail call in CLISP is a feature of its compiler only, not of the interpreter.SBCL compiles everything, so there is no need.
But SBCL requires an existing Common Lisp installation for bootstrapping, whereas CLISP bootstraps itself just from C thanks to its interpreter.
(I am nominally familiar with Common Lisp, but have never used it in a serious capacity.)
The SBCL people claim you can bootstrap SBCL with CLISP.
- You bootstrap SBCL
https://www.reddit.com/r/LispMemes/comments/bduwrl/you_boots...
It's been a while since I've messed with Clojure, but I thought Clojure had a function specifically for tail calls. I think it was recur?
Back in '83 when the author took 6.001 only a minority of students had previously used a computer (note that Hal is quoted as having said, “If you already know how to program..." (emphasis mine). For the majority of students back then this wasn't just "introduction to programming" but "introduction to computers".
Lots of non-EECS students took the class.
I have come to feel that "first class" will eventually be expanded to "can operate against definitions as easily as with them." (That is, can your language let you modify code with the same style code that can be used to execute it?)
Even if it's true, as you say, that it's rare to write a derivative function, it still does come up. And there should be a good way to express it in at least some languages.
[1] https://web.archive.org/web/20100310163003/https://www.cs.in...
This one is particularly relevant. Tail-call optimsiation _not_ implemented and why. Usually the sort of thing that is not mentioned when celebrating all the good fun of lisp.
- I don't get big schemes (like R6RS). For a while, I thought of my own implementation of R5RS. But I've heard R7RS is pretty much R5RS with improvements. So R7RS it is.
- But I'd like to have macros. I wonder if R7RS has macros.
- Mainly interested in crazy metaprogramming and logic/relational experiments using Friedman/Byrd school of thought (reasoned schemer, minikanren, constraint kanren, that sort of stuff).
- Finally instead of rolling my own from scratch, I've heard I could use something like chibi as a starting point (additional points since chibi is implemented in C).
Comments?
I guess I'll have to think about dynamic-scoping a little more.
> - I don't get lisp's (lisp-2) dynamic scoping (a big reason I haven't switched from vim to emacs). I'm much more comfortable with the lexical scoping of scheme (lisp-1).
Lisp-1/2 has to do with functions and other identifiers living in separate namespaces. So in a Lisp 2 requires special functions that live in the function namespace to call functions that exist as a local variable, (i.e. funcall or apply in Common Lisp)If you're having a hard time coming to terms with dynamic scoping (and I think most Lispers don't like it) and you're interested in metaprogramming, try writing a macro in a dynamically-scoped lisp that lets you use lexical scoping. This is kind of a fun exercise and you can do it in Elisp.
I used to be interested in Lisp - in particular Clojure - for metaprogramming and mini-kanren mostly - so you're mindset reminds me of my own. But I realized I could get much the same thing as metaprogramming with data-oriented programming, or making an interpreted language to do what I needed. The performance of raw native code is comparable in some cases. Where that fails, you could theoretically use Perl or Python to generate C code that just has to be performant but is very repetitive. The benefit of that is that mere mortals will understand what you're doing better, and you're not forced to bear the burden of a garbage collected runtime.
Common Lisp, which is the main representative of Lisp-2 family, is firmly in lexical scoping, which is what enabled the example in the linked article. It also has wild unrestricted macros, and much more stable language (less redoing work when moving from implementation to implementation, especially if you use wrapper libs around stuff outside standard library)
I don't know about elisp, but common lisp uses primarily lexical scoping. If you ignore ‘defparameter’/‘defvar’, then you'll be living exclusively in lexical scoping land.
> I'd like to have macros. I wonder if R7RS has macros
R7RS only specifies hygienic macros, but unhygienic macros are a commonly implemented extension. They aren't present in chibi scheme, but you find chicken scheme[0] or s7 scheme[1] interesting.
Edit: er-macro-transformer IS unhygienic unless you tell it otherwise, much like gensym, except all renamed bindings introduced in the same macro invocation are the same. Gensym is of course available as well
$ cd=/mnt/cdrom
your "cd" commmand doesn't stop working, right?What's there not to understand, really.
foo:; struct foo *foo; {
int x;
x:
goto x;
}
Macros are all in one namespace, but a function-like macro won't replace an identifier not followed by an opening paren: #define mac(x)
mac("abc") /* disappears */
int mac; /* unaffected */(not replying directly to you, but for other people)
And R6RS is too big for me.
"Dynamic vs lexical scope" doesn't have anything to do with "Lisp-1 vs Lisp-2". "Lisp-1 vs Lisp-2" is about whether function values are distinguishable from "defun"-defined functions. "Dynamic vs lexical scope" is about whether variable name-resolution is done based on the lexical scope stack, or the call stack.
You can enable lexical scoping in Elisp files by starting them with:
;; -*- mode: Emacs-Lisp; lexical-binding: t; -*-
That's how my ~/.emacs.d/init.el starts. #include <stdio.h>
#define DX 0.0001
#define DERIV(_f) float deriv_##_f(float x) { return (_f(x + DX) - _f(x)) / DX; }
float cube(float x) { return x * x * x; }
DERIV(cube)
int main(void) {
printf("%f\n", deriv_cube(2));
printf("%f\n", deriv_cube(3));
printf("%f\n", deriv_cube(4));
}
Any language supporting closures or generics could get rid of the unsexy DERIV macro.What sets Lisp apart is its "code as data" approach which lets the coder write incredibly powerful macro (and also incredibly hard to understand code). The rest can easily be emulated in other languages.
I do like this naive approach mind you, there's a certain Zen to be found reimplementing these mathematical concepts from first principles, I just really fail to see what makes Lisp so remarkable in this context.
I'm not sure how you would do that in C, other than by Greenspunning up an implementation of first-class functions.
double fprime(double (*f)(double), double x);
but you wouldn't be able to define double (*deriv(double (*f)(double)))(double);
as C does not support closures.Meanwhile, in C++ you might use
std::function<double(double)> deriv(std::function<double(double)> f);If you're going to claim it is different, then I claim that LISP can't do addition. When you have (+ 1 1) that is something completely different, because you have the syntax fucked up. It's not addition like I know it, so it doesn't count.
And also, it has nothing to do with the syntax (+ 1 1), the keyword is homoiconicity.
No decent-performing system totally skips compilation. You just don't notice that it is happening. Even a web browser compiles the javascript before running it; that is what a JIT does.
I mentioned the syntax "(+ 1 1)" because you're trying to disqualify C++ homoiconicity on a superficial syntactic basis. If you can do that, then it is equally valid to say that LISP can't do simple addition.
C++ can manipulate itself. Granted, you'll want a library such as LLVM, but you can get the job done yourself if you are a glutton for punishment.
Code is represented via data structures on normal (von Neumann) hardware. LISP is every bit as compiled as C++. Both forms of C++, human-readable and compiled, can be manipulated. It's the same as LISP, except that "human readable" is debatable for any form of LISP.
BTW, in LISP, actual usage of the homoiconicity is the surest way to make a mess of unmaintainable code.
I really wouldn’t have got this stuff 5 years ago but functions returning functions is a super sweet tool and I’m glad it’s finally seeing mainstream adoption. It’s a small leap from the imperative mindset but results in much neater flows when done well.
C's preprocessor is very powerful and like the rest of the language it can be used to shoot yourself in the foot - but that doesn't mean you shouldn't take full advantage of it or that actually taking advantage of it instead of treating it as a poor man's module system is "abusing" it.
(define deriv (lambda (f)
(let ((dx .0001))
(lambda (x)
(/ (- (f (+ x dx)) (f x)) dx))))) (define (foo bar) ...)
is equivalent to (define foo (lambda (bar) ...))I really don't get this sort of "Lisp is cool" article touting advantages the Author was impressed by during their childhood that have long since been overtaken by technical advancements in other languages.
Pattern matching would probably help a ton for destructuring - being able to take "$a + $b" and decompose "2 + 3" into "a=2, b=3", but that's kind of difficult. There's ways you could do it cleanly, like imposing a very predictable structure on your AST nodes, but they end up sort of reinventing Lisp through the backdoor. Luckily you don't need that for most macros, afaict.
Weirdly, lots of D people end up doing that. I blame the fact that it's a language that's both obviously better than the languages it's competing with, and also obviously worse than it could be.
Yes, getting recursion right is old hat. But the fact that it was solved a long time ago doesn't mean it's solved for me today. If I program like that in my day job in 2020, I do in fact "consume a small amount of memory and eventually run out"