Re: Explanation of macros; Haskell macros
From: Dirk Thierbach (dthierbach_at_gmx.de)
Date: Thu, 6 Nov 2003 16:00:37 +0100
Coby Beck <firstname.lastname@example.org> wrote:
> The problem was a fairly ordinary one and is easy to describe at a high
> level. The server existed to control a complicated configuration process
> and later process data according to the result. It was a simple socket
> server and I got to dictate the form of the commands and arguments.
> Ultimately, it boiled down to (funcall command client-session args).
> I would be very happy to learn other approaches, there are always
> many ways to skin a cat. (how un-PC is that saying these days ;)
Here's a very simplified example how to do something similar in
Haskell. I probably missed lots of details (I don't know how the
session object works, and have no idea what the session-blueprints
are, I have no details about the send and receive commands, and so on),
but maybe it should give you an idea.
The main task is to define a function that implements a server
command, given some constraints and the code for the command.
The command should work on the arguments received over the socket.
So we need some code that does the marshalling und un-marshalling of
the data. Here, I will cheat and just use the "Dynamic" library
to simulate that. In reality, one could use a similar approach
to write marshalling routines, or use the standard read and show
routines, or whatever. I'll use the functions
toDyn :: Typeable a => a -> Dynamic
that converts any "typeable" value into a Dynamic value, and
fromDynamic :: Typeable a => Dynamic -> Maybe a
which converts it back, or fails with a value of "Nothing" if the type
doesn't match. The class Typeable will need instances for all the
types we want to convert.
Let's assume there is a type "Session" for sessions. Then the type
of define_server_cmd will be
define_server_cmd :: Typeable a => (a -> Bool) -> (Session -> a -> IO ())
-> (Session -> Dynamic -> IO ())
which pretty much expresses out expectations about this function given
above. The implementation is straightforward:
define_server_cmd constraints code session dyn_args = do
(error "Wrong type of arguments")
(\args -> if constraints args
then code session args
else error "Constraints violated"
Your set-field-sequence example would probably look something like
(I am guessing a lot here)
set_field_sequence = define_server_cmd constraints code where
constraints field_list = all (`elem` logical_types) field_list
code session field_list = do
let field_sequence = source_blueprint session
state = get_state session
writeIORef field_sequence field_list
send session (show state)
You don't need the first constraint, because the unmarshalling will
check if the arguments are indeed a list. I am not sure if the
second constraint is also a type check or if it checks the contents
or names of the fields.
As another example, let's define the code for a print command
seperately (so it is testable), and then make a server command out
of it with the constraint that the first argument is less then 100:
print_cmd_code :: Session -> (Integer, String) -> IO ()
print_cmd_code session (x, y) =
print ("First=" ++ show x ++ ", Second=" ++ show y)
print_cmd = define_server_cmd (\(x, _) -> x < 100) print_cmd_code
As I said, this is a very simple example, so let's see what is missing.
> So I needed (wanted) a way to, in one stroke,
> - define the method, properly specialized on the session object
> - ensure it became an allowed function to call
> - define a way to gather the arguments needed from the client
> according to number of args and the type of each.
Should work. Note that the compiler automatically figures out at
compile time which code to combinbe to marshall or unmarshall the
"dynamic" value, and verifies the type as well. No need to do this
> - provide a facility for arbitrarily complex validation of any
> of the arguments or combinations thereof.
> - ensure that any changes, enhancements or additions to
> argument passing would require a single point of change in my code.
One potential drawback of the above approach is that you have to write
down the argument list twice, once for the contraint check, and once
for the actual code. But since one might want to do pattern matching
on the arguments, this might be exactly the right thing and not a
drawback. The type checker will verify that the arguments are of the
same for both parts.
> Additional benefits:
> - automatically provided information to a kind of "help" facility.
I didn't see that in your code, but one could probably handle this
in the same ways as errors below.
> - allowed for very informative error messages, both in the debug
> environment and as returned data for clients.
I kept the error handling *very* simple by just throwing exceptions.
In reality, this would be handled by an error function that sends back
the error message. Type errors could be automatically handled. It is
of course not possible to send back sourc code as a string without
using a macro; and while error messages of this kind are arguably
informative to someone who knows lisp, they might be very confusing to
someone who doesn't :-) So I'd add explicit error messages, and maybe
a few infix functions as syntactic sugar to write them down nicely.
> - simplified the main server loop while remaining completely flexible
The main loop would need to lookup the server functions (which all
have the same type), and then execute the functions without
unmarshalling the arguments, since that is done inside the
function. Also simple and flexible.
> I did have to define a set of safe-read functions for lists and
> integers and strings etc for a combination of security and
> timing-out reasons.
Yes. This code would go in the typeclasses mentioned at the beginning,
and as you say, you'd need that in Lisp, too.
> But the grammar was just s-expressions.
The grammar is completely up to the coder, be it s-expressions, XML, or
whatever. However, it should include the type of the arguments passed
over the socket.
> I actually have macros I use now that generate strings of SQL code,
> some of it as format strings that will be used at a later stage of
> processing (a "format string" would be something like "SELECT NEW.~A
> FROM ~A WHERE ~A > 3" where the ~A's get filled with variables
It might be interesting for you to have a look at HaXML or one of the
other XML libraries that do a similar thing with XML, using only
HOFs. I don't see any reason why SQL couldn't be handled in a similar way.
> But back to macros, and really any language feature, they are all
> just tools and it is always a judgment call as to what the best tool
> for a job is and judgements are always subjective. It is very hard
> to convince anyone that a tool they are completely unfamiliar with
> is the best one for some problem they never thought they had.
Amen to that.