Friday, May 11th, 2007
An hour in the life of a Curl developer
I’ve been working on some sample applications with a local database, and needed to use transactions around a bulk import.
The standard coding idiom for that is to initiate a SQL transaction, and perform the operation wrapped in a try block, finalizing with commit or rollback on success or failure.
{con.execute database, "BEGIN;"}
{try
{con.import-csv "db", "customers", "customers.csv"}
{con.execute database, "COMMIT;"}
catch e:Exception do
{con.execute database, "ROLLBACK;"}
{throw e}
}
This code is relatively readable, especially if you recognize the idiom, but there’s quite a bit of boilerplate around the essential intent, which is a nuisance to write, and a bigger nuisance to read.
In short, it violates the DRY principle, and is a candidate for refactoring.
The obvious solution is to move the plumbing into the basement, by defining a method which performs an arbitrary input action inside the transaction idiom.
{method public {with-transaction
database:String,
action:{proc-type {}:void}
}:void
{self.execute database, "BEGIN;"}
{try
{action}
{self.execute database, "COMMIT;"}
catch e:Exception do
{self.execute database, "ROLLBACK;"}
{throw e}
}
}
Now the transaction idiom is encapsulated, and the application needs only to supply the desired action procedure.
{con.with-transaction "db"
{proc {}:void
{con.import-csv "db", "customers", "customers.csv"}
}
}
The signal to noise ratio is imporved. But there’s still the thunk. You may find it comprehensible, concise, or even elegant, but its still plumbing, and unrelated to the design intent, which is the primary focus of the author and readers of the code.
This requires extending the syntax of the language itself. Fortunately, Curl has a powerful macro facility, with a pattern matching language for the input, the full power of the language for the transformation.
{define-macro
public {with-transaction
on ?con:expression
for ?db:{literal String}
do
?body:statements
}
{return
{expand-template
{?con.with-transaction ?db,
{proc {}:void
?body}}
}}
}
Now the application program contains nothing but design intent: a transaction, which connection, which database, what operation.
{with-transaction on con for "db" do
{con.import-csv "db", "customers", "customers.csv"}
}
Those of you who have browsed the Curl Developer’s Guide and API Guide may recognize the ‘with’ naming convention. Its used for “transactional” declarations, that wrap some contextual processing around a body of code.
with-open-streams
with-render-properties
with-compiler-directives
with
Now you know how they were implemented.