docs: def persist(arg, depth=Ellipsis, on_mutable=None):
      '''
      persist(x) yields a persistent version of x if possible, or yields x itself.
  
      The transformations performed by persist(x) are as follows:
        * If x is an immutable object, yields x.persist()
        * If x is a set, yield a frozenset of of persist(u) for all u in x.
        * If x is a numpy array, yield imm_array(x).
        * If x is a map, yields a persistent version of x with all keys and values replaced with their
          persist()'ed form; note that this respects laziness and itables.
        * If x is a list/tuple type, yields a tuple of persist()'ed contents.
        * Otherwise, if the type of x is not recognized, yields x.
  
      The depth to which persist() searches the argument's elements is controlled by the depth option;
      the default behavior is to persist objects down to the point that a persistent object is found,
      at which its elements are not checked for persistence.
  
      Note that persist() is not guaranteed to recognize a particular object; it is intended as a
      utility function for basic functional-style and immutable data code in Python. In particular,
      it is usefl for pimms's immutable objects; dicts and mappings; pimms's lazy-maps and itables;
      pyrsistent's sets, vectors, and maps; sets and frozensets (which are both communted to
      frozensets); and anything implementing __iter__ (which are commuted to tuples). Objects that are
      not numbers or strings are considered potentially-mutable and will trigger the on_mutable case.
  
      The optional arguments may be passed to persist:
        * depth (default: Ellipsis) specifies the depth to which toe persist() function should search
          when persisting objects. The given argument is considered depth 0, so persist(arg, 0) will
          persist only arg and not its contents, if it is a collection. If None is given, then goes to
          any depth; if Ellipsis is given, then searches until a persistent object is found, but does
          not attempt to persist the elements of already-persistent containers (this is the default).
        * on_mutable (default: None) specifies what to do when a non-persistable object is encountered
          in the search. If None, then the object is left; if 'error', then an error is raised;
          otherwise, this must be a function that is passed the object--the return value of this
          function is the replacement used in the object returned from persist().
      '''
      from .immutable import (is_imm, imm_copy)
      # Parse the on_mutable argument
      if on_mutable is None: on_mutable = lambda x:x
      elif on_mutable == 'error':
          def _raise(x):
              raise ValueError('non-persistable: %s' % x)
          on_mutable = _raise
      if depth in (None, Ellipsis): depth_next = depth
      elif depth < 0: return arg
      else: depth_next = depth - 1
      precur = lambda x:persist(x, depth=depth_next, on_mutable=on_mutable)
      # See if we have an easy type to handle
      if is_imm(arg): return imm_copy(arg)
      if is_quantity(arg):
          (m,u) = (mag(arg), unit(arg))
          mm = precur(m)
          if mm is m: return arg
          else: return quant(mm, u)
      elif isinstance(arg, np.ndarray): return imm_array(arg)
      elif isinstance(arg, np.generic):
          x = type(arg)(arg)
          x.setflags(write=False)
          return x
      elif is_str(arg) or is_number(arg): return arg
      elif isinstance(arg, ps.PVector):
          if depth is Ellipsis or depth == 0: return arg
          for (k,v0) in zip(range(len(arg)), arg):
              v = precur(v0)
              if v0 is not v: arg = arg.set(k,v)
          return arg
      elif isinstance(arg, ps.PSet):
          if depth is Ellipsis or depth == 0: return arg
          for v0 in arg:
              v = precur(v0)
              if v0 is not v: arg = arg.discard(v0).add(v)
          return arg
      elif is_pmap(arg):
          if depth is Ellipsis or depth == 0: return arg
          return key_map(precur, value_map(precur, arg))
      elif is_map(arg):
          if not is_pmap(arg): arg = ps.pmap(arg)
          if depth == 0: return arg
          return key_map(precur, value_map(precur, arg))
      elif isinstance(arg, frozenset):
          if depth is Ellipsis or depth == 0: return frozenset(arg)
          a = [x for x in arg]
          q = [precur(x) for x in a]
          if all(ai is qi for (ai,qi) in zip(a,q)): return arg
          return frozenset(q)
      elif isinstance(arg, set):
          if depth == 0: return frozenset(arg)
          a = [x for x in arg]
          q = [precur(x) for x in a]
          if isinstance(arg, frozenset) and all(ai is qi for (ai,qi) in zip(a,q)): return arg
          return frozenset(q)
      elif hasattr(arg, '__iter__'):
          if depth == 0 or (depth is Ellipsis and isinstance(arg, tuple)): return tuple(arg)
          q = tuple(precur(x) for x in arg)
          if isinstance(arg, tuple) and all(ai is qi for (ai,qi) in zip(arg,q)): return arg
          else: return q
      elif isinstance(arg, (types.FunctionType, partial)):
          return arg
      else: return on_mutable(arg)