Creates a hash function which will return the same hashable value if 
    passed a set of *args and **kwargs which are equivalent from the 
    perspective of a function call.
    
    That is, the order of keyword arguments, or the fact that an argument 
    without a default was called as a kwarg, will not produce a different hash.
    
    If any arguments provided are not hashable, a TypeError is raised.
    
    *args and **kwargs are supported.

def fn_arg_hash_function(fn):
    """Creates a hash function which will return the same hashable value if 
    passed a set of *args and **kwargs which are equivalent from the 
    perspective of a function call.
    
    That is, the order of keyword arguments, or the fact that an argument 
    without a default was called as a kwarg, will not produce a different hash.
    
    If any arguments provided are not hashable, a TypeError is raised.
    
    *args and **kwargs are supported.
    """
    fn = get_fn(fn)
    n_explicit = fn_available_argcount(fn)
    has_args = fn_has_args(fn)
    has_kwargs = fn_has_kwargs(fn)
    default_kwargs = fn_kwargs(fn)
    for name, value in list(default_kwargs.items()):  
        # store only hashes of values to prevent memory leaks
        try: default_kwargs[name] = hash(value)
        except TypeError: default_kwargs[name] = _unhashable_object
    explicit_kwarg_args = set(default_kwargs.keys())
    n_explicit_kwargs = len(explicit_kwarg_args)
    n_explicit_args = n_explicit - n_explicit_kwargs
    def _hashes(*args, **kwargs):        
        # explicit args
        i = 0
        n_explicit_args_ = min(len(args), n_explicit_args)
        while i < n_explicit_args_:
            #print args[i], 'is an explicit arg.'            
            yield hash(args[i])
    
            i += 1
    
        # explicit kwargs
        for name in explicit_kwarg_args:
            if len(args) > i:
                #print args[i], 'is a kwarg without a default'
                yield hash(args[i])
                i += 1                
            else:
                try:
                    #print kwargs[name], 'is a kwarg taken from kwargs'
                    yield hash(kwargs[name])
                except KeyError:
                    #print default_kwargs[name], 'is a kwarg taken from defaults'
                    yield default_kwargs[name]
    
        # *args
        if has_args:
            #print args[i:], 'is *args'
            yield hash(args[i:])
    
        # **kwargs
        # NOTE: we're treating the kwargs dicts as hashable even though 
        # technically they aren't... be wary if you define **kwargs and then
        # depend on its mutable characteristics.
        if has_kwargs:
            items = frozenset(item for item in list(kwargs.items())
                              if item[0] not in explicit_kwarg_args)
            #print items, 'is **kwargs items'
            yield hash(items)
        
    def hash_(*args, **kwargs):
        return tuple(_hashes(*args, **kwargs))
    
    return hash_