#!/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'| {htmlescape(key)}')
for keys, val in self.values.items():
rv.append(' |
')
for key in keys:
rv.append(f'| {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))