planfn(val1=fn1, val2=fn2...) uses the pimms plan mechanism to yield a function f that produces
      an immutable imap when called with the correct paramters. Unlike in plan(), planfn() does not
      care about the names of the calculation units; instead the val1, val2, etc. are names of the
      efferent values while the given fn objects are the functions that calculate them (they are
      automatically converted into calc objects, and names are automatically generated for the items
      required).
    planfn(res, val1=fn1, val2=fn2...) yields a function f that is equivalent to the function
      lambda *args,**kw: g(*args,**kw)[res] where g = planfn(val1=fn1, val2=fn2...).
    planfn((p1, p2...), val1=fn1, val2=fn2...) yields a function f that is equivalent to the
      function lambda *a: g({p1:a[0], p2:a[1]...}) where g = planfn(val1=fn1, val2=fn2...).
    planfn((p1, p2...), res, val1=fn1, val2=fn2...) applies both of the above translations to the
      resulting function; note that either the first or second argument may be None to specify
      default behavior. Additionally, the first argument may be a string s equivalent to (s,).

def planfn(*args, **kwargs):
    '''
    planfn(val1=fn1, val2=fn2...) uses the pimms plan mechanism to yield a function f that produces
      an immutable imap when called with the correct paramters. Unlike in plan(), planfn() does not
      care about the names of the calculation units; instead the val1, val2, etc. are names of the
      efferent values while the given fn objects are the functions that calculate them (they are
      automatically converted into calc objects, and names are automatically generated for the items
      required).
    planfn(res, val1=fn1, val2=fn2...) yields a function f that is equivalent to the function
      lambda *args,**kw: g(*args,**kw)[res] where g = planfn(val1=fn1, val2=fn2...).
    planfn((p1, p2...), val1=fn1, val2=fn2...) yields a function f that is equivalent to the
      function lambda *a: g({p1:a[0], p2:a[1]...}) where g = planfn(val1=fn1, val2=fn2...).
    planfn((p1, p2...), res, val1=fn1, val2=fn2...) applies both of the above translations to the
      resulting function; note that either the first or second argument may be None to specify
      default behavior. Additionally, the first argument may be a string s equivalent to (s,).
    '''
    # first: parse options
    if   len(args) > 2:           raise ValueError('no more than 2 non-keyword arguments allowed')
    elif len(args) == 0:          (params,result) = (None,None)
    elif len(args) == 2:          (params,result) = args
    elif is_str(args[0]):         (params,result) = (None,args[0])
    elif is_vector(args[0], str): (params,result) = (args[0],None)
    else:                         raise ValueError('cannot interpret argument: %s' % args[0])
    # okay, now, we make the calcs and the calc plan:
    calcs = {}
    for (k,v) in six.iteritems(kwargs):
        # if v is already a calc object, we can only use this if it has exactly one efferent
        if is_calc(v):
            if len(v.efferents) != 1:
                raise ValueError('calc objects given to planfn must have exactly 1 efferent')
            eff = v.efferents[0]
            if eff != k: v = v.tr({eff:k})
        else: v = calc(k)(v) # otherwise convert it to a calc with the output we want
        # it's possible that the names of the calc objects are not unique, so we want to auto-
        # generate unique names based on their names...
        calcs['calc_' + k] = v
    p0 = plan(calcs)
    # see if we need to fix the input
    if params is None:           p = p0
    elif is_str(params):         p = lambda arg: p0({params:arg})
    elif is_vector(params, str): p = lambda *args: p0({k:v for (k,v) in zip(params,args)})
    else: raise ValueError('params must be a string or list of strings or None')
    # and if we need to fix the output
    if result is None: return p
    def f(*args, **kw): return p(*args, **kw)[result]
    return f