Occasionally, absolutely crazy ideas crop up into my noggin. Recently, I've had two take up residence almost simultaneously, both related to pynads.
Since Pynads is, nominally, a learning exercise for me to understand some concepts in functional programming -- specifically in terms of Haskell -- a little more deeply. I found myself wanting to using Haskell type signatures with some of my functions. The reason why is because I like Haskell's type signatures. They stay out of the way of my actual function signatures.
Here's the current way Python 3 annotates functions:
In [1]:
def my_func(a: int, b: str = 'hello') -> tuple:
return (a, b)
my_func(1, 'wut')
Out[1]:
That's so much line noise. Like what. Look at that default assignment. Like, I get why the annotations are inlined with the signature. But they're just ugly. Meanwhile, here's a similar Haskell function with the type signature:
myFunc :: Int -> String -> (Int, String)
myFunc a b = (a, b)
That type signature is both helpful AND out of the way. However, there's one really nice thing that Python does with these annotations:
In [2]:
my_func.__annotations__
Out[2]:
Nice. We retain that information in a dictionary out of the way. What if we could combine these two things?
In [3]:
from pynads.utils.decorators import annotate
@annotate(type="Int -> String -> (Int, String)")
def my_func(a, b='hello'):
return (a, b)
I like this. It stays out of the way, it uses a decorator (never enough decorators). Let's checkout the __annotations__
dict:
In [4]:
my_func.__annotations__
Out[4]:
Uh....Well, it didn't fill anything in. What did it do? Well, it attaches the signature to the docstring...
In [5]:
print(my_func.__doc__)
That's...nice. I'm actually perfectly content with this solution currently. But wouldn't it be cool? (this is a phrase that only preceeds michief and trouble). Wouldn't it be cool if that Haskell type was somehow transformed into a Python annotations dictionary and on the other end we'd be able to inspect the annotation and get this:
In [6]:
{'a': 'Int',
'b': 'String',
'returns': '(Int, String)'
}
Out[6]:
However, this is complicated because what if we had a higher order function? The Haskell type signature looks like this:
map :: (a -> b) -> [a] -> [b]
"Take a function of type a to type b, a list of as and return a list of bs." Simple. How is this parsed? What if we put type class restrictions on the type:
(Monad m) => m a -> (a -> b) -> m b
Where m is an instance of Monad, take a m a
and a function of type a to b and return a m b
.
What if we have multiple typeclass restrictions:
(Monad m, Monoid s) => m s -> m s -> (s -> s -> m s) -> m s
Maybe we're lifting mappend
into a monad? Let's also pretend that this isn't a bad way of doing it as well. How do we parse this?
What about "existentially types", aka. forall a b.
(which is something I've not used, nor understand, but apparently it's useful because reasons).
Of course, there are cons with this:
forall a b. ((a, b) -> (Char, Bool))
?mypy
seems to have gotten the BDFL nod and in fact, seems to be on track for inclusion to Python 3.5. Is it worth taking a second step and translating the parsed type signature to a type system?__annotations__
are use for things that aren't type annotations. Is this good? Is this bad? I don't know. Well, I think they should be for type signatures, but there's some cool stuff that is done with them.Things to think about.
I implemented a rudimentary do-notation esque syntax sourced from Peter Thatcher's post on monads in Python. Here's the thing, it works...except with the List
monad, because we need to repeatedly call the bound function and then flatten the list out. Except with this implementation, it hits mreturn
and the whole thing blows up. Wah wah wah. But I like this style:
In [7]:
from pynads.do import do, mreturn
from pynads import List
@do(monad=List)
def chessboard(ranks, files):
r = yield List(*ranks)
f = yield List(*files)
mreturn((r,f))
#chessboard('abcdefgh', range(1,9))
Oops. But this works:
In [8]:
def chessboard(ranks, files):
return List(*ranks) >> (lambda r:
List(*files) >> (lambda f:
List.unit((r,f)) ))
chessboard('abcdefgh', range(1,9))
Out[8]:
But compare to Haskell:
ranks = ['a'..'h']
files = [1..8]
chessboard :: [(Char, Integer)]
chessboard = do
r <- ranks
f <- files
return (r,f)
It just works. Obviously, something in Pynads is implemented poorly. Wah wah wah. But what if do
did something different? What if it took a look at that function and said, "No no no, this is all wrong...let me...let me just rewrite it for you." And instead of actually using:
def chessboard(ranks, files):
r = yield List(*ranks)
f = yield List(*files)
mreturn((r,f))
We actually end up using:
def chesboard(ranks, files):
return List(*ranks) >> (lambda r:
List(*files) >> (lambda f:
List.unit((r,f)) ))
And just rewrite the function with AST and compile it and use the new function instead. I mean if Lisp can be implemented in Python by parsing Lisp into Python AST, surely it's (relatively, being the keyword) simple to take a function with a bunch of yields and rewrite it into a function with a bunch of lambdas.
Besides, wouldn't it be cool? ;)
Of course both of these are nuclear options. Well, the parser less so than rewriting AST. But, I know if I become a professional developer, at some point I'll need to have a passable understanding of parsers and ASTs (they're not even that unrelated) so instead of breaking something someone's using, why not break something I'll be the only one cursing?