SICP in JavaScript
sourceacademy.org
sourceacademy.org
Here is an excerpt, it's not good:
function deriv(exp, variable) {
return is_number(exp)
? 0
: is_variable(exp)
? is_same_variable(exp, variable) ? 1 : 0
: is_sum(exp)
? make_sum(deriv(addend(exp), variable),
deriv(augend(exp), variable))
: is_product(exp)
? make_sum(make_product(multiplier(exp),
deriv(multiplicand(exp),
variable)),
make_product(deriv(multiplier(exp),
variable),
multiplicand(exp)))
: error(exp, "unknown expression type -- deriv");
}
Here is the original code: (define (deriv exp var)
(cond ((number? exp) 0)
((variable? exp)
(if (same-variable? exp var) 1 0))
((sum? exp)
(make-sum (deriv (addend exp) var)
(deriv (augend exp) var)))
((product? exp)
(make-sum
(make-product (multiplier exp)
(deriv (multiplicand exp) var))
(make-product (deriv (multiplier exp) var)
(multiplicand exp))))
(else
(error "unknown expression type -- DERIV" exp))))maybe it's only me, but it's really hard to reason about this.
Oh, but fully agreed, nested ternaries is right out. In fact, usually in those other cases too, it's just annoying that there's not a good alternative.
It's not the biggest thing in the world, and I don't want to distract from the rest of the book, but this is a situation where writing a one or two line helper function:
const _ = (cond, a, b) => cond ? a : b;
would have made the code much more readable without much downside that I can see -- at least to my subjective opinion. Maybe I'm missing something.Edit: comment below correctly points out that if it's important for you to avoid immediate evaluation, you'll need to wrap your conditionals in functions.
_(cond, () => a, () => b)
And _ becomes: const _ = (cond, a, b) => cond? a(): b();
And it does matter in this case when looking at the last condition which signals an error (does not return an error value if I understand it correctly). In which case your _ would raise an error even when not appropriate.I'm not sure it matters here, the error you're pointing out looks to be getting returned (unless I'm misunderstanding what the book intends the `error` function to do), and creating an Error in Javascript is fine, it doesn't break your program until it's actually thrown.
Edit: just looked at your comment again, and you're saying it does actually throw the error rather than returning it :) So double-corrected on my part :)
But your point stands regardless. There will be scenarios where what you're talking about matters -- JSX also follows this pattern of immediate evaluation and yeah, I see errors from that plenty of times. So it's good to mention.
function deriv(exp, variable) {
if (is_number(exp)) {
return 0;
}
if (is_variable(exp)) {
if (is_same_variable(exp, variable)) {
return 1;
} else {
return 0;
}
}
if (is_sum(exp)) {
return make_sum(deriv(addend(exp), variable),
deriv(augend(exp), variable));
}
if (is_product(exp)) {
return make_product(deriv(multiplier(exp),
variable),
multiplicand(exp)))
}
return error(exp, "unknown expression type -- deriv");
}I'll add a couple of things onto this: early returns are very helpful for me in avoiding nesting if statements (although that's less applicable in this specific example).
function op(cond) {
if (cond) {
//do something
}
}
function op (cond) {
if (!cond) { return; }
//do something
}
And it's good to remember that you can basically stick functions anywhere including inline, so it's not necessarily a requirement to take a function like this and move it to a top level as a private function. If you're only using it in one place you can just define it and call it anonymously.And don't be afraid to still use ternary operators non-nested. There's a sibling comment complaining about the nested if statement. If that really bothers you, you can still do:
if (is_variable(exp)) {
return is_same_variable(exp, variable) ? 1 : 0;
} if (is_same_variable(exp, variable)) {
return 1;
} else {
return 0;
}
should be return +is_same_variable(exp, variable)"Languages do not differ in what they make possible, but in what they make easy."
In my implementation nested ternary operators were a parse error.
That said Nystrom's book has adorable drawings, and feels more "passionate".
I've read both, but I only followed the go-book because I was learning Golang at the time. No regrets.
It should also be easier to reason about nested ternaries than an equivalent set of nested if-elses, because at least with ternaries you know that every branch is an expression resulting in some value (and statically typed languages ensure that these values have the correct type), whereas if-else blocks can contain anything, are likely (and in most languages (which lack if-else expressions), forced) to mutate things, and have no guarantee of producing the result that you were expecting, unlike ternaries (especially in statically typed languages).
The reasoning is because ternary operations eliminate programming singularities.
Example:
var x;
if (someExpression) {
x = True;
}
//var x is undefined
Example 2: var x;
x = someExpression ? true : false;
// use of ternary expression prevents var x from ever being undefined.
The ternary expression forces you to handle the alternative case while the if expression can leave a singularity. A hole where x remains undefined.Some people say ternary expressions are less readable. But Readability depends on your "opinion." There are no hard logical facts about this. So it's a weak-ish argument.
It is actual fact (ie not an opinion) that ternary expressions are categorically more Safe then if-statements. Therefore, they are factually better in terms of hard logical metrics.
This is the reasoning behind nested ternary statements. It's also one of the strange cases in programming where superficial qualitative attributes of "readability" trumps hard and logical benefits. The overwhelming majority of the population will in fact find many nested ternary operations highly unreadable and this "opinion" overrides the logical benefit.
Usually though, to maintain safety and readability I just alias boolean statements with variable names. The complexity of a conditional expression can be reduced by modularizing parts of it under a symbolic name, you don't necessarily have to reduce complexity by forcing the conditional into the more readable bracketed spatial structures favored by if-statements.
Example:
isXgreaterThanY = x > y;
isWlessThan2 = w < 2;
isSubExpressionTrue = isXgreaterThanY & isWlessThan2 ? False : True;
isFactNotTrue = isSubExpressionTrue ? False : True;
Yes technically the ternary expressions are still nested in a way here, but when reading this code you don't have to dive in deep past the symbolic name.Imagine if you have a boolean condition that relies on multitudes of these sub expressions. By defining the final statement as a composition of Named ternary expressions you eliminate the possibility of a hole;... a singularity where one alternative wasn't considered. In my opinion this is the best way to define your logic.
If-statements, imo, should be reserved for functions that are impure (void return type).. code that mutates things and touches IO which is something you as a programmer should keep as minimal and segregated away from the rest of your pure logic as possible.
Example:
if true:
sendDataToIO(data)
//the alternative of not sending data to IO is not a singularity. It is also required... a ternary expression makes less sense here.
Last but not least: The ternary expression is also a bit weak because it only deals with two possible branches: True/False. The safest and most readable primitive to deal with multiple branches of logic is exhaustive pattern matching. Both Rust and Haskell have a form of this. In rust, it is the match keyword.In fact, I believe the problem with the JS example's readability is the formatting. Here's how I'd format the same code, and I find this quite readable:
function deriv(exp, variable) {
return is_number(exp)
? 0
: is_variable(exp)
? is_same_variable(exp, variable)
? 1
: 0
: is_sum(exp)
? make_sum(
deriv(addend(exp), variable),
deriv(augend(exp), variable)
)
: is_product(exp)
? make_sum(
make_product(multiplier(exp), deriv(multiplicand(exp), variable)),
make_product(deriv(multiplier(exp), variable), multiplicand(exp))
)
: error(exp, "unknown expression type -- deriv");
}Example
is_same_variable(exp, variable) ? 1 : 0
If is_same_variable returns a truthy value then return 1 else 0.
We can remove that and it will still follow the same while also being more readable
function deriv(exp,v){
//v = variable;
if(is_number(exp) ){ return 0 }
if(is_variable(exp){ return is_same_variable(exp,v) * 1 }
if(is_sum(exp)){
a = deriv(addend(exp),v);
return make_sum(a,a)
}
if(is_product(exp)){
m = multiplier(exp);
c = multiplicand(exp);
a = make_product(m,deriv(c,v));
b = make_product(deriv(m,v),c));
return make_sum(a,b)
}
return error(exp, "unknown expression type -- deriv");
}That said, this is pretty much how I'd write it, and seems a lot simpler than the heavily nested ternary (and I say that as a fan of nested ternaries!)
function deriv(exp, variable) {
return if(is_number(exp))
0
else if(is_variable(exp))
if(is_same_variable(exp, variable))
1
else
0
else if(is_sum(exp))
make_sum(
deriv(addend(exp), variable),
deriv(augend(exp), variable)
)
else if(is_product(exp))
make_sum(
make_product(multiplier(exp), deriv(multiplicand(exp), variable)),
make_product(deriv(multiplier(exp), variable), multiplicand(exp))
)
else error(exp, "unknown expression type -- deriv");
}In f# everything is an expression and it simplifies the language a lot. The last value in a block is considered the value "return" value of the whole block, which has the knockon effect of getting rid of most returns, allows you to check an if/else to return the same type and such. This also works in conjunction with the type inference, so you get an error when you try to return a string in one branch and an int in another.
JavaScript has a limited form of that via the comma operator.
Code example below, `cond` is an example in their tutorial: https://www.sweetjs.org/doc/tutorial.html#sweet-cond
let realTypeof = cond {
case x === null: 'null'
case Array.isArray(x): 'array'
case typeof x === 'object': 'object'
default: typeof x
}
Discussed in 2017 on HN: https://news.ycombinator.com/item?id=14695495The whole point of SCIP was mathematical beauty. Losing that in translation is not good.
Mathematical beauty was one of the ways to make the point, but far from the most important.
As a footnote, one of the changes which happened since publication is the victory of functional. Note that both JavaScript and Python now embody many of the principles, design patterns, and best practices from SICP.
As a second footnote, you're right: SCIP reads better than SICP. "Skippity do da..." (and if you want a third footnote, I know in the original it's zippity, but that's how kids in MY elementary school sang it, so to me, it will always be skippity"
I found the most helpful part of SICP is the chapter 4, but to call it mathematical is a bit like to say "everything is foundamentally math"...
Because Scheme - being a LISP - has the same syntax for code and data, metalinguistic abstraction is facilitated. Nobody should have to read through boilerplate code to dig up the core message of SICP.
I took the Algorithmics I course based on SICP/Scheme in 1993 at the University of Erlangen, and although I never used Scheme or CommonLISP in production, it made me a better programmer regardless in what language. Thanks to Abelson and Sussman!
Functional core, imperative wrapper is one of the few sane ways to build a large (Conway’s Law afflicted) system, and too many of my peers never studies SICP in school. They don’t even know the damage they do, and as we’ve learned in many, many other circles, vilifying people does not get them to change. It’s a tool of last resort, and most often used to publicly label someone for ostracism, not help.
So excuse me, but y’all are crazy. We need SICP for JavaScript developers more than we need SICP for every functional language in the world, combined. And JavaScript is only one corner of the programming world.
Write functional code in a language that does not force you to write functional code. Take the training wheels off. Live in the real world.
(define lol
(let ((a 0))
(lambda ()
(set! a (+ a 1))
a)))
Scheme is a functional programming language in the same way python is. The only difference is that it has tail call optimisation and the stack doesn't have an arbitrary limit on the number of function calls it can hold.True, but it's a very obtuse corner!
So the code for versions of the book written using those languages would need to be restructured, losing a big chunk of elegance and possibly destroying the actual lessons about how to express oneself as a computer programmer.
This is really the death knell of SICP isn't it?
I'm using SICP for self-learning and I had the choice of Scheme, JS, or Python. I still chose Scheme.
This is because I figured Scheme would offer something those other languages didn't since Scheme is the original language of the book and Scheme is just "different" than other languages. It forces you to think differently about programming.
I'm glad I chose Scheme. SICP with Scheme is the best choice imo since Scheme most clearly illustrates the lessons of the book.
Even having them ship it to you will probably be cheaper than that CAD price. It's unfortunate the price has shot up so much, I guess I'll be taking very good care of my hardback edition.
https://mitp-content-server.mit.edu/books/content/sectbyfn/b...
Used books are awesome. I have north of 500 books and I doubt more than 50 were purchased new. Only that many because some of my relatives want Christmas and birthday wishlists every year, but won't shop anywhere but Amazon and think it's weird to buy someone a used book as a gift (I'd rather have 2-3x as many books for the same money, but hey, I'm not the one paying, so whatever, I guess).
I still believe there is room for SICP in the CS curriculum. While as an introductory language SICP and Scheme may lose out to more commercially popular languages like Python and Java, I still believe that eventually students should be exposed to Scheme as an introduction to functional programming and to demonstrate a minimal, highly flexible language as a vehicle for teaching programming language design and implementation. Then eventually the students can be introduced to the world of statically-typed functional programming languages like the ML family and Haskell.
Sure you can use a spectrum scan or the output of a radio telescope, but it's really not the same as looking at in a telescope.
Same thing here -- SICP just isn't the same if it's not in scheme or lisp.
So then astronomy is about telescopes...?
I'm happy to accept SICP is intimately related to lisp (which means it is less general than people claim) but just as long as we're consistent.
No? I never said that. SICP teaches concepts, those concepts are better understood with lisp.
Astronomy teaches concepts, those concepts are better understood with telescopes.
In neither case is the subject about the tool, it's just better understood with the tool.
>Same thing here -- SICP just isn't the same if it's not in scheme or lisp.
That straight up reads like SICP and lisp are inseparable and that exactly means that SICP is not as general as people claim.
What we have here is unstoppable force meets immovable object (otherwise known as a contradiction). The resolution is simple: either SICP is valuable for the ideas, in which case renderings in js or python are fine, or SICP's value is diminished without lisp, in which case it's not actually about ideas but about the language of choice.
Can't have it both ways.
You can use other tools to understand it, but it will be more work and you won't understand it as well.
Just like you could use a screwdriver to drive a nail into a board. But it will be easier and better understood if you use a hammer. The nail provides the same value either way.
They are separable, but there is a loss of elegance.
For example Scheme has linked lists and symbols built-in in an elegant way. The JavaScript version tries to provide them, but in a clunky way. Symbols are replaced by strings and the (a b c) notation of Scheme looks as ["a", ["b", ["c", null]]] or list("a" "b" "c") in the JavaScript version.
That a text may make use of the language it is written in, that should not be surprising. Thus the language shapes what and how it is said.
I haven't read the original, but knowing Scheme I could see why the book would be structured the way it is. In JavaScript, though, it really just doesn't work. The code is horribly non-idiomatic for JS, and because JS isn't actually Scheme the order of the concepts introduced doesn't make sense. When it does talk about JavaScript, it's condescending towards it in ways that were off-putting to my coworkers (we're a TS shop), which is weird given that it's a book in JS.
Someday I'm going to get around to reading the original, but I can't recommend this one.
Anyone with strong opinions about mainstream programming languages is automatically suspicious to me.
I mean, I get it - plenty more elegant and better thought-out languages than JavaScript, with their practitioners pounding the floor, saying "it should have been me - not him!".
But, ultimately, who can't write good code just because they're not using their favourite language?
Not quite the right analogy... JS isn't as bad as Oil. But you get the idea...
Kinda... don't make sense?
Then you should probably SCIP SCIP.
To be more precise, as Thomson and Thompson (of Tintin comics) would say, I meant it as a double irony on the GP's misspelling or typo.
SICP(SICP)
;-)
Only the math and Lisp / Scheme cognoscenti will get that one.
You sound like Google.
not just by the literal text "Did you mean" that you used, but also due to being like Google in telling us what and how to think and read, or in (mis)interpreting the meaning of what we type.
You have a bright future.
In the above cases, I chose not to, for whatever reason (I'm not sure or don't remember why).
But your comment makes me think that I should have used it in some of the cases above.
I was someone who thought he knew all about computers because I could program a tiny bit of C++ and Java, and thought this class would be a piece of cake. I couldn't have been more mistaken. The class kicked my arse so badly. I remember my score for the midterm: 34/75. I ended up getting a B– for it. I'll be honest, I hated it at first. Where were the loops, where were the arrays? Why was everything involving recursion? Why did some recursive code give rise to iterative algorithms, and others, recursive ones (the difference is tail-call optimisation to give constant stack space versus some growing stack space)? Why were lists this weird nested square bracket syntax???
The irony is that in my fifth year last year, I ended up becoming a teaching assistant for the very same module itself, because I realised how useful the module would be. There was truly a little bit of everything: from algorithmic complexity, to data structures, to even a bit of compilers and garbage collection, with the environment model of execution, meta-circular evaluator at the end of the textbook, respectively.
I had to prove to the lecturer that despite my rather lousy grade in the module itself, my experience in everything else counted as something. A compilers class that was conducted entirely in OCaml was a great refresher on functional programming techniques, as was my internships where I learnt C#'s LINQ. I read the textbook end-to-end while preparing to teach the class—something I ought to have done as a student in the first place. My opinion of the class has changed drastically: it's one of the best introductory CS modules there is, and the fact that NUS chose to use SICP as a textbook is one reason why its CS department has skyrocketed.
I have no comment about emulating Scheme with JS. I neither like nor use either language.
[1]: https://nusmods.com/courses/CS1101S/programming-methodology
Yes.
The key word here is allegedly.
It's like how people rue the fact (a meme by now) that the (self-described) allegedly smartest minds in the world are desperately busy trying to, guess what, make people click more on ads(!), while largely ignoring ethics like giving people a choice about it, via opt-in rather than opt-out, confusing legalese in Privacy Policies and Terms and Conditions, fine print, etc.
SICP – JavaScript Version (2022) [pdf] - https://news.ycombinator.com/item?id=30033052 - Jan 2022 (2 comments)
SICP: JavaScript Edition available for pre-order - https://news.ycombinator.com/item?id=30016323 - Jan 2022 (117 comments)
SICP – Upcoming JavaScript Edition - https://news.ycombinator.com/item?id=27737942 - July 2021 (2 comments)
Structure and Interpretation of Computer Programs – JavaScript Adaptation - https://news.ycombinator.com/item?id=21822903 - Dec 2019 (180 comments)
SICP JavaScript Going Public - https://news.ycombinator.com/item?id=21779397 - Dec 2019 (1 comment)
SICP translated to JavaScript - https://news.ycombinator.com/item?id=6385617 - Sept 2013 (107 comments)
"All the compound data objects we have used so far were constructed ultimately from numbers. In this section we extend the representational capability of our language by introducing the ability to work with strings of characters as data."
In SICP there were Lisp's symbolic expressions (s-expressions). They were removed without even mentioning them...
https://sourceacademy.org/sicpjs/index is the URL you get redirected to in other browsers. (It still doesn't work in safari.)
Pretty happy with the content and the way it was taught.