[plt-scheme] A matter of design
Hello all,
I hereby come asking you to read a long, boring post, for some ideas
on design. :-)
So, I'm developing on my free time, an advanced calculator as a
DrScheme tool (the only thing connecting it to DrScheme at the moment
is that it cannot run elsewhere).
The user is supposed to interact with the tool using a REPL, and it
should be able to load calculator components during run-time.
For this I implemented a core in Scheme which starts a read-eval-print
loop with the usual read, a special eval and a special print.
The printer just prints values in a special way (I'll explain what
values are further down) in DrScheme (basically generates picts of the
values which DrScheme happily renders in the REPL, that's why it is
connected to DrScheme).
The user/developer can define values and functions as scheme units and
load them at runtime in the calculator. These values are units which
import a structure property type and exports a name (symbol) and a
series of lambdas (with attached contracts), a constructor, a printer
and a copier (I don't think the mutator is needed because I won't let
the user mutate values directly).
These are loaded by the core and added to hash tables keyed by the
integer (one hash-table for constructors, one for printers, etc.). So,
if I do > ((constructor 'integer) 2), my eval will get the constructor
for the integer and apply it to 2. The return value, a structure
defined in the integer unit, will be printed by my printer. How do I
know the structure is an integer? Because it has the property type
imported by the unit set in the structure as a symbol: 'integer. So, I
get the value printer, and pass it the value.
This already works quite well, I can create values and they are
printed by the repl, but nothing much.
I wanted to define functions... but these are quite an headache that
I'm trying my best to solve it cleanly.
On one hand I wanted also the value unit to export the basic type
operations so that functions don't need to access the value internals.
So, for example, the integer unit could export a hash-table with
+: int x int -> int
*: int x int -> int
-: int x int -> int... etc.
And the example of a function could be the factorial which would be
defined as a unit which exports its name, lambda and would require
(not import) the values units needed to compute the function (in this
case the integer value unit) and then import the unit values to the
function unit namespace.
Now there are two problems. This is not as clean as I would like.
Issues:
On functions:
- Factorial will require the integer unit, then probably use
define-values/invoke-unit to import the values basic operations. In
this case for a trivial factorial definition It would need zero?, *
and dec. But if the basic operations are returned in hash-table it
would have to do:
(define int:* (hash-table-get int:basic-ops '*)
(define int:dec (hash-table-get int:basic-ops 'dec)
etc.
Which is not beautiful at all...
On the core side we will have a huge issue... integer values defines
basic operations on integers +, -, etc with attached contracts and
puts this all in the functions hash-table. Then comes the matrix value
and defines +, -, * on matrices but with different contracts.
Now the user does (+ matrix matrix) or something... how is the core
supposed to know which + it is talking about (not to mention the best
way to keep this.
My initial feeling which I gave a thought about in the last days while
walking home at the end of the day is the trivial thing (which would
have some problems probably) of having an hash-table with a list of
lambdas as value. So + would have a list of lambdas. Each with a
different contract. One would be + : int x int -> int, other would be
+ : matrix x matrix -> matrix. And when the user calls +, the core
gets all the lambdas and tries to apply them one by one, catching the
contract failure exception thrown until one of the lambdas gets called
without raising an exception. It meant the operation can handle the
types and will do what the user asked for. This is however,
inefficient. If I have + for integer, complex, rational, matrix,
vector... etc in the worst case it will have to call and catch all of
the lambdas and probably fail in the end if you are trying to sum two
things 'not summable'.
So, my request is if you have any idea on how to find a cleaner
designer or how to implement these stuff in a more sensible, to reply.
Thank you very much,
--
Paulo Jorge Matos - pocm at soton.ac.uk
http://www.personal.soton.ac.uk/pocm
PhD Student @ ECS
University of Southampton, UK