def normalize(data):
    '''
    normalize(obj) yields a JSON-friendly normalized description of the given object. If the data
      cannot be normalized an error is raised.

    Any object that implements a normalize() function can be normalized, so long as the mapping 
    object returned by normalize() itself can be normalized. Note that the normalize() function
    must return a mapping object.

    Objects that can be represented as themselves are returned as themselves. Any other object will
    be represented as a map that includes the reserved key '__type__' which will map to a
    2-element list [module_name, class_name]; upon denomrlization, the module and class k are looked
    up and k.denomalize(data) is called.
    '''
    if data is None: return None
    elif pimms.is_array(data, 'complex') and not pimms.is_array(data, 'real'):
        # any complex number must be handled specially:
        return {normalize_type_key: [None, 'complex'], 're':np.real(data), 'im': np.imag(data)}
    elif is_set(data):
        # sets also have a special type:
        return {normalize_type_key: [None, 'set'], 'elements': normalize(list(data))}
    elif pimms.is_scalar(data, ('number', 'string', 'unicode', 'bool')):
        # scalars are already normalized
        return data
    elif sps.issparse(data):
        # sparse matrices always get encoded as if they were csr_matrices (for now)
        (i,j,v) = sps.find(data)
        return {normalize_type_key: [None, 'sparse_matrix'],
                'rows':i.tolist(), 'cols':j.tolist(), 'vals': v.tolist(),
                'shape':data.shape}
    elif pimms.is_map(data):
        newdict = {}
        for (k,v) in six.iteritems(data):
            if not pimms.is_str(k):
                raise ValueError('Only maps with strings for keys can be normalized')
            newdict[k] = normalize(v)
        return newdict
    elif pimms.is_array(data, ('number', 'string', 'unicode', 'bool')):
        # numpy arrays just get turned into lists
        return data.tolist() if pimms.is_nparray(data) else data
    elif data is Ellipsis:
        return {normalize_type_key: [None, 'ellipsis']}
    elif pimms.is_scalar(data):
        # we have an object of some type we don't really recognize
        try:    m = data.normalize()
        except Exception: raise ValueError('Failed to run obj.normalize() on unrecognized obj: %s' % data)
        if not pimms.is_map(m): raise ValueError('obj.normalize() returned non-map; obj: %s' % data)
        m = dict(m)
        tt = type(data)
        m[normalize_type_key] = [tt.__module__, tt.__name__]
        return m
    else:
        # we have an array/list of some kind that isn't a number, string, or boolean
        return [normalize(x) for x in data]
normalize(obj) yields a JSON-friendly normalized description of the given object. If the data
      cannot be normalized an error is raised.

    Any object that implements a normalize() function can be normalized, so long as the mapping 
    object returned by normalize() itself can be normalized. Note that the normalize() function
    must return a mapping object.

    Objects that can be represented as themselves are returned as themselves. Any other object will
    be represented as a map that includes the reserved key '__type__' which will map to a
    2-element list [module_name, class_name]; upon denomrlization, the module and class k are looked
    up and k.denomalize(data) is called.