docs: def _outer_join(left, right, left_key_fn, right_key_fn, join_fn=union_join):
      """
      :param left: left iterable to be joined
      :param right: right iterable to be joined
      :param function left_key_fn: function that produces hashable value from left objects
      :param function right_key_fn: function that produces hashable value from right objects
      :param join_fn: function called on joined left and right iterable items to complete join
      :rtype: list
      """
      left_joiner = defaultdict(list)
      for ele in left:
          left_joiner[left_key_fn(ele)].append(ele)
      right_joiner = defaultdict(list)
      for ele in right:
          right_joiner[right_key_fn(ele)].append(ele)
      keys = set(left_joiner.keys()).union(set(right_joiner.keys()))
  
      def iter_join(l, r, join_keys):
          for join_key in join_keys:
              for ele in l.get(join_key, [None]):
                  for other in r.get(join_key, [None]):
                      yield join_fn(ele, other)
  
      return list(iter_join(left_joiner, right_joiner, keys))