def pipe(data, *functions):
    """pipe data trough a pipeline of functions.

    Think of unix pipes or elixir's pipes.
    ``pipe`` assumes that data is the first argument of a function.

    ``pipe`` tries to be smart and automatically cerates partial functions if
    you pass a tuple instead of a callable:

    - ``pipe(data, (pow, 2, 5))`` --> ``pow(data, 2, 5)``.

    - ``pipe(data, (sorted, {"reverse": True}))`` -->
      ``sorted(data, reverse=True)``

    Args:
        data (whatever): the data that is passed into the first function.
        functions (callables): functions that create the pipeline.

    Returns:
        The result of your pipeline.

    Examples:
        ``pipe`` allows you to turn something which is hard to read:

        >>> from math import ceil, sqrt
        >>> sqrt(pow(int(ceil(float("2.1"))), 2))
        3.0

        into something that is easy to read:

        >>> pipe("2.1", float, ceil, int, lambda x: x*x, sqrt)
        3.0

        Genrate your own partial functions with lambda

        >>> pipe("2.1", float, ceil, int, lambda x: pow(x, 2), sqrt)
        3.0

        But there is a shortcut: tuples are interpreted as as
        ``(functions, arguments, ...)``.
        The data passed from the previous function is the first argument of the
        punction, followed by the arguments from the tuple.

        >>> pipe("2.1", float, ceil, int, (pow, 2), sqrt)
        3.0

        To make this clearer ``pipe(3, (pow, 2))`` is equivalent to
        ``pow(3, 2)``.
        Of course you can pass multiple arguments with the tuple:

        >>> pipe(3, (pow, 2, 8))  # pow(2, 3, 8) -> pow(2, 3) % 8
        1

        It can be convenient to use the following notation if the function
        names are longer:

        >>> text = "atababsatsatsastatbadstssdhhhnbb"
        >>> pipe(text,
        ...      set,
        ...      sorted)
        ['a', 'b', 'd', 'h', 'n', 's', 't']

        It's also possible to use named arguments by using a dict:

        >>> pipe(text,
        ...      set,
        ...      (sorted, {"reverse": True}))
        ['t', 's', 'n', 'h', 'd', 'b', 'a']

    """
    for f in functions:
        if isinstance(f, tuple):
            if isinstance(f[1], dict):
                data = f[0](data, **f[1])
            else:
                data = f[0](data, *f[1:])
        else:
            data = f(data)
    return data
