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