268 lines
6.3 KiB
Python
268 lines
6.3 KiB
Python
'''Generalised multiplication tables'''
|
|
|
|
import collections
|
|
import itertools
|
|
import inspect
|
|
|
|
|
|
# table :: Int -> [[Maybe Int]]
|
|
def table(xs):
|
|
'''An option-type model of a multiplication table:
|
|
a tabulation of Just(x * y) values for all
|
|
pairings (x, y) of integers in xs where x > y,
|
|
and Nothing values where y <= x.
|
|
'''
|
|
axis = fmap(Just)(xs)
|
|
return list(cons(
|
|
cons(Nothing())(axis)
|
|
)(zipWith(cons)(axis)([
|
|
[
|
|
Nothing() if y > x else Just(x * y)
|
|
for x in xs
|
|
]
|
|
for y in xs
|
|
])))
|
|
|
|
|
|
# TEST ----------------------------------------------------
|
|
# main :: IO ()
|
|
def main():
|
|
'''Test'''
|
|
print('\n\n'.join(
|
|
fmap(fmap(fmap(showTable)(table))(
|
|
liftA2(enumFromTo)(fst)(snd)
|
|
))(
|
|
[(13, 20), (1, 12), (95, 100)]
|
|
)
|
|
))
|
|
|
|
|
|
# DISPLAY -------------------------------------------------
|
|
|
|
# showTable :: [[Maybe Int]] -> String
|
|
def showTable(xs):
|
|
'''A stringification of an abstract model
|
|
of a multiplication table.
|
|
'''
|
|
w = 1 + len(str(last(last(xs))['Just']))
|
|
gap = ' ' * w
|
|
rows = fmap(fmap(concat)(
|
|
fmap(maybe(gap)(
|
|
fmap(justifyRight(w)(' '))(str)
|
|
))
|
|
))(xs)
|
|
return unlines([rows[0]] + [''] + rows[1:])
|
|
|
|
|
|
# GENERIC -------------------------------------------------
|
|
|
|
# Just :: a -> Maybe a
|
|
def Just(x):
|
|
'''Constructor for an inhabited Maybe (option type) value.'''
|
|
return {'type': 'Maybe', 'Nothing': False, 'Just': x}
|
|
|
|
|
|
# Nothing :: Maybe a
|
|
def Nothing():
|
|
'''Constructor for an empty Maybe (option type) value.'''
|
|
return {'type': 'Maybe', 'Nothing': True}
|
|
|
|
|
|
# concat :: [[a]] -> [a]
|
|
# concat :: [String] -> String
|
|
def concat(xs):
|
|
'''The concatenation of all the elements
|
|
in a list or iterable.'''
|
|
chain = itertools.chain
|
|
|
|
def f(ys):
|
|
zs = list(chain(*ys))
|
|
return ''.join(zs) if isinstance(ys[0], str) else zs
|
|
|
|
return (
|
|
f(xs) if isinstance(xs, list) else (
|
|
chain.from_iterable(xs)
|
|
)
|
|
) if xs else []
|
|
|
|
|
|
# cons :: a -> [a] -> [a]
|
|
def cons(x):
|
|
'''Construction of a list from x as head,
|
|
and xs as tail.'''
|
|
chain = itertools.chain
|
|
return lambda xs: [x] + xs if (
|
|
isinstance(xs, list)
|
|
) else chain([x], xs)
|
|
|
|
|
|
# curry :: ((a, b) -> c) -> a -> b -> c
|
|
def curry(f):
|
|
'''A curried function derived
|
|
from an uncurried function.'''
|
|
signature = inspect.signature
|
|
if 1 < len(signature(f).parameters):
|
|
return lambda x: lambda y: f(x, y)
|
|
else:
|
|
return f
|
|
|
|
|
|
# enumFromTo :: (Int, Int) -> [Int]
|
|
def enumFromTo(m):
|
|
'''Integer enumeration from m to n.'''
|
|
return lambda n: list(range(m, 1 + n))
|
|
|
|
|
|
# fmap :: Functor f => (a -> b) -> f a -> f b
|
|
def fmap(f):
|
|
'''A function f mapped over a functor.'''
|
|
def go(x):
|
|
defaultdict = collections.defaultdict
|
|
return defaultdict(list, [
|
|
('list', fmapList),
|
|
# ('iter', fmapNext),
|
|
# ('Either', fmapLR),
|
|
# ('Maybe', fmapMay),
|
|
# ('Tree', fmapTree),
|
|
# ('tuple', fmapTuple),
|
|
('function', fmapFn),
|
|
('type', fmapFn)
|
|
])[
|
|
typeName(x)
|
|
](f)(x)
|
|
return lambda v: go(v)
|
|
|
|
|
|
# fmapFn :: (a -> b) -> (r -> a) -> r -> b
|
|
def fmapFn(f):
|
|
'''fmap over a function.
|
|
The composition of f and g.
|
|
'''
|
|
return lambda g: lambda x: f(g(x))
|
|
|
|
|
|
# fmapList :: (a -> b) -> [a] -> [b]
|
|
def fmapList(f):
|
|
'''fmap over a list.
|
|
f lifted to a function over a list.
|
|
'''
|
|
return lambda xs: list(map(f, xs))
|
|
|
|
|
|
# fst :: (a, b) -> a
|
|
def fst(tpl):
|
|
'''First member of a pair.'''
|
|
return tpl[0]
|
|
|
|
|
|
# justifyRight :: Int -> Char -> String -> String
|
|
def justifyRight(n):
|
|
'''A string padded at left to length n,
|
|
using the padding character c.
|
|
'''
|
|
return lambda c: lambda s: s.rjust(n, c)
|
|
|
|
|
|
# last :: [a] -> a
|
|
def last(xs):
|
|
'''The last element of a non-empty list.'''
|
|
return xs[-1]
|
|
|
|
|
|
# liftA2 :: (a -> b -> c) -> f a -> f b -> f c
|
|
def liftA2(f):
|
|
'''Lift a binary function to the type of a.'''
|
|
def go(a, b):
|
|
defaultdict = collections.defaultdict
|
|
return defaultdict(list, [
|
|
# ('list', liftA2List),
|
|
# ('Either', liftA2LR),
|
|
# ('Maybe', liftA2May),
|
|
# ('Tree', liftA2Tree),
|
|
# ('tuple', liftA2Tuple),
|
|
('function', liftA2Fn)
|
|
])[
|
|
typeName(a)
|
|
](f)(a)(b)
|
|
return lambda a: lambda b: go(a, b)
|
|
|
|
|
|
# liftA2Fn :: (a0 -> b -> c) -> (a -> a0) -> (a -> b) -> a -> c
|
|
def liftA2Fn(op):
|
|
'''Lift a binary function to a composition
|
|
over two other functions.
|
|
liftA2 (*) (+ 2) (+ 3) 7 == 90
|
|
'''
|
|
def go(f, g):
|
|
return lambda x: curry(op)(
|
|
f(x)
|
|
)(g(x))
|
|
return lambda f: lambda g: go(f, g)
|
|
|
|
|
|
# maybe :: b -> (a -> b) -> Maybe a -> b
|
|
def maybe(v):
|
|
'''Either the default value v, if m is Nothing,
|
|
or the application of f to x,
|
|
where m is Just(x).
|
|
'''
|
|
return lambda f: lambda m: v if m.get('Nothing') else (
|
|
f(m.get('Just'))
|
|
)
|
|
|
|
|
|
# typeName :: a -> String
|
|
def typeName(x):
|
|
'''Name string for a built-in or user-defined type.
|
|
Selector for type-specific instances
|
|
of polymorphic functions.
|
|
'''
|
|
if isinstance(x, dict):
|
|
return x.get('type') if 'type' in x else 'dict'
|
|
else:
|
|
return 'iter' if hasattr(x, '__next__') else (
|
|
type(x).__name__
|
|
)
|
|
|
|
|
|
# snd :: (a, b) -> b
|
|
def snd(tpl):
|
|
'''Second member of a pair.'''
|
|
return tpl[1]
|
|
|
|
|
|
# uncurry :: (a -> b -> c) -> ((a, b) -> c)
|
|
def uncurry(f):
|
|
'''A function over a pair of arguments,
|
|
derived from a vanilla or curried function.
|
|
'''
|
|
signature = inspect.signature
|
|
if 1 < len(signature(f).parameters):
|
|
return lambda xy: f(*xy)
|
|
else:
|
|
return lambda x, y: f(x)(y)
|
|
|
|
|
|
# unlines :: [String] -> String
|
|
def unlines(xs):
|
|
'''A single string derived by the intercalation
|
|
of a list of strings with the newline character.
|
|
'''
|
|
return '\n'.join(xs)
|
|
|
|
|
|
# zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
|
|
def zipWith(f):
|
|
'''A list constructed by zipping with a
|
|
custom function, rather than with the
|
|
default tuple constructor.
|
|
'''
|
|
return lambda xs: lambda ys: (
|
|
map(uncurry(f), xs, ys)
|
|
)
|
|
|
|
|
|
# MAIN ---
|
|
if __name__ == '__main__':
|
|
main()
|