/coppertop

Multiple-dispatch, partial functions and pipelines in Python

Primary LanguagePythonApache License 2.0Apache-2.0

coppertop - multiple-dispatch, partial functions and pipeline style for Python

Coppertop provides an alternative programming experience in Python via the following:

  • multiple-dispatch
  • partial functions
  • piping syntax
  • an embryonic core library of common functions

Installation

pip install coppertop-bones-dm for the dm core library and the @coppertop decorator.
pip install coppertop just for the @coppertop decorator.


Multiple-dispatch

To use multiple-dispatch decorate functions with @coppertop and use different type annotations. Missing annotations are taken as fallback wildcards. Class inheritance is ignored when matching caller and function signatures.

from coppertop.pipe import *

@coppertop
def addOne(x:int) -> int:
    return x + 1
    
@coppertop
def addOne(x:str) -> str:
    return x + 'One'
    
@coppertop
def addOne(x):                 # fallback
    if isinstance(x, list):
        return x + [1]
    else:
        raise NotYetImplemented()

assert addOne(1) == 2
assert addOne('Three Two ') == 'Three Two One'
assert addOne([0]) == [0, 1]

Partial (application of) functions

syntax: f(_, a) -> f(_)
where _ is used as a sentinel place-holder for arguments yet to be confirmed (TBC)

We create partials of @coppertop decorated functions when we use _ indicating deferred arguments. For example:

@coppertop
def appendStr(x, y):
    assert isinstance(x, str) and isinstance(y, str)
    return x + y

appendWorld = appendStr(_, " world!")           # first argument is deferred

assert appendWorld("hello") == "hello world!"

Piping syntax

The @coppertop function decorator also extends functions with the >> operator and so allows code to be written in a more essay style format - i.e. left-to-right and top-to-bottom. The idea is to make it easier to express program syntax (aka sequence).


unary style - takes 1 piped argument and 0+ called arguments

syntax: A >> f(args) -> f(args)(A)

from coppertop.pipe import *

@coppertop(style=unary)
def addOne(x):
    return x + 1

1 >> addOne
"hello" >> appendStr(_," ") >> appendStr(_, "world!")

1 >> partial(lambda x: x +1)

binary style - takes 2 piped arguments and 0+ called arguments

syntax: A >> f(args) >> B -> f(args)(A, B)

from bones.core.errors import NotYetImplemented
import dm.core
from groot import collect, inject

@coppertop(style=binary)
def add(x, y):
    return x + y

@coppertop(style=binary)
def op(x, action, y):
    if action == "+":
        return x + y
    else:
        raise NotYetImplemented()

1 >> add >> 1
1 >> op(_,"+",_) >> 1
[1,2] >> collect >> (lambda x: x + 1)
[1,2,3] >> inject(_,0,_) >> (lambda x,y: x + y)

ternary style - takes 3 piped arguments and 0+ called arguments

syntax: A >> f(args) >> B >> C -> f(args)(A, B, C)

from groot import both, check, equals

actual = [1,2] >> both >> (lambda x, y: x + y) >> [3,4]
assert (1 >> equal >> 1) == True
actual >> check >> equal >> [4, 6]

Example - Cluedo notepad

See algos.py, where we track a game of Cluedo and infer who did it. See ex_games.py for example game input.


a whimsical exercise for the ambitious

(both, collect, inject, addOne, appendStr, check, equals are all illustrated above)

from groot import to
[1,2] >> both >> (lambda x, y: x + y) >> [3,4] 
   >> collect >> (lambda x: x * 2)
   >> inject(_,1,_) >> (lambda x,y: x * y)
   >> addOne >> addOne >> addOne
   >> to >> str >> appendStr(_," red balloons go by")
   >> check >> equal >> ???

Appendix - comparison of unary piping with other languages

Python (using the @coppertop decorator)

@coppertop     # default is unary
def unaryAddOne(x):
  return x + 1

@coppertop
def unaryAdd2Args(x, y):
  return x + y

@coppertop
def unaryAdd3Args(x, y, z):
  return x + y + z

unaryAddOne(1)
1 >> unaryAddOne

unaryAdd2Args(1,2)
1 >> unaryAdd2Args(_,2)
2 >> unaryAdd2Args(1,_)

unaryAdd3Args(1,2,3)
1 >> unaryAdd3Args(_,2,3)
2 >> unaryAdd3Args(1,_,3)
3 >> unaryAdd3Args(1,2,_)

R - TBD

# with magrittr

q / kdb (an APL derivative)

unaryAddOne: {x + 1}
unaryAdd2args: {x + y}
unaryAdd3Args: {x + y + z}

unaryAddOne[1]
unaryAddOne 1

unaryAdd2Args[1;2]
unaryAdd2Args[;2] 1
unaryAdd2Args[1;] 2

unaryAdd3Args[1;2;3]
unaryAdd3Args[;2;3] 1
unaryAdd3Args[1;;3] 2
unaryAdd3Args[1;2;] 3

F# / OCaml

unaryAddOne x = x + 1
unaryAdd2Args x y = x + y
unaryAdd3Args x y z = x + y + z

unaryAddOne 1
1 |> unaryAddOne

unaryAdd2Args 1 2
2 |> unaryAdd2Args 1

unaryAdd3Args 1 2 3
3 |> unaryAdd3Args 1 2

Smalltalk

unaryAddOne
    ^ self + 1

unaryAdd2Args: y
    ^ self + y

unaryAdd3Args: y with: z
    ^ self + y + z

1 addOne
1 unaryAdd2Args: 2
1 unaryAdd3Args: 2 with: 3

bones (influenced by q/kdb and Smalltalk)

unaryAddOne: {x + 1}
unaryAdd2Args: {x + y}
unaryAdd3Args: {x + y + z}

unaryAddOne(1)
1 unaryAddOne

unaryAdd2Args(1,2)
1 unaryAdd2Args(,2)
2 unaryAdd2Args(1,)

unaryAdd3Args(1,2,3)
1 unaryAdd2Args(,2,3)
2 unaryAdd2Args(1,,3)
3 unaryAdd2Args(1,2,)