I was participating in a discussion on another mailing list about how
to make it more expensive, and thus less attractive, to commit spam.
I needed to do a fair bit of calculation for this, so I had the
opportunity to reflect on current technology.
I find the traditional REPL extremely annoying for use as a desk
calculator. I like spreadsheets a lot better, but I don't have any
decent spreadsheets on my laptop.
But I do have Numerical Python, which lets me do part of what
spreadsheets do: easy array computation. In some ways, it's a lot
more powerful than a spreadsheet; it has better array operations,
things like outer product. But it generally doesn't have the
instant-feedback feeling you get from a spreadsheet. I used it
anyway, via the traditional Python REPL.
There was this thing recently on Sweetcode called Elca:
Elca (Extended Line Calculator) is a real-time Perl calculator
(i.e., it evaluates expressions immediately as you type them). It
supports hexadecimal, octal, and binary numbers, complex numbers,
variables, string, list and hash operations, etc.
I never did get Elca to run (I think it needs a less antediluvian
version of Perl than I have on my laptop), but it sounded inspiring.
So I thought I'd try to hack up something like a cross between Elca
and a spreadsheet, with access to Numerical Python thrown in for extra
power. It still needs a lot of polish, but it's already a useful
tool. Please be warned that the "save" feature will overwrite
existing files without prompting.
Here's a sample file of formulas that duplicates the work I did for
the email discussion.
sizes = Numeric.array([1, 2, 4]) * 1024
matrix = Numeric.divide.outer([56000., 384000, 1540000], sizes)/8
= 1/matrix
= 3*matrix
spams_per_month = 365.25/12 * 86500 * matrix
dollars_per_spam = Numeric.transpose([40, 60, 2000] /
Numeric.transpose(spams_per_month))
= min(dollars_per_spam)
= max(dollars_per_spam)
kwh_per_cpu_per_month = 150. / 1000 * 24 * 365.25 / 12
= kwh_per_cpu_per_month * .12
And here's the program that gives you an interactive environment for
these expressions. I've tested it under Python 2.1 and 1.5.2 on
Linux; it depends on Tkinter, and the above input file (which you can
load with the "Open" button) depends on Numeric, although some parts
of it will work without Numeric.
#!/usr/bin/python
# interactive Tk environment
import Tkinter, traceback, sys, string, types
# useful for expressions:
import math
# useful for expressions if it's installed:
try: import Numeric
except ImportError: pass
# bug list:
# - output font too big
# - when the window resizes, the entry often scrolls away from the cursor until
# the next time the window is displayed
# - no way to import more modules (except commented out)
# - no topological sort
# - add fields button is still there
# - no way to delete or rearrange cells
# - neither str() nor repr() is really ideal; probably repr() with
# 80-column wrapping would be better
# - because results are in labels, they aren't scrollable or cut-and-pastable
# - no automatic dependent recalculation
# - namespace headaches: if someone makes a variable called 'field', then
# everything will still work except for loading files, which says something
# like "object of type 'int' is not callable". Also, old values are left
# around when the name has changed.
# - no error handling or overwrite protection in file access
# - loading files should perhaps erase existing fields! No way to erase 'em
# now.
# - tab order is wrong: load and save come after fields and before Add field.
# - save file format could easily be legal Python, but isn't
def modules():
rv = []
for name, value in globals().items():
if type(value) is types.ModuleType:
rv.append(name)
return rv
mw = Tkinter.Tk()
##def importsomething():
## try: exec "import " + importentry.get()
## except: pass
##importframe = Tkinter.Frame()
##importbutton = Tkinter.Button(importframe, text='import',
## command=importsomething)
##importentry = Tkinter.Entry(importframe)
##importbutton.pack(side='left')
##importentry.pack(side='left')
##importframe.pack()
fields = []
def chomp(astring):
if astring and astring[-1] in '\r\n': astring = astring[:-1]
return astring
def loadfile(filename):
infile = open(filename)
try:
while 1:
line = infile.readline()
if not line: break
line = chomp(line)
pos = string.find(line, "=")
if pos != -1:
field(string.strip(line[:pos]), string.strip(line[pos+1:]))
finally: infile.close()
def savefile(filename):
outfile = open(filename, 'w')
try:
for eachfield in fields:
eachfield.dump(outfile)
finally: outfile.close()
fileframe = Tkinter.Frame()
loadentry = Tkinter.Entry(fileframe)
loadbutton = Tkinter.Button(fileframe, text="Open", command=lambda:
loadfile(loadentry.get()))
savebutton = Tkinter.Button(fileframe, text="Save", command=lambda:
savefile(saveentry.get()))
saveentry = Tkinter.Entry(fileframe)
for widget in [loadentry, loadbutton, savebutton, saveentry]:
widget.pack(side='left')
fileframe.pack()
class field:
def __init__(self, name='', value=''):
frame = Tkinter.Frame(mw)
self.name = Tkinter.StringVar()
nameframe = Tkinter.Entry(frame, width=5, textvariable=self.name)
self.name.set(name)
self.var = Tkinter.StringVar()
expression = Tkinter.Entry(frame, textvariable=self.var)
self.lastresult = "(no value yet)"
self.output = Tkinter.Label(frame, text="(no value yet)",
font="courier-10", justify='left')
self.indicator = Tkinter.Button(frame, command=self.display_error)
self.error = "(no error yet)"
self.var.trace('w', self.set_output)
self.name.trace('w', self.store_value)
self.var.set(value) # after trace!
nameframe.pack(side='left')
expression.pack(side='left', expand=1, fill='x')
self.indicator.pack(side='left', anchor='nw')
self.output.pack(side='left')
frame.pack(expand=1, fill='both')
expression.focus()
fields.append(self)
def dump(self, outfile):
for str in [self.name.get(), " = ", self.var.get(), "\n"]:
outfile.write(str)
def store_value(self, *crap):
globals()[self.name.get()] = self.lastresult
def set_output(self, crap1, crap2, crap3):
try:
# the lambda might be handy for figuring out what vars
# are referred to for topological sorting. The 'dis' module
# demonstrates how to find LOAD_GLOBAL instructions to do
# the topological sort.
myfunc = eval("lambda: (%s\n) " % self.var.get())
self.lastresult = myfunc()
mytext = str(self.lastresult)
succeeded = 1
except:
strings = apply(traceback.format_exception, sys.exc_info())
self.error = string.join(strings, "")
succeeded = 0
if succeeded:
self.store_value()
if len(mytext) > 5000:
# My X server crashes if I try to display 'x' * 20000 in a Tk
# label. This is a quick half-assed workaround to keep
# that from happening by mistake.
mytext = mytext[:5000] + ' (truncated)'
self.output.configure(text=mytext, foreground='Black')
self.indicator.configure(background='#d9d9d9')
self.error = '(no error)'
else:
self.output.configure(foreground="#777777")
self.indicator.configure(background='#ff7777')
def display_error(self):
self.output.configure(text=self.error)
mb = Tkinter.Button(mw, text="Add field", command=field)
field()
mb.pack(side='bottom')
mw.mainloop()