-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
an old attempt at implementing pure functionality. commiting for backup.
- Loading branch information
Showing
2 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
import copy | ||
import inspect | ||
import queue | ||
import app | ||
import collections | ||
|
||
import functools | ||
import turtle | ||
import enum | ||
import typing | ||
|
||
# We want a clean functional python code, even though python is not made for this... | ||
# => Efficiency will be secondary to clarity. | ||
# Note : Terseness is another thing altogether and is a non-goal, | ||
# because the final aim is to generate code on the fly. | ||
# => Highest Priority : Code Semantic Clarity (given Category Theory and Map Theory knowledge). | ||
|
||
|
||
#TODO : single dispatch on first argument (th only one in our design...) | ||
|
||
class Map(collections.OrderedDict): | ||
""" | ||
Callable Iterator as a curried function with iterable arguments, supporting multi call sequence. | ||
Allows decoupling calling a function and receiving a result, while keeping most of python function call capabilities. | ||
BEWARE: Map is mutable, so that future computation is not done again if it was already done in the past. | ||
""" | ||
|
||
def __init__(self, f, existing=None): | ||
# TODO : maxsize + overflow/underflow exceptions | ||
self._calls = app.QueueGroup() # TODO: call queue or call stack ?? | ||
self._inner = f | ||
super().__init__(existing or {}) | ||
|
||
def __next__(self): | ||
""" | ||
Iterator has a different meaning here : We want result of next() application | ||
:return: | ||
""" | ||
if not callable(self._inner): | ||
return self._inner | ||
elif not inspect.signature(self._inner).parameters: | ||
return self._inner() # no need to wait for call before getting result. | ||
else: | ||
# retrieve call from queue (apply by need) | ||
# But one at a time | ||
needed = self._calls.get_nowait() | ||
for a in needed: | ||
assert callable(needed[a]) | ||
self[a] = needed[a]() # TODO: What if over max dict size ? | ||
self._calls.task_done() | ||
|
||
return self | ||
|
||
def __lt__(self, other): | ||
""" | ||
One Map is less than another iff one map is less than another, or number of calls is less than another. | ||
CHecking in that order is important, the presence in the map means more actuation has been done. | ||
:param other: | ||
:return: | ||
""" | ||
return super().__lt__(other) or self._calls.unfinished_tasks < other._calls.unfinished_tasks | ||
|
||
def __eq__(self, other): | ||
""" | ||
Strict equality : must be same mapping content, same queued calls. | ||
Note the intrinsic equality of function is a matter of implementation and should not matter at this level (Yoneda Lemma) | ||
:param other: | ||
:return: | ||
""" | ||
return super().__eq__(other) and self._calls.unfinished_tasks == other._calls.unfinished_tasks # and self._inner == other._inner | ||
|
||
def __ne__(self, other): | ||
return not self.__eq__(other) | ||
|
||
def __le__(self, other): | ||
return self.__eq__(other) or self.__lt__(other) | ||
|
||
def __gt__(self, other): | ||
return not self.__le__(other) | ||
|
||
def __ge__(self, other): | ||
return self.__eq__(other) or self.__gt__(other) | ||
|
||
def __repr__(self): | ||
"""Consistent repr""" | ||
return super().__repr__() + self._calls.__repr__() | ||
|
||
def __call__(self, *args, **kwargs): | ||
# Note if args are sequential 1-arg calls, kwargs are onetime n-arg calls. | ||
# => enforce unambiguous coding style when user wants to bypass functional iterative call implementation. | ||
|
||
if args or kwargs: | ||
|
||
if callable(self._inner) and inspect.signature(self._inner).parameters: | ||
# Iterate on arguments and construct a mapping, for later application | ||
self._calls.put_nowait( | ||
{a: functools.partial(self._inner, a, **kwargs) for a in args if a not in self} | ||
) # TODO : what if over max queue size ? | ||
return self # for successive curried application | ||
else: | ||
# silently absorb useless args. | ||
return self # for successive curried application | ||
|
||
else: | ||
# TODO : do we do it here, or in the 'background' (potentially another thread later), or on demand? | ||
return next(self) # empty call marks one application and retrieval of result. | ||
|
||
def __copy__(self): | ||
""" | ||
When duplicating the Map, the Map itself must remain the same (_inner is assumed a pure function) | ||
However the call queue must be different as to not have the same Map for ever, resource-wise... | ||
Note this goes against python core functionality, so implementing an Interactive Combinator based paradigm will not look pretty... | ||
Maybe in a specific context (with:) ? | ||
TODO What is the meaning of a shallow copy in this context ? | ||
:return: | ||
""" | ||
|
||
# we do not want to keep the same reference on the super class map, only the existing content | ||
other = Map(self._inner, existing=self) | ||
# other._calls = self._calls # drop current call queue. | ||
return other | ||
|
||
def __deepcopy__(self, memo=None): | ||
""" | ||
When duplicating the Map, the Map itself must remain the same (_inner is assumed a pure function) | ||
However the call queue must be different as to not have the same Map for ever, resource-wise... | ||
Note this goes against python core functionality, so implementing an Interactive Combinator based paradigm will not look pretty... | ||
Maybe in a specific context (with:) ? | ||
:return: | ||
""" | ||
|
||
# we do not want to keep the same reference on the super class map, only the existing content | ||
other = Map(self._inner, existing=self) # TODO : since we copy here the function, using a memoized function should work all the same. | ||
# keep existing calls | ||
other._calls = self._calls.split() | ||
return other | ||
|
||
def __add__(self, other): | ||
""" | ||
We can add maps: merging the map if _inner is the exact same, and puting one queue into another (non commutative) | ||
This gives as a basic lattice of maps (for the same function), by allowing joins (with the copy as meets)... | ||
:param other: | ||
:return: | ||
""" | ||
pass # TODO | ||
|
||
|
||
if __name__ == '__main__': | ||
import doctest | ||
doctest.testmod() | ||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import copy | ||
|
||
import pytest | ||
import fun | ||
|
||
|
||
#@pytest.mark.skip | ||
def test_const(): | ||
|
||
c = fun.Map(42) | ||
|
||
assert c() == 42 | ||
|
||
|
||
#@pytest.mark.skip | ||
def test_applicable_lambda(): | ||
|
||
a = fun.Map(lambda: 42) | ||
|
||
assert a() == 42 | ||
|
||
|
||
#@pytest.mark.skip | ||
def test_applicable_function(): | ||
|
||
def answer(): | ||
return 42 | ||
|
||
f = fun.Map(answer) | ||
|
||
assert f() == 42 | ||
|
||
|
||
def test_1arg_function(): | ||
|
||
def incr(i: int): | ||
return i + 1 | ||
|
||
i = fun.Map(incr) | ||
|
||
assert i(41)()[41] == 42 | ||
|
||
# Check we can call multiple times: | ||
r = copy.deepcopy(i(42)) | ||
# try calling all at once | ||
s = copy.deepcopy(i(42, 43)) | ||
|
||
assert s == r # (same map, same number of calls) | ||
r = copy.deepcopy(r(43)) | ||
# Note how keeping same object and mutate it, should be explicit via A = A(changes) | ||
# This goes against python basic syntax... | ||
|
||
assert r > s # (same map, more calls - more complex) | ||
|
||
r() # application | ||
assert r != s # since r was applied once more (more resource intensive) | ||
assert r > s # number of apply is more important than number of call. | ||
|
||
s() | ||
assert r == s | ||
|
||
# result are same, differs only in control flow (call/return) | ||
assert r[41] == s[41] == i(41) == 42 | ||
assert r[42] == s[42] == i(42) == 43 | ||
assert r[43] == s[43] == i(43) == 44 | ||
|
||
s() | ||
|
||
# Check it will raise when no result is expected immediately (fapply underflow) | ||
with pytest.raises(StopIteration): | ||
i() | ||
|
||
# Check it will raise when too much call without consumption (fapply overflow) | ||
with pytest.raises(): | ||
i(1)(2)(3)(4)(5)(6)(7)(8)(9)(0) | ||
|
||
|
||
assert i(41)(40) == 4 | ||
|
||
|
||
def test_2arg_function(): | ||
def incr(a: int, b:int): | ||
return a + b | ||
# | ||
# i = fun.G(incr) | ||
# | ||
# assert callable(i) | ||
# assert callable(i(41)) | ||
# | ||
# # Check result is in the iterator | ||
# assert next(i(41)) == 42 | ||
# # check result is retrievable by unit apply | ||
# assert i(41)() == 42 | ||
# | ||
# # Check it will raise when no result is expected immediately (fapply underflow) | ||
# with pytest.raises(StopIteration): | ||
# i() | ||
# | ||
# # Check it will raise when too much call without consumption (fapply overflow) | ||
# with pytest.raises(): | ||
# i(1)(2)(3)(4)(5)(6)(7)(8)(9)(0) | ||
|
||
|
||
|
||
if __name__ == '__main__': | ||
pytest.main(['-s', __file__]) |