#!/usr/bin/python3 """Exploration of "principled APL" ideas, based on Codd and modal logic. See file `principled-apl-codd.md`. Named after Jaakko Hintikka. Lacking: - index/join - assertion to reduce the possible worlds - pretty HTML output in Jupyter (how does Pandas do this?) """ import operator class Column: def __init__(self, keys, values): self.keys = keys self.values = values def _repr_html_(self): rv = [''] for key in self.keys: rv.append(f'') for key in keys: rv.append(f'
{htmlescape(key)}') for keys, val in self.values.items(): rv.append('
{htmlescape(str(key))}') rv.append(f'{htmlescape(str(val))}') rv.append("
") return ''.join(rv) def append(self, key, val): assert len(key) == len(self.keys) self.values[key] = val def __str__(self): return '\n'.join(['\t'.join(self.keys)] + ['\t'.join(tuple(map(str, k)) + (str(v),)) for k, v in self.values.items()]) + '\n' def apply(self, other, f): # I’m trying to do a very dumb brute force thing here because # the structure of a reasonable solution is not crystallizing # for me right now. keys = tuple(sorted(set(self.keys) | set(other.keys))) keyvals = {keyname: set() for keyname in keys} for mykey in self.values: for keyname, keyval in zip(self.keys, mykey): keyvals[keyname].add(keyval) for theirkey in other.values: for keyname, keyval in zip(other.keys, theirkey): keyvals[keyname].add(keyval) results = {} for assignment in all_possibilities(list(keyvals.items())): mykey = tuple(assignment[k] for k in self.keys) theirkey = tuple(assignment[k] for k in other.keys) if mykey in self.values and theirkey in other.values: results[tuple(assignment[k] for k in keys)] = f( self.values[mykey], other.values[theirkey]) return Column(keys, results) def __add__(self, other): return self.apply(as_column(other), operator.add) def __mul__(self, other): return self.apply(as_column(other), operator.mul) # XXX include the other 53 operators def aggregate(self, columns, f): # Again, a super dumb approach. key_indexes = [i for i in range(len(self.keys)) if self.keys[i] not in columns] accumulator = {} for mykey in self.values: reduced_key = tuple(mykey[i] for i in key_indexes) if reduced_key not in accumulator: accumulator[reduced_key] = [] accumulator[reduced_key].append(self.values[mykey]) return Column(tuple(self.keys[i] for i in key_indexes), {k: f(v) for k, v in accumulator.items()}) def htmlescape(s): return s.replace('&', '&').replace('<', '<') def as_column(val): return (constant(val) if type(val) is str or type(val) is float or type(val) is int else val) def all_possibilities(domains): if not domains: yield {} return key, values = domains[0] for val in values: for possibility in all_possibilities(domains[1:]): possibility[key] = val yield possibility #print(list(all_possibilities([('year', [2013, 2024]), ('month', ['Jan', 'Dec'])]))) def constant(v): return Column((), {(): v}) # In Codd 01971, this is a table of part quantities required from # suppliers “s”, part number “p”, for project “pj”. It happens that # in this version of the table either (s, p) or (p, pj) are unique # keys, but the idea is that this is not necessarily the case. quantity = Column(('s', 'p', 'pj'), { (1, 2, 5): 17, (1, 3, 5): 23, (2, 3, 7): 9, (2, 7, 5): 4, (4, 1, 1): 12, }) # In such a case you might have a price list. price = Column(('s', 'p'), { (1, 2): 37.99, (1, 3): 22.50, (2, 3): 53.53, #(4, 1): 69.17, your price list might be incomplete (4, 2): 39.00, # and might include prices for items you aren’t ordering }) if __name__ == '__main__': print(quantity) # You can, say, add a constant to each value. print(quantity + 53) # Given the price list, you can calculate how much each project # will be spending on each (supplier, part) tuple. print(price) budget = quantity * price print(budget) # And in that case you might also want to know how much each # project is spending in total, across all suppliers and products. print(budget.aggregate(('s', 'p'), sum))