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


Reply via email to