But re: the Python code—I would say that, from the perspective of the 1960s, all modern "dynamic" languages that have REPLs (like Python) are Lisps in essential character.
"Lisp", back then, referred less to "a language that uses a lot of parentheses", and more to things like:
• runtime sum-typing using implicit tagged unions;
• parameterization of functions using linked lists (or hash-maps) of paired interned-string "keys" and arbitrary product-typed values, rather than parameterization using bitflags or product-types of optional positional parameters;
• heap allocation and garbage-collection;
• a compiler accessible by the runtime;
• "symbolic linkage" of functions and global variables, such that a named function or variable "slot" can be redefined (even to a new type!) at runtime, and its call-sites will then use the new version.
We only notice the parens as the differentiating feature of Lisps nowadays, because everything else has become widely disseminated. Perl and Python and PHP and Ruby (and even Bash) are fundamentally Lisps, in all of the above ways. Lisp "won."
My point, though, was that while the proponents of Lisp define Lisp one way (by the things only Lisp can do due to e.g. homoiconicity), the opponents of Lisp (like the author was, coming into the course) define Lisp by the set of features that make Lisp "not a Real Programmer†'s programming language"—i.e. the set of things that make them not want to use it.
† http://www.catb.org/jargon/html/R/Real-Programmer.html
My assertion was, from these opponents' perspectives, there are very few languages left for "Real Programmers"; most modern languages have inherited nearly all of those horribly convenient Lisp-isms. Heck—modern CPUs are so good at pointer-chasing, they may as well be Lisp Machines!
On another side of the Smalltalk family tree, languages like Java embraced a kind of static typing and found strong success with it, leading to the promotion of strongly-typed structures over lambdas. This side, ironically, did the most work on JITs, which are meant to compensate for dynamic language features, and languages like Dart are still marrying JIT techniques to strong static type systems today.
And on another side of the Smalltalk family tree, languages like E are fully oriented around objects with rights, so that instead of structures which can be examined, values must have messages sent to them instead. Homoiconicity, lambdas, pattern-matching lists, and other staples of Scheme are nonetheless common in E and Monte.
I think that the Smalltalk family, overall, could be viewed as a response to the barren syntactic landscape of Lisp, but I think that they could also be viewed like any other language: Taking what they like from ancestors, and not taking what they don't like.
That was Lisp back then. Lisp then early on was used in a bunch of symbolic computing domains: theorem provers, computer algebra, natural language processing, planning, rule-based languages, programming language implementation (languages like ML or Scheme were implemented first in Lisp).
In the Lisp sense Python does not have a REPL, since it does not implement READ, EVAl and PRINT over symbolic data, like Lisp does.
You are correct that the analytic derivative isn't that hard to do in any language.
Edit: Should have said "many languages" not just "any"
func cube_3rd_deriv = NthDeriv(3, &Cube); /* returns a function */
double result = cube_3rd_derive(5.0); #include<stdio.h>
#define DX 0.0001
typedef double (*function)(double);
typedef double (*closure_body)(void*, double);
struct deriv_env { function f; };
struct closure { void* env; closure_body body; };
#define CALL(c, x) ((c)->body((c)->env, (x)))
double deriv_body(void *env, double x) {
function f = (deriv_env *)env->f;
return (f(x + DX) - f(x)) / DX;
}
void deriv(function f, struct closure *out) {
out->env = (void *)f;
out->body = deriv_body;
}
double cube(double x) { return x * x * x; }
int main(void) {
struct closure cube_deriv;
deriv(&cube, &cube_deriv);
printf("%f\n", CALL(&cube_deriv, 2.));
printf("%f\n", CALL(&cube_deriv, 3.));
printf("%f\n", CALL(&cube_deriv, 4.));
return 0;
} double Cube3rdDeriv(double x) {
return NthDeriv(3, &Cube, x);
}
&Cube3rdDeriv can now be passed around in a higher-order way. *Main> derivative f x = (f(x+dx)-f(x))/dx where dx = 0.0001
*Main> derivative (^3) 2
12.000600010022566 def deriv(f): return lambda x: (f(x+dx) - f(x))/dx
or even: deriv = lambda f: lambda x: (f(x+dx) - f(x))/dxMaybe that's the misunderstanding in the original article that made it seem so mindblowing?
Evaluating (f(5.0001) - f(5.0)) / 0.0001 just gives an approximation of the derivative at a certain point in the function (aka the "finite difference method").
https://mitpress.mit.edu/sites/default/files/sicp/full-text/... is the chapter. Turns out it wasn't the "very next" one. But it is in my head.