#!/usr/bin/env python """ A Python syntax hack to write functional-lazy code like an assembly line. Also much more convenient in the CLI because you needn't leap back & forth to add pairs of brackets. Most callables here expect an iterable on the LHS & result in an iterator. Pipe names starting with 't' (for 'terminate') result in something not iterable, or nothing. Conventional syntax is odd (Lisp, Python, C, ...) because the functions called later must precede those called earlier (ignoring what you might do with Lisp read macros), you have to say what comes last 1st, like saying: 'eat a sandwich; make a sandwich; buy groceries.' the words are spelled left to right but the sentence is right to left. This isn't really more brief, it just lets sequences of operations be written naturally from left to right, wo/distant brackets | is bitwise OR for numbers & union for sets, but is otherwise hardly used & recalls UNIX pipes; this would be much easier with Lisp read-macros Lazy-functional code is an assembly line; the syntax should make that clear. -- But I really don't use this all that much. It's neat, but doesn't seem to be worth the trouble. Would be fun at the CLI though. This is really so much clearer since otherwise one ends up using a strange combination of syntaxes that is much less clear: all(x[0].isupper() for x in ichain(w.words for w in nltk.wordnet.N[w]))) vs nltk.wordnet.N[w] | pattr('words') | pchain | pitem(0) | pmethod('isupper') | tall pchainsort = pchain | psort Related: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/276960 http://en.wikipedia.org/wiki/Dataflow_programming """ __author__ = 'Patrick Roberts' __copyright__ = 'Copyright 2008 Patrick Roberts' from itertools import * from functools import * import operator, sys, time def group(x, keyfunc=None): return dict((k, list(v)) for k, v in groupby(sorted(x, key=keyfunc), keyfunc)) def ichain(seq): for s in seq: for t in s: yield t disjoin = lambda *predicates: lambda *args: any(f(*args) for f in predicates) _any = any _all = all def any(seq, test=bool): """Return True if test_func returns a non-zero result for an item in the given sequence. >>> any([]) False >>> any([1, 2, 3]) True >>> any([1, 2], lambda x: x > 1) True >>> any([1, 2], lambda x: x > 2) False """ return _any(imap(test, seq)) # __builtins__.any -- __builtins__ is annoying because it's a module when this is run alone, but it's a dict when this is imported def all(seq, test=bool): """Return True if test_func returns a non-zero result for every item in the given sequence. >>> all([]) True >>> all([0, 1, 2]) False >>> all([0, None], operator.__not__) True """ return _all(imap(test, seq)) # __builtins__.all def none(seq, test=bool): """Return True if the predicate returned False for every element in the sequence. >>> none([]) True >>> none([0, 1]) False >>> none([0, False]) True >>> none([0, 1], operator.__not__) False """ return not any(seq, test) def mapc(func, seq):#*seqs): """Works just like the Lisp operator of the same name.""" #if len(seqs) == 1: #seq = seqs[0] for e in seq: func(e) return seq # wouldn't work with iterators!!! class Pipe: """""" def __init__(self, f): self.f = f def __ror__(self, o): if isinstance(o, self.__class__): # allows pipes to be assembled without evaluating them return Pipe(lambda x: self.f(o.f(x))) else: return self.f(o) #def __repr__(self): # """For the Python CLI.""" -- doesn't work cuz '_' seems to already refer to self # return str(self.f(_)) class SidePipe(Pipe): """performs some operation for a side effect, while returning a copy of the source iterator""" def __ror__(self, iter): seq = list(iter) self.f(seq) return seq class DefaultFunction: # another syntax hack def __init__(self, f): self.f = f def __call__(self, *args, **kwargs): return self.f(*args, **kwargs) def __ror__(self, o): return o | self.f() def pmap(f, *args, **kwargs): # accept a sequence of map functions? to replace ipipe? return Pipe(partial(imap, partial(f, *args, **kwargs))) # there's pattern in these too!! def pmapc(f, *args, **kwargs): return Pipe(partial(mapc, partial(f, *args, **kwargs))) #def pfilter(f=None, *args, **kwargs): # ? allow a pipe as an argument # return Pipe(partial(ifilter, partial(f, *args, **kwargs) if f else None)) pfilter = DefaultFunction(lambda f=None, *args, **kwargs: Pipe(partial(ifilter, partial(f, *args, **kwargs) if f else None))) def pnfilter(f=None, *args, **kwargs): # - dislike the name; pnfilter? pfilterfalse return Pipe(partial(ifilterfalse, partial(f, *args, **kwargs) if f else None)) def pitem(k, *args): assert len(args) <= 1 # or have pitemd(k, default) and pitem(*args) could dig into nested dicts return pmap(lambda o, d=args[0]: o.get(k, d)) if args else pmap(operator.itemgetter(k)) def pattr(k, *args): assert len(args) <= 1 return pmap(lambda o, d=args[0]: getattr(o, k, d)) if args else pmap(operator.attrgetter(k)) def psetattr(k, v): """Called for side effect, so isn't done lazily.""" return SidePipe(lambda seq: [setattr(o, k, v) for o in seq]) # setattr takes no ky args, so partial() won't work def parenot(o): return pfilter(partial(operator.is_not, o)) def methodcaller(name, *args, **kwargs): """Like attrgetter, but calls methods.""" return lambda o: getattr(o, name)(*args, **kwargs) def pmethod(name, *args, **kwargs): # could have side effect so don't do lazily? maybe distinguish return pmap(methodcaller(name, *args, **kwargs)) #return Pipe(lambda iter: [getattr(o, name)(*args, **kwargs) for o in iter]) def pinstances(c): return pfilter(lambda o: isinstance(o, c)) def ptee(*pipes): """Splits a duplicate pipe off to the side.""" return SidePipe(lambda seq: [seq | pipe for pipe in pipes]) # a generator can't be used because this is for side-effect #psplit = DefaultFunction(lambda f=None, *args, **kwargs: SidePipe(partial(ifilter, partial(f, *args, **kwargs) if f else None))) # -- not done -- or maybe this is the way pfilter should work, with an optional pipe arg to catch the elements that don't match, though that probably couldn't be very lazy anymore #def psort(key=None, reverse=False): # return Pipe(partial(sorted, key=key, reverse=reverse)) psort = DefaultFunction(lambda key=None, reverse=False: Pipe(partial(sorted, key=key, reverse=reverse))) pchain = Pipe(ichain) plist = Pipe(list) ptuple = Pipe(tuple) punique = pset = Pipe(set) pnot = pmap(operator.not_) tnull = Pipe(lambda o: None) def tmax(key=None): return Pipe(max if key is None else partial(max, key=key)) def tmin(key=None): return Pipe(min if key is None else partial(min, key=key)) def tjoin(s): return Pipe(s.join) thelp = Pipe(help) # not really a pipe since it doesn't accept an iterator tprint = Pipe(lambda iter: [sys.stdout.write('%s\n' % o) for o in iter] and None) tsum = Pipe(sum) tlen = Pipe(len) pdir = Pipe(dir) tall = DefaultFunction(lambda test=bool: Pipe(partial(all, test=test))) tany = DefaultFunction(lambda test=bool: Pipe(partial(any, test=test))) tnone = DefaultFunction(lambda test=bool: Pipe(partial(none, test=test))) def trace(*args, **kwargs): print args, kwargs ptrace = SidePipe(lambda seq: sys.stdout.write('%r\n' % seq)) # doesn't work right with ptimer!!! because of SidePipe waiting for all results; this would be useless for any long slow pipe; maybe make SidePipe smarter then; why does this use SidePipe anyway? pmap(trace) doesn what I want except for the formatting def _timer(seconds=1, limit=None): """Yields every s seconds.""" i = 0 while limit is None or i < limit: yield i i += 1 time.sleep(seconds) ptimer = DefaultFunction(_timer) # inspired by timer() in the WaveScript language # DefaultFunction isn't working here for some reason, maybe because it's on the LHS!!! #def plimit(max=1): # """Yield if __name__ == '__main__': 0 # - run tests