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()