Partial application and piping with ... and @



In newer versions of Python we have two not much used features: ellipsis:

>>> print (...) Ellipsis

And matrix multiplication operator:

class Dummy ( str ): def __matmul__ ( self , other ): print ( '{}@{}' . format ( self , other )) >>> Dummy ( 'ok' ) @ 'there' ok @ there

So let’s start with ... , in Scala we can partially apply (or curry) function with _ :

def add ( x : Int , y : Int ) = x + y val addOne = add ( 1 , _: Int ) addOne ( 5 ) 6 : Int

Wouldn’t it be nice to have similar option in Python, like:

def add ( x , y ): return x + y addFive = add (..., 5 )

And we can easily implement it with some decorator:

from functool import wraps class Partial : def __init__ ( self , fn , args , kwargs ): self . _fn = fn self . _args = args self . _kwargs = kwargs def __call__ ( self , replacement ): args = [ replacement if arg is ... else arg for arg in self . _args ] kwargs = { key : replacement if val is ... else val for key , val in self . _kwargs . items ()} return self . _fn ( * args , ** kwargs ) def __repr__ ( self ): return '<Partial: {}(*{}, **{})>' . format ( self . _fn . __name__ , repr ( self . _args ), repr ( self . _kwargs )) def ellipsis_partial ( fn ): @ wraps ( fn ) def wrapper ( * args , ** kwargs ): ellipsises = ( list ( args ) + list ( kwargs . values ())). count (...) if ellipsises > 1 : raise TypeError ( 'Only one ... allowed as an argument.' ) elif ellipsises : return Partial ( fn , args , kwargs ) else : return fn ( * args , ** kwargs ) return wrapper

So here if we find ... in arguments, we return Partial object. And when the object called – we replace ... with passed value. In action:

@ ellipsis_partial def add ( x , y ): return x + y addFive = add ( 5 , ...) >>> addFive ( 10 ) 15

And it works! So back to matrix multiplication operator. In F# there’s nice piping operators:

> [ 1 .. 10 ] |> List . filter ( fun x -> x % 3 = 0 ) val it : int list = [ 3 ; 6 ; 9 ]

So with our operator in Python it should look like:

range ( 1 , 10 ) @ filter ( lambda x : x % 3 == 0 , ...)

And we can easily implement it just by adding __rmatmul__ to Partial :

class Partial : def __init__ ( self , fn , args , kwargs ): self . _fn = fn self . _args = args self . _kwargs = kwargs def __call__ ( self , replacement ): args = [ replacement if arg is ... else arg for arg in self . _args ] kwargs = { key : replacement if val is ... else val for key , val in self . _kwargs . items ()} return self . _fn ( * args , ** kwargs ) def __rmatmul__ ( self , replacement ): return self ( replacement ) def __repr__ ( self ): return '<Partial: {}(*{}, **{})>' . format ( self . _fn . __name__ , repr ( self . _args ), repr ( self . _kwargs ))

And in action:

filter = ellipsis_partial ( filter ) to_list = ellipsis_partial ( list ) >>> range ( 1 , 10 ) @ filter ( lambda x : x % 3 == 0 , ...) @ to_list (...) [ 3 , 6 , 9 ]

And we can use it in even more complex cases:

map = ellipsis_partial ( map ) join = ellipsis_partial ( str . join ) >>> range ( 1 , 10 ) @ map ( lambda x : x + 4 , ...) \ @ filter ( lambda x : x % 3 == 0 , ...) \ @ map ( str , ...) \ @ join ( ', ' , ...) 6 , 9 , 12

But it’s a bit not nice to wrap all callables in ellipsis_partial , we can use some hacks with inspect or module loaders to doing it automatically, but it’s too magic for me. So we can add little function that wrap and call:

def _ ( fn , * args , ** kwargs ): return ellipsis_partial ( fn )( * args , ** kwargs )

Usage:

from functools import reduce >>> range ( 1 , 10 ) @ map ( lambda x : x + 4 , ...) \ @ filter ( lambda x : x % 3 == 0 , ...) \ @ _ ( reduce , lambda x , y : x * y , ...) \ @ _ ( 'value: {}' . format , ...) value : 648

However it may look strange and unpythonic, but I guess it would be nice to see something like this in future Python releases.

Gist with sources, next part.