docs: 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