: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

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))