[Seaside] [ANN] Seaside-Mysql-0.2.1

Avi Bryant avi at beta4.com
Wed Apr 2 22:40:44 CEST 2003


On Thu, 3 Apr 2003, Keith P. Hodges wrote:

> I am pleased to present my first seaside2 efforts. It is a simple
> MySql database table viewer. I would be very grateful for someone to
> review what I have done so far in detail, and I would welcome any
> comments on design, interaction, appropriate use of seaside etc.

Keith, this is fascinating.  Forgive me if I take a moment to be
pedagogical, the opportunity is perfect.  The control flow of your
application is built around using an #onClickDo: method, which allows your
Table component to take a block that is executed when someone clicks on an
item.  My guess is that you modelled it after the WATableReport, which
does something similar, but let me look into how you're using it.  For
example, in #showDatabasesTables, you do this:

 showDatabaseTables

  self call:
    ((WAMysqlTable newDb: mysql resultsSet: mysql showTables)
	 onClickDo: [ :tables | self selectAllFromTable: (tables clickedVal) ])

Now we might call that block you pass in the "continuation" for this
component, since it is what you want to happen (how you want the app to
continue) after the user has made their choice.  What you are doing is
actually called "continuation passing style" which is what it sounds like:
you pass a function's continuation to it as one of the parameters, and
the function invokes it explicitly when it is done.  You never really
return from that function - instead, the continuation takes over.

Now, as you've probably noticed, continuation passing style (CPS) is a
very flexible way of building web applications.  Paul Graham used it
extensively in ViaWeb/Yahoo Store, for example, for exactly the same
reason you do: you can use the same page in a number of different
contexts, by parameterizing the "what happens next" with a continuation.
Not only that, but the continuation is a block defined in the same context
from which you are calling the page - so "what happens next" is sharing
scope with "what happened before", which is a pretty natural and useful
way for things to be.

The only problem is, it can get a little ugly, particularly when you nest
it a couple of times.  If I can inline and
simplify the main control flow of your application, it looks a little like
this:

self call: ((WAMysqlTable newDb: mysql resultsSet: mysql showDatabases)
  onClickDo: [:dbs | self database: (dbs clickedVal).
                     self call: ((WAMysqlTable newDb: mysql
                                  resultsSet: mysql showTables)
                          onClickDo:
                             [:dbs |
                             tableName := dbs clickedVal.
                             self call:
                                ((WAMysqlTable
                                    newDb: mysql
                                    resultsSet: (mysql selectAllFromTable: tableName)
                              ]
               ]

Ok, that's probably hard to undersand when I put it all in one method like
that (your code was much better split up, of course).  But let me factor
out a method so it's a little more legible:

clickValForResults: aResultSet do: aBlock
  self call: ((WAMysqlTable newDb: mysql resultsSet: aResultSet))
   onClickDo: [:dbs | aBlock value: dbs clickVal]

Now your flow becomes

self clickValForResults: mysql showDatabases do:
   [:database |
   self database: database.
   self clickValForResults: mysql showTables do:
    [:table |
    self clickValueForResults: mysql selectAllFromTable: table do: [:v]]]

Hopefully the flow is a little clearer here - first you select a database,
then you select a table from that database, then you see all the rows for
that table.  All done in nice CPS.  And you'll note that (in theory) it
should be back button friendly - if you go back to the list of tables and
click on a different one, the continuation block will get invoked again,
and you'll see the rows for that table instead.

But here's the thing - Seaside doesn't need CPS, because it uses first
class continuations instead.  Every time you send #call:, there is an
implicit continuation, an automatic #onClickDo:, if you like - the code
that comes after the #call:!  If, instead of using
"clickBlock value: ..." in your Table component, you used "self answer:
...", that value would be returned from #call:, and the execution would
keep going from there.  So this code:

self call: ((...) onClickDo: [:dbs | "do something with dbs"])

becomes this code:

dbs := self call: (...).
"do something with dbs"

Or, assuming this convenience method:

showResults: aResultSet
  |dbs|
  dbs := self call: (WAMysqlTable newMysql: mysql resultSet: aResultSet).
  ^ dbs clickVal

then your flow becomes something like this:

self database: (self showResults: mysql showDatabases).
table := self showResults: mysql showTables.
self showResults: (mysql selectAllFromTable: table).

Of course, since the clickVal is all you ever use, I would make that what
was answered from the component - so your method #doClickX:y:val:col:
becomes

WAMysqlTable>>doClickX: x y: y val: val col: col
  self answer: val

Phew!  Does that make sense?

Now, you might wonder, why does TableReport use a click block then?  The
answer is that it was meant more to be embedded then to be #call:ed, and
so the click block is meant as a callback up to its parent component.
Ideally, those two concepts - using continuations via #call: and CPS or
callbacks via #onClickDo: need to be better unified, and that's on my list
of things to do.

> Q. I havent looked at what I need to do to support the back
button
> etc, most stuff kind of works. Forking a new browser doesnt really -
> what is the way ahead?

Ok, so thinking about this, I lied to you when I said the connection
should be in the session.  The problem is that each connection can only be
associated with one database at a time.  So if you want to be able fork
the login window, and log into two databases at once in the same session,
then what you really need to do is this - separate your login component,
which creates connections, from a new component which uses them.  When you
choose a database you create a new connection for it, create a new
instance of this other component, and pass the connection in.  Then if you
fork the browser, each browser window would be associated with a different
instance of the connection, and they could access different databases.
If you do this, the back button will also work properly.

Cheers,
Avi



More information about the Seaside mailing list