'''Flatten a nested list'''

from itertools import (chain)


# flatten :: NestedList a -> [a]
def flatten(x):
    '''A list of atomic values resulting from fully flattening
       an arbitrarily nested list.'''
    return concatMap(flatten)(x) if isinstance(x, list) else [x]


def main():
    '''Test: flatten an arbitrarily nested list.'''
    print(
        fTable(__doc__ + ':')(showList)(showList)(
            flatten
        )([
            [[[]]],
            [[1, 2, 3]],
            [[1], [[2]], [[[3, 4]]]],
            [[1], 2, [[3, 4], 5], [[[]]], [[[6]]], 7, 8, []]
        ])
    )


# GENERIC -------------------------------------------------

# compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
def compose(g):
    '''Right to left function composition.'''
    return lambda f: lambda x: g(f(x))


# concatMap :: (a -> [b]) -> [a] -> [b]
def concatMap(f):
    '''A concatenated list over which a function has been mapped.
       The list monad can be derived by using a function f which
       wraps its output in a list,
       (using an empty list to represent computational failure).'''
    return lambda xs: list(
        chain.from_iterable(map(f, xs))
    )


# fTable :: String -> (a -> String) ->
#                     (b -> String) ->
#        (a -> b) -> [a] -> String
def fTable(s):
    '''Heading -> x display function ->
                 fx display function ->
          f -> value list -> tabular string.'''
    def go(xShow, fxShow, f, xs):
        w = max(map(compose(len)(xShow), xs))
        return s + '\n' + '\n'.join([
            xShow(x).rjust(w, ' ') + (' -> ') + fxShow(f(x))
            for x in xs
        ])
    return lambda xShow: lambda fxShow: lambda f: lambda xs: go(
        xShow, fxShow, f, xs
    )


# showList :: [a] -> String
def showList(xs):
    '''Stringification of a list.'''
    return '[' + ','.join(str(x) for x in xs) + ']'


if __name__ == '__main__':
    main()
