Seaside – Developing Sophisticated Web Applications in Smalltalk
seaside.st
seaside.st
But Javascript has come a looong way in the last 15 years, and sophisticated web applications do a lot more client-side processing these days. That leads to different architectures, where continuations aren't as advantageous as they had been.
Hacker News is notable in that it doesn't use much client-side code and sticks to an old-school links-and-forms UI. Closures still make a lot of sense for that architecture.
Writing the code to phase out those handlers was a deep lesson for me, because the new code came out more verbose and harder to follow. One rarely gets to do an apples-to-apples comparison of programming techniques on a production system, and somewhat to my surprise the experience convinced me (I was agnostic to begin with) that the closure approach is superior.
There are at least four reasons: (1) the closure has immediate access to all the state that existed when the request was previously processed; (2) the closure is nested inside the code that creates it, so everything you need to understand a full request cycle is in one place; (3) much boilerplate is eliminated, making the code easier to read and write—it can be explicitly at the problem level; (4) security is less of a worry when the client only has to pass back a token instead of all the original parameters to the request, the latter being safely packed away in the closure. That last advantage, i.e. reduced attack surface, was particularly surprising.
Learning this the hard way, by dismantling those advantages and replacing them with manual code in selected cases, taught me something else about this design: it's more than just a clever device. It's really a triumph of two of the greatest programming ideas: first-class functions—more specifically, what used to be called upward funargs [2] back when people were arguing about whether they were a good idea—and lexical scoping. The combination of those two abstractions is what allows this model to shuffle off most of the gruntwork instead of making you, in pg's memorable phrase, a human compiler.
The fact that the design eventually hit scalability problems at millions of users is also a vindication of it, in the sense that it enabled pg to write HN more quickly and safely in the first place. We could selectively deal with the scalability problems later, and have done so. That has worked fine as an engineering tradeoff, but IMO a better solution will come from new implementation ideas that allow another couple orders of magnitude of closures to be kept without slowing down the rest of the system. If we get there, I will take pleasure in rolling all that manual code back into the original design.
1. They're closures, not continuations, which for HN is fine.
2. Meaning the ability to return a function from a function, and thus keep it around indefinitely, as opposed to passing a function to another function as a parameter (downward funargs).
Client-side AJAX apps also allow you to keep state from one request to the next, instead of tossing it back and forth over the net. You can use closures on buttons and everything. In some ways it's even more natural than keeping state on the server, because the client knows exactly how long the page stays open. And it doesn't break when you have lots of users, because they keep the state instead of you. I guess that's part of the reason why AJAX got so popular :-)
That said, if I were building a site like HN, I wouldn't use any of that stuff. I'd go with a bog standard request-response model. There's just not that much state to be preserved. Except maybe draft comments, but these should go in browser storage anyway.
> There's just not that much state to be preserved
Are you sure? From my perspective there's enough to make the complexity difference in the two models significant.
Out of the stuff that I did see, the "goto" parameter is the most obvious candidate for the closure treatment. It's also the most obvious candidate for the AJAX treatment, if you add in-place editing like on Reddit, which would be a better UI in my opinion.
Sorry if that sounds arrogant! Of course there could be a lot of functionality that I've missed.
The free "Dynamic Web Development with Seaside" book guides you through creating an ajax webapp, but the last time I checked the chapters on persistence were kind of weak. That being said, for toy webapps it's sufficient to just save the state of the Seaside image, with a singleton as your "database".
[1] https://docs.racket-lang.org/web-server/Note that you don't have to expire continuations, necessarily—you could actually persist them statelessly (i.e. without server-side state), by shipping their encoded representations to the client embedded into HMAC-signed links. You're basically giving the server a raw bytecode-eval endpoint, and then making sure that it only accepts code you yourself wrote. Kind of a crazy strategy compared to the standard predeclared REST API, but interestingly flexible.
What's an example of such a representation?
Here's the relevant section of the Erlang "External Term Format encoding" spec: http://erlang.org/doc/apps/erts/erl_ext_dist.html#id95269
Said spec is nominally "about" the wire protocol Erlang nodes use to communicate with one another in a distributed system. But the ETF format defined there is much more general, and is used all over the place (e.g. when persisting values in Erlang's DETS tables to disk.) It's what you get when you call Erlang's global term_to_binary/1 function.
Erlang's ETF could probably be most closely compared to Python's "pickle" format. (Though, sadly, you can't pickle a lexical closure in Python.)
I really like your design idea, and it would be fun to think of an application for which it was the best fit.
What this all means is that, although Erlang nodes can rehydrate closures from other nodes that are running the same code, they can't rehydrate arbitrary closures defined and serialized at your personal REPL running different code. (Which defeats half the purpose; you can't then use dehydrated closures as an arbitrary query/callback/RPC syntax between Erlang-based microservices, unless they all share code.)
On the other hand, there's nothing explicitly stopping the Erlang Run Time System from supporting the full version, where a closure is dehydrated as its AST (plus free vars, as in ETF encoding.) And so this is exactly what Elixir does. Yet another reason Elixir is an interesting language.
"Inverting back the inversion of control" describes the idea (in Scheme) https://pages.lip6.fr/Christian.Queinnec/PDF/www.pdf
Paul Graham used the idea in ViaWeb - and apparently got a patent on some aspect of it https://web.archive.org/web/20060323211251/http://paulgraham...
I suspect it didn't catch on because (1) people are afraid of continuations, and (2) web apps are typically designed in at least a pseudo-REST-like style, so the paradigm of "asking" the user (like "await" in ES6) isn't needed very often.
But it's a cool thing to be able to do, especially for funnel-type flows, and other types of more complex session-based interactions.
I wonder if there are any JavaScript inversion of control things for client side code. I've been working for the past week on a signup funnel with React and Flux, and it's just a big explicit state machine... which kind of works, but is also kind of tedious.
There were client side JavaScript continuation like things. Back in the mid-2000's there was Narrative JavaScript. I used it to implement threads and Alice ML style futures/promises on the client side: https://bluishcoder.co.nz/2006/06/05/more-concurrency-in-nar...
IIRC it uses Stackless Python, not the standard interpreter, and I don't know how active its development is.
I'm not a Smalltalk developer; I ask this as someone who finds himself missing Objective-C's dynamism when programming C# or Java, so I'm sympathetic to the advantages of Smalltalk. But customers know .NET and Java. That's not a hard sell.
There is a bootstrap package for Seaside, check out the demo page at: http://pharo.pharocloud.com
It is true that meanwhile single page applications have hit the client web space - but you can address them easily with the Seaside-REST package (for instance sending JSON to clients side AngularJS). If you need a framework with less footprint check out "Teapot" which you can easily run on Pharo (http://www.pharo.org).
Jim: Yo bob!, how's it going man?
Bob: Not so much, what's going on your side?
Jim: Nah, not so much, how's your family?
Bob: They're fine, how's your wife?
Jim: All is good man, all is good...
Bob: ....
Jim:.....
Bob: Hey we should make a webapp together!
Jim: That would be totally rad !..
Jim: .. I've even heard there's a sophisticated webapp framework for people having a smalltalk like us!
Bob: Whoa that's cool !, let's get on it man!
Jim: Smashing!!, But what kind of app should we work on?!
Bob: I don't know let's just download it and make something simple
Jim: Darn, it's only made for sophisticated webapps, says so on their website:(
Bob: Naaah, life sucks man, lets wait till they support simple stuff in the future
Jim: Sure thing, let's take it in our next smalltalk, gotta run now I have a scrum!
Bob: Ok, I'll get my coffee and leave before no one notices I don't even work here!
Jim: K man, cya!