From fb45ae49fe02a4ba091b42c96b3d6c7b055ea509 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 28 Jan 2023 16:59:35 +0300 Subject: [PATCH 1/6] Remove let and Assignments --- README.md | 3 +- docs/reference.md | 1 - docs_src/index/main.py | 3 +- funml/__init__.py | 3 +- funml/assignments.py | 69 ------------------------------ funml/data/monads.py | 26 ++++------- funml/expressions.py | 35 +++++++++++++++ funml/types.py | 59 +++---------------------- tests/test_assignments.py | 90 --------------------------------------- tests/test_expressions.py | 31 ++++++++++++++ tests/test_match.py | 2 +- tests/test_monads.py | 15 ------- 12 files changed, 85 insertions(+), 252 deletions(-) delete mode 100644 funml/assignments.py create mode 100644 funml/expressions.py delete mode 100644 tests/test_assignments.py create mode 100644 tests/test_expressions.py diff --git a/README.md b/README.md index d6a749a..8dbfb45 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,6 @@ def main(): We have some builtin primitive expressions like - ml.val - - ml.let - ml.match - ml.execute - ml.ireduce @@ -183,7 +182,7 @@ def main(): accum_factorial = ml.val(lambda num, accum: ( accum if num <= 0 else accum_factorial(num - 1, num * accum) )) - cube = ml.let(int, power=3) >> superscript + cube = ml.val(lambda v: superscript(v, 3)) factorial = ml.val(lambda x: accum_factorial(x, 1)) get_item_types = ml.ireduce(lambda x, y: f"{type(x)}, {type(y)}") num_type_err = ml.val( diff --git a/docs/reference.md b/docs/reference.md index a1719e0..9f2353d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -9,7 +9,6 @@ Types ::: funml.types options: members: - - Assignment - Expression - MatchExpression - MLType diff --git a/docs_src/index/main.py b/docs_src/index/main.py index 66adc72..60a5ae5 100644 --- a/docs_src/index/main.py +++ b/docs_src/index/main.py @@ -82,7 +82,6 @@ class Color: We have some builtin primitive expressions like - ml.val - - ml.let - ml.match - ml.execute - ml.ireduce @@ -140,7 +139,7 @@ class Color: accum if num <= 0 else accum_factorial(num - 1, num * accum) ) ) - cube = ml.let(int, power=3) >> superscript + cube = ml.val(lambda v: superscript(v, 3)) factorial = ml.val(lambda x: accum_factorial(x, 1)) get_item_types = ml.ireduce(lambda x, y: f"{type(x)}, {type(y)}") num_type_err = ml.val( diff --git a/funml/__init__.py b/funml/__init__.py index 74fe34e..9f32802 100644 --- a/funml/__init__.py +++ b/funml/__init__.py @@ -14,7 +14,7 @@ Pattern matching helps handle both scenarios. """ from .pattern_match import match -from .assignments import let, val +from .expressions import val from .data.enum import Enum from .data.monads import ( Option, @@ -33,7 +33,6 @@ from .pipeline import execute __all__ = [ - "let", "match", "val", "Enum", diff --git a/funml/assignments.py b/funml/assignments.py deleted file mode 100644 index e4ce3f6..0000000 --- a/funml/assignments.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Assigning variables and literals -""" -from typing import Type, Any - -from funml.types import Assignment, to_expn, Expression - - -def let(t: Type, **kwargs: Any) -> Assignment: - """Creates a variable local to any expressions that follow it after piping. - - It creates a variable, initializing it to a given value. That variable - with its given name can then be accessed by any expressions after it in the - given pipe. - - For a given expression to access the value of that variable, one of its argument - must have the same name as that variable. - - Args: - t: the type of the variable - kwargs: *Only ONE* key-word argument whose key is variable name and value is the value of the variable. - - Returns: - An [`Assignment`][funml.types.Assignment] that can be connected to another by piping, thus making the created - variable accessible to the subsequent expression. - - Example: - - ```python - import funml as ml - - expn = ml.let(int, x=9) >> (lambda x: x + 10) >> (lambda y: y * 20) >> str - assert expn() == "380" - ``` - """ - if len(kwargs) == 1: - [(_var, _val)] = kwargs.items() - return Assignment(var=_var, t=t, val=_val) - - raise ValueError(f"kwargs passed should be only 1, got {len(kwargs)}") - - -def val(v: Any) -> Expression: - """Converts a generic value or lambda expression into a functional expression. - - This is useful when one needs to use piping on a non-ml function or - value. It is like the connection that give non-ml values and functions - capabilities to be used in the ml world. - - Args: - v: the value e.g. 90 or function e.g. `min` - - Returns: - an ml [`Expression`][funml.types.Expression] that can be piped to other ml-expressions or invoked to return its - output. - - Example: - - ```python - import funml as ml - - ml_min = ml.val(min) - ml_min_str = ml_min >> str - - expn = ml.val([6, 7, 12]) >> ml_min_str - expn() - # returns '6' - ``` - """ - return to_expn(v) diff --git a/funml/data/monads.py b/funml/data/monads.py index e20ada2..7d33a81 100644 --- a/funml/data/monads.py +++ b/funml/data/monads.py @@ -6,19 +6,17 @@ from .enum import Enum from .. import match -from ..types import Assignment, Expression, to_expn, Operation +from ..types import Expression, to_expn, Operation -def if_ok( - do: Union[Expression, Assignment, Callable, Any], strict: bool = True -) -> Expression: +def if_ok(do: Union[Expression, Callable, Any], strict: bool = True) -> Expression: """Does the given operation if value passed to resulting expression is Result.OK. If the value is Result.ERR, it just returns the Result.ERR without doing anything about it. Args: - do: The expression, function, assignment to run or value to return when Result.OK + do: The expression, function to run or value to return when Result.OK strict: if only Results should be expected Example: @@ -60,16 +58,14 @@ def if_ok( return Expression(Operation(routine)) -def if_err( - do: Union[Expression, Assignment, Callable, Any], strict: bool = True -) -> Expression: +def if_err(do: Union[Expression, Callable, Any], strict: bool = True) -> Expression: """Does the given operation if value passed to resulting expression is Result.ERR. If the value is Result.OK, it just returns the Result.OK without doing anything about it. Args: - do: The expression, function, assignment to run or value to return when Result.ERR + do: The expression, function, to run or value to return when Result.ERR strict: if only Results should be expected Example: @@ -111,16 +107,14 @@ def if_err( return Expression(Operation(routine)) -def if_some( - do: Union[Expression, Assignment, Callable, Any], strict: bool = True -) -> Expression: +def if_some(do: Union[Expression, Callable, Any], strict: bool = True) -> Expression: """Does the given operation if value passed to resulting expression is Option.SOME. If the value is Result.NONE, it just returns the Result.NONE without doing anything about it. Args: - do: The expression, function, assignment to run or value to return when Option.SOME + do: The expression, function, to run or value to return when Option.SOME strict: if only Options should be expected Example: @@ -162,16 +156,14 @@ def if_some( return Expression(Operation(routine)) -def if_none( - do: Union[Expression, Assignment, Callable, Any], strict: bool = True -) -> Expression: +def if_none(do: Union[Expression, Callable, Any], strict: bool = True) -> Expression: """Does the given operation if value passed to resulting expression is Option.NONE. If the value is Option.SOME, it just returns the Option.SOME without doing anything about it. Args: - do: The expression, function, assignment to run or value to return when Option.NONE + do: The expression, function, to run or value to return when Option.NONE strict: if only Options should be expected Example: diff --git a/funml/expressions.py b/funml/expressions.py new file mode 100644 index 0000000..a9be259 --- /dev/null +++ b/funml/expressions.py @@ -0,0 +1,35 @@ +"""Assigning variables and literals +""" +from typing import Type, Any + +from funml.types import to_expn, Expression + + +def val(v: Any) -> Expression: + """Converts a generic value or lambda expression into a functional expression. + + This is useful when one needs to use piping on a non-ml function or + value. It is like the connection that give non-ml values and functions + capabilities to be used in the ml world. + + Args: + v: the value e.g. 90 or function e.g. `min` + + Returns: + an ml [`Expression`][funml.types.Expression] that can be piped to other ml-expressions or invoked to return its + output. + + Example: + + ```python + import funml as ml + + ml_min = ml.val(min) + ml_min_str = ml_min >> str + + expn = ml.val([6, 7, 12]) >> ml_min_str + expn() + # returns '6' + ``` + """ + return to_expn(v) diff --git a/funml/types.py b/funml/types.py index c83dd0b..cb2c020 100644 --- a/funml/types.py +++ b/funml/types.py @@ -1,53 +1,11 @@ """All types used by funml""" from inspect import signature -from typing import Any, Type, Union, Callable, Optional, List, Tuple +from typing import Any, Union, Callable, Optional, List, Tuple from funml import errors from funml.utils import is_equal_or_of_type -class Assignment: - """A variable assignment - - Assigns a given value a variable name and type. It will check that - the data type passed is as expected. It can thus be used to - validate third party data before passing it through the ml-program. - - Args: - var: the variable name - t: the variable type - val: the value stored in the variable - - Raises: - TypeError: `val` passed is not of type `t` - """ - - def __init__(self, var: Any, t: Type = type(None), val: Any = None): - self.__var = var - self.__t = t - - if not isinstance(val, t): - raise TypeError(f"expected type {t}, got {type(val)}") - - self.__val = val - - def __rshift__(self, nxt: Union["Expression", "Assignment", Callable]): - """This makes piping using the '>>' symbol possible - - Combines with the given expression, assignments, Callables to produce a new expression - where data flows from current to nxt - """ - return _append_expn(self, nxt) - - def __iter__(self): - """Generates an iterator that can be used to create a dict using dict()""" - yield self.__var, self.__val - - def __call__(self) -> Any: - """Returns the value associated with this assignment""" - return self.__val - - class Context(dict): """The context map containing variables in scope.""" @@ -125,14 +83,14 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._f(*args, **self._context, **kwargs) - def __rshift__(self, nxt: Union["Expression", "Assignment", Callable]): + def __rshift__(self, nxt: Union["Expression", Callable]): """This makes piping using the '>>' symbol possible. Combines with the given `nxt` expression to produce a new expression where data flows from current to nxt. Args: - nxt: the next expression, assignment or callable to apply after the current one. + nxt: the next expression, or callable to apply after the current one. """ merged_expn = _append_expn(self, nxt) @@ -334,15 +292,10 @@ def _get_func_signature(func: Callable): return signature(func.__call__) -def to_expn(v: Union["Expression", "Assignment", Callable, Any]) -> "Expression": +def to_expn(v: Union["Expression", Callable, Any]) -> "Expression": """Converts a Callable or Expression into an Expression""" if isinstance(v, Expression): return v - elif isinstance(v, Assignment): - # update the context - return Expression( - Operation(lambda *args, **kwargs: Context(**kwargs, **dict(v))) - ) elif isinstance(v, Callable): return Expression(Operation(v)) # return a noop expression @@ -350,8 +303,8 @@ def to_expn(v: Union["Expression", "Assignment", Callable, Any]) -> "Expression" def _append_expn( - first: Union["Expression", "Assignment", Callable, Any], - other: Union["Expression", "Assignment", Callable, Any], + first: Union["Expression", Callable, Any], + other: Union["Expression", Callable, Any], ): """Returns a new combined Expression where the current expression runs before the passed expression""" other = to_expn(other) diff --git a/tests/test_assignments.py b/tests/test_assignments.py deleted file mode 100644 index a3d5a72..0000000 --- a/tests/test_assignments.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from funml import let, val - - -def test_let_sets_internal_value(): - """let() a value in the internal value""" - test_data = [ - (let(int, x=90), 90), - (let(str, foo="bar"), "bar"), - (let(dict, data={"bar": "bel"}), {"bar": "bel"}), - (let(float, y=900.0), 900.0), - ] - - for (assign, expected) in test_data: - assert assign() == expected - - -def test_let_can_be_piped(): - """let() can be piped""" - v = let(int, x=9) >> (lambda x: x + 10) >> (lambda y: y * 20) >> str - assert v() == "380" - - # order of the let statements doesn't matter as long as they come before they - # are used - v = let(int, power=3) >> let(int, num=9) >> (lambda num, power: num**power) - assert v() == 729 - - -def test_let_sets_value_in_context(): - """let() sets a value in the context, passing it to expression as kwargs""" - - def get_context(**kwargs): - return kwargs - - test_data = [ - (let(int, x=90), {"x": 90}), - (let(float, y=900.0), {"y": 900.0}), - (let(str, foo="bar"), {"foo": "bar"}), - (let(dict, data={"bar": "bel"}), {"data": {"bar": "bel"}}), - ] - - for (assign, expected) in test_data: - expn = assign >> get_context - assert expn() == expected - - -def test_let_checks_types_when_initializing(): - """let() makes sure `value` is of right type""" - test_data = [ - (int, "string"), - (bytes, "foo"), - (float, 90), - (str, {"P": 9}), - (dict, (9, "tuple")), - ] - - for (t, v) in test_data: - with pytest.raises(TypeError): - _ = let(t, a=v) - - -def test_val_literals(): - """val just creates an unnamed variable or a literal""" - test_data = ["foo", True, 90, 909.0] - - for v in test_data: - assert val(v)() == v - - -def test_val_expressions(): - """val converts a function into an expression""" - fn = val(min) >> str - test_data = [ - ([2, 6, 8], "2"), - ([2, -12, 8], "-12"), - ([20, 6, 18], "6"), - ([0.2, 6.0, 0.08], "0.08"), - ] - - for v, expected in test_data: - assert fn(v) == expected - - -def test_val_piping(): - """val literals can be piped to other expressions""" - - v = val(900) >> (lambda x: x + 10) >> (lambda y: y * 20) >> str - - assert v() == "18200" diff --git a/tests/test_expressions.py b/tests/test_expressions.py new file mode 100644 index 0000000..59236f5 --- /dev/null +++ b/tests/test_expressions.py @@ -0,0 +1,31 @@ +from funml import val + + +def test_val_literals(): + """val just creates an unnamed variable or a literal""" + test_data = ["foo", True, 90, 909.0] + + for v in test_data: + assert val(v)() == v + + +def test_val_expressions(): + """val converts a function into an expression""" + fn = val(min) >> str + test_data = [ + ([2, 6, 8], "2"), + ([2, -12, 8], "-12"), + ([20, 6, 18], "6"), + ([0.2, 6.0, 0.08], "0.08"), + ] + + for v, expected in test_data: + assert fn(v) == expected + + +def test_val_piping(): + """val literals can be piped to other expressions""" + + v = val(900) >> (lambda x: x + 10) >> (lambda y: y * 20) >> str + + assert v() == "18200" diff --git a/tests/test_match.py b/tests/test_match.py index 984c78c..87f7e93 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -2,7 +2,7 @@ from functools import reduce from typing import Any -from funml import Option, match, record, l, imap, ireduce +from funml import Option, match, record, l, imap, ireduce, val def test_match_any_type(): diff --git a/tests/test_monads.py b/tests/test_monads.py index 029d017..dfb8ccb 100644 --- a/tests/test_monads.py +++ b/tests/test_monads.py @@ -2,7 +2,6 @@ from funml import ( Result, - let, if_ok, Option, record, @@ -33,11 +32,9 @@ def test_if_ok(): (val("excellent"), Result.OK(90), "excellent"), (lambda v: v * 20, Result.OK(9), 180), (val(lambda v: v * 20), Result.OK(9), 180), - (let(int, h=60), Result.OK("yeah"), {"h": 60}), ("excellent", Result.ERR(ValueError()), Result.ERR(ValueError())), (val("excellent"), Result.ERR(ValueError()), Result.ERR(ValueError())), (lambda v: v * 20, Result.ERR(Exception()), Result.ERR(Exception())), - (let(int, h=60), Result.ERR(TypeError()), Result.ERR(TypeError())), ] for do, value, expected in test_data: @@ -52,11 +49,9 @@ def test_if_err(): (val("excellent"), Result.ERR(ValueError()), "excellent"), (lambda v: str(v), Result.ERR(Exception("error")), "error"), (val(lambda v: str(v)), Result.ERR(Exception("error")), "error"), - (let(int, h=60), Result.ERR(TypeError()), {"h": 60}), ("excellent", Result.OK(90), Result.OK(90)), (lambda v: v * 20, Result.OK(9), Result.OK(9)), (val(lambda v: v * 20), Result.OK(9), Result.OK(9)), - (let(int, h=60), Result.OK("yeah"), Result.OK("yeah")), ] for do, value, expected in test_data: @@ -70,11 +65,9 @@ def test_if_ok_if_err_match_error(): ("excellent", 90), (lambda v: v * 20, "Result.OK(9)"), (val(lambda v: v * 20), "Result.OK(9)"), - (let(int, h=60), Option.SOME("yeah")), ("excellent", None), (lambda v: v * 20, dict(h=90)), (val(lambda v: v * 20), dict(h=90)), - (let(int, h=60), Color(r=6, b=90, g=78)), ] for do, value in test_data: @@ -129,7 +122,6 @@ def test_is_ok_is_err_match_error(): Option.SOME("yeah"), None, dict(h=90), - (let(int, h=60), Color(r=6, b=90, g=78)), ] for value in test_data: @@ -150,11 +142,9 @@ def test_if_some(): ("excellent", Option.SOME(90), "excellent"), (lambda v: v * 20, Option.SOME(9), 180), (val(lambda v: v * 20), Option.SOME(9), 180), - (let(int, h=60), Option.SOME("yeah"), {"h": 60}), ("excellent", Option.NONE, Option.NONE), (lambda v: v * 20, Option.NONE, Option.NONE), (val(lambda v: v * 20), Option.NONE, Option.NONE), - (let(int, h=60), Option.NONE, Option.NONE), ] for do, value, expected in test_data: @@ -168,11 +158,9 @@ def test_if_none(): ("excellent", Option.NONE, "excellent"), (lambda v: 180, Option.NONE, 180), (val(lambda v: 180), Option.NONE, 180), - (let(int, h=60), Option.NONE, {"h": 60}), ("excellent", Option.SOME(90), Option.SOME(90)), (lambda v: v * 20, Option.SOME(9), Option.SOME(9)), (val(lambda v: v * 20), Option.SOME(9), Option.SOME(9)), - (let(int, h=60), Option.SOME("yeah"), Option.SOME("yeah")), ] for do, value, expected in test_data: @@ -186,11 +174,9 @@ def test_if_some_if_none_match_error(): ("excellent", 90), (lambda v: v * 20, "Option.SOME(9)"), (val(lambda v: v * 20), "Option.SOME(9)"), - (let(int, h=60), Result.OK("yeah")), ("excellent", None), (lambda v: v * 20, dict(h=90)), (val(lambda v: v * 20), dict(h=90)), - (let(int, h=60), Color(r=6, b=90, g=78)), ] for do, value in test_data: @@ -245,7 +231,6 @@ def test_is_some_is_none_match_error(): Result.OK("yeah"), None, dict(h=90), - (let(int, h=60), Color(r=6, b=90, g=78)), ] for value in test_data: From 367ada48fe1df43fc0dde9d515b23f293defe923 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 28 Jan 2023 17:01:20 +0300 Subject: [PATCH 2/6] Add tests for pure expressions --- tests/test_expressions.py | 36 ++++++++++++++++++++++++++++ tests/test_match.py | 50 +++++++++++++++++++++++++++++++++++++++ tests/test_pipeline.py | 5 ++++ 3 files changed, 91 insertions(+) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 59236f5..238023a 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -29,3 +29,39 @@ def test_val_piping(): v = val(900) >> (lambda x: x + 10) >> (lambda y: y * 20) >> str assert v() == "18200" + + +def test_expressions_pure_by_default(): + """expressions have no hidden side effects by default""" + accum_factrl = lambda v, accum: accum if v <= 0 else accum_factrl(v - 1, v * accum) + pure_factorial = lambda v: accum_factrl(v, 1) + + unit_expn = val(lambda v: v) + if_else_expn = val( + lambda check=unit_expn, do=unit_expn, else_do=unit_expn: lambda *args, **kwargs: ( + do(*args, **kwargs) if check(*args, **kwargs) else else_do(*args, **kwargs) + )() + ) + + tester = lambda v: v <= 0 + accum_factrl_expn = lambda v, accum: if_else_expn( + check=tester, do=val(accum), else_do=accum_factrl_expn(v - 1, v * accum) + ) + factorial_expn = val(lambda v: accum_factrl_expn(v, 1)) + + test_data = [ + (1, 1), + (2, 2), + (3, 6), + (4, 24), + (5, 120), + (6, 720), + (7, 5040), + (8, 40320), + (9, 362880), + (10, 3628800), + ] + + for value, expected in test_data: + assert pure_factorial(value) == expected + assert factorial_expn(value) == expected diff --git a/tests/test_match.py b/tests/test_match.py index 87f7e93..730abdf 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -175,3 +175,53 @@ def test_match_piping(): ) assert value() == expected + + +def test_match_are_pure_by_default(): + """Match expressions are pure by default""" + pure_unit = lambda v, *args, **kwargs: v + if_else = lambda check=pure_unit, do=pure_unit, else_do=pure_unit: ( + lambda *args, **kwargs: do(*args, **kwargs) + if (check(*args, **kwargs)) + else else_do(*args, **kwargs) + ) + + unit_expn = val(pure_unit) + if_else_expn = val( + lambda check=unit_expn, do=unit_expn, else_do=unit_expn: lambda *args, **kwargs: ( + match(check(*args, **kwargs)) + .case(True, do=do(*args, **kwargs)) + .case(False, do=else_do(*args, **kwargs)) + )() + ) + + is_num = lambda v: isinstance( + v, + ( + int, + float, + ), + ) + is_str = lambda v: isinstance(v, str) + + to_str = str + to_num = int + + to_str_expn = val(to_str) + to_num_expn = val(to_num) + + is_num_expn = val(is_num) + is_str_expn = val(is_str) + + test_data = [ + # check, do, else_do, value, expected + (is_num, to_str, to_num, 89, "89"), + (is_num, to_str, to_num, "89", 89), + (is_num_expn, to_str_expn, to_num_expn, 89, "89"), + (is_num_expn, to_str_expn, to_num_expn, "89", 89), + (is_str_expn, to_num_expn, to_str, "89", 89), + ] + + for check, do, else_do, value, expected in test_data: + assert if_else(check=check, do=do, else_do=else_do)(value) == expected + assert if_else_expn(check=check, do=do, else_do=else_do)(value) == expected diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 7d34460..43789f8 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -27,3 +27,8 @@ def test_execute_rshift_error(): with pytest.raises(NotImplementedError): execute() >> val("hey") >> (lambda x: f"{x} you") >> (lambda g: f"{g}, John") + + +def test_execute_are_pure_by_default(): + """execute is a pure function by default""" + assert False From 6b2605d652a9f56e5d5099d3854122340079619d Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 30 Jan 2023 08:31:54 +0300 Subject: [PATCH 3/6] Remove Context --- funml/types.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/funml/types.py b/funml/types.py index cb2c020..81210da 100644 --- a/funml/types.py +++ b/funml/types.py @@ -6,12 +6,6 @@ from funml.utils import is_equal_or_of_type -class Context(dict): - """The context map containing variables in scope.""" - - pass - - class MLType: """An ML-enabled type, that can easily be used in pattern matching, piping etc. @@ -60,7 +54,6 @@ class Expression: # factorial = ml.val(lambda x: accum_factorial(x, 1)) def __init__(self, f: Optional["Operation"] = None): self._f = f if f is not None else Operation(lambda x, *args, **kwargs: x) - self._context: "Context" = Context() self._queue: List[Expression] = [] def __call__(self, *args: Any, **kwargs: Any) -> Any: @@ -75,13 +68,11 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: """ prev_output = self._run_prev_expns(*args, **kwargs) - if isinstance(prev_output, Context): - self._context.update(prev_output) - elif prev_output is not None: + if prev_output is not None: # make sure piped expressions only consume previous outputs args, and kwargs - return self._f(prev_output, **self._context, **kwargs) + return self._f(prev_output, **kwargs) - return self._f(*args, **self._context, **kwargs) + return self._f(*args, **kwargs) def __rshift__(self, nxt: Union["Expression", Callable]): """This makes piping using the '>>' symbol possible. @@ -100,7 +91,7 @@ def __rshift__(self, nxt: Union["Expression", Callable]): return merged_expn - def _run_prev_expns(self, *args: Any, **kwargs: Any) -> Union["Context", Any]: + def _run_prev_expns(self, *args: Any, **kwargs: Any) -> Any: """Runs all the previous expressions, returning the final output. In order to have expressions piped, all expressions are queued in the @@ -119,12 +110,10 @@ def _run_prev_expns(self, *args: Any, **kwargs: Any) -> Union["Context", Any]: for expn in self._queue: if output is None: - output = expn(*args, **expn._context, **kwargs) - elif isinstance(output, Context): - output = expn(*args, **expn._context, **output, **kwargs) + output = expn(*args, **kwargs) else: # make sure piped expressions only consume previous outputs args, and kwargs - output = expn(output, **expn._context, **kwargs) + output = expn(output, **kwargs) return output @@ -271,7 +260,7 @@ def __init__(self, func: Callable): else: self.__f = func - def __call__(self, *args: Any, **kwargs: "Context") -> Any: + def __call__(self, *args: Any, **kwargs: Any) -> Any: """Applies the logic attached to this operation and returns output. Args: From 83a4c49910c592efcd1d34ca1bc83a14cbee8631 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 30 Jan 2023 13:55:00 +0300 Subject: [PATCH 4/6] Make expressions pure --- docs/reference.md | 1 - docs_src/index/main.py | 33 ++++--- funml/types.py | 201 +++++++++++++++++--------------------- tests/test_expressions.py | 27 +++-- tests/test_match.py | 13 ++- tests/test_pipeline.py | 46 +++++++-- 6 files changed, 176 insertions(+), 145 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 9f2353d..2bddcc9 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -13,7 +13,6 @@ Types - MatchExpression - MLType - Operation - - Context Lists -- diff --git a/docs_src/index/main.py b/docs_src/index/main.py index 60a5ae5..4e6efcf 100644 --- a/docs_src/index/main.py +++ b/docs_src/index/main.py @@ -98,11 +98,19 @@ class Color: """ unit = ml.val(lambda v: v) is_even = ml.val(lambda v: v % 2 == 0) - mul = ml.val(lambda x, y: x * y) + mul = ml.val(lambda args: args[0] * args[1]) superscript = ml.val(lambda num, power: num**power) get_month = ml.val(lambda value: value.month) is_num = ml.val(lambda v: isinstance(v, (int, float))) is_exp = ml.val(lambda v: isinstance(v, BaseException)) + is_zero_or_less = ml.val(lambda v, *args: v <= 0) + if_else = lambda check=unit, do=unit, else_do=unit: ml.val( + lambda *args, **kwargs: ( + ml.match(check(*args, **kwargs)) + .case(True, do=lambda: do(*args, **kwargs)) + .case(False, do=lambda: else_do(*args, **kwargs)) + )() + ) """ Higher-level Expressions @@ -127,30 +135,23 @@ class Color: composing simpler functions into more complex functions to an indefinite level of complexity BUT while keeping the complex functions readable and predictable (pure) - - NOTE: - --- - Avoid calling expressions recursively. Each expression has state - and unexpected things happen when hidden state is maintained during - recursion. """ - accum_factorial = ml.val( - lambda num, accum: ( - accum if num <= 0 else accum_factorial(num - 1, num * accum) - ) + accum_factorial = if_else( + check=is_zero_or_less, + do=lambda v, ac: ac, + else_do=lambda v, ac: accum_factorial(v - 1, v * ac), ) cube = ml.val(lambda v: superscript(v, 3)) factorial = ml.val(lambda x: accum_factorial(x, 1)) get_item_types = ml.ireduce(lambda x, y: f"{type(x)}, {type(y)}") - num_type_err = ml.val( - lambda *args: TypeError(f"expected numbers, got {get_item_types(args)}") + nums_type_err = ml.val( + lambda args: TypeError(f"expected numbers, got {get_item_types(args)}") ) is_seq_of_nums = ml.ireduce(lambda x, y: x and is_num(y), True) to_result = ml.val(lambda v: ml.Result.ERR(v) if is_exp(v) else ml.Result.OK(v)) try_multiply = ( - ml.val(lambda x, y: num_type_err(x, y) if is_seq_of_nums([x, y]) else mul(x, y)) - >> to_result + if_else(check=is_seq_of_nums, do=mul, else_do=nums_type_err) >> to_result ) result_to_option = ml.if_ok(ml.Option.SOME, strict=False) >> ml.if_err( @@ -257,7 +258,7 @@ class Color: print(f"blue: {blue}") - data = ml.val(data) >> ml.imap(lambda x: try_multiply(*x)) >> ml.execute() + data = ml.val(data) >> ml.imap(try_multiply) >> ml.execute() print(f"\nafter multiplication:\n{data}") data_as_options = ml.val(data) >> ml.imap(result_to_option) >> ml.execute() diff --git a/funml/types.py b/funml/types.py index 81210da..2865648 100644 --- a/funml/types.py +++ b/funml/types.py @@ -36,6 +36,72 @@ def _is_like(self, other: Any) -> bool: raise NotImplemented("_is_like not implemented") +class Pipeline: + """A series of logic blocks that operate on the same data in sequence. + + This has internal state so it is not be used in such stuff as recursion. + However when compile is run on it, a reusable (pure) expression is created. + """ + + def __init__(self): + self._queue: List[Expression] = [] + self._is_terminated = False + + def __rshift__(self, nxt: Union["Expression", Callable, "Pipeline"]): + """Uses `>>` to append the nxt expression, callable, pipeline to this pipeline. + + Args: + nxt: the next expression, pipeline, or callable to apply after the current one. + + Raises: + ValueError: when the pipeline is already terminated with ml.execute() in its queue. + """ + self.__update_queue(nxt) + if self._is_terminated: + return self() + + return self + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + """Computes the logic within the pipeline and returns the value. + + This method runs all those expressions in the queue sequentially, + with the output of an expression being used as + input for the next expression. + + Args: + args: any arguments passed. + kwargs: any key-word arguments passed + + Returns: + the computed output of this pipeline. + """ + output = None + queue = self._queue[:-1] if self._is_terminated else self._queue + + for expn in queue: + if output is None: + output = expn(*args, **kwargs) + else: + # make sure piped expressions only consume previous outputs args, and kwargs + output = expn(output, **kwargs) + + return output + + def __update_queue(self, nxt): + """Appends a pipeline or an expression to the queue.""" + if self._is_terminated: + raise ValueError("a terminated pipeline cannot be extended.") + + if isinstance(nxt, Pipeline): + self._queue += nxt._queue + self._is_terminated = nxt._is_terminated + else: + nxt_expn = to_expn(nxt) + self._queue.append(nxt_expn) + self._is_terminated = isinstance(nxt, ExecutionExpression) + + class Expression: """Logic that returns a value when applied. @@ -46,15 +112,8 @@ class Expression: f: the operation or logic to run as part of this expression """ - # FIXME: Expressions are not working right when undergoing recursion - # It seems as if the operation f is frozen and never changes - # or something like that. Try: accum_factorial = ml.val(lambda num, accum: ( - # ml.match(num <= 0).case(True, do=lambda: accum).case(False, accum_factorial(num - 1, num * accum))() - # )) - # factorial = ml.val(lambda x: accum_factorial(x, 1)) def __init__(self, f: Optional["Operation"] = None): self._f = f if f is not None else Operation(lambda x, *args, **kwargs: x) - self._queue: List[Expression] = [] def __call__(self, *args: Any, **kwargs: Any) -> Any: """Computes the logic within and returns the value. @@ -66,68 +125,39 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: Returns: the computed output of this expression. """ - prev_output = self._run_prev_expns(*args, **kwargs) - - if prev_output is not None: - # make sure piped expressions only consume previous outputs args, and kwargs - return self._f(prev_output, **kwargs) - return self._f(*args, **kwargs) - def __rshift__(self, nxt: Union["Expression", Callable]): + def __rshift__(self, nxt: Union["Expression", "Pipeline", Callable]) -> "Pipeline": """This makes piping using the '>>' symbol possible. - Combines with the given `nxt` expression to produce a new expression + Combines with the given `nxt` expression or pipeline to produce a new pipeline where data flows from current to nxt. Args: - nxt: the next expression, or callable to apply after the current one. - """ - merged_expn = _append_expn(self, nxt) - - if isinstance(nxt, ExecutionExpression): - # stop pipeline, execute and return values - return merged_expn() - - return merged_expn - - def _run_prev_expns(self, *args: Any, **kwargs: Any) -> Any: - """Runs all the previous expressions, returning the final output. - - In order to have expressions piped, all expressions are queued in the - expression at the end of the pipe. This method runs all those expressions sequentially, - with the output of the previous expression being used as input of the current expression. - - Args: - args: Any args passed - kwargs: Any key-word arguments passed. + nxt: the next expression, pipeline, or callable to apply after the current one. - Returns: - The output of the most previous expression, after running all expressions before it and piping their output - sequentially to the next. + Returns: + a new pipeline where the first expression is the current expression followed by `nxt` """ - output = None + new_pipeline = Pipeline() + new_pipeline >> self >> nxt + return new_pipeline - for expn in self._queue: - if output is None: - output = expn(*args, **kwargs) - else: - # make sure piped expressions only consume previous outputs args, and kwargs - output = expn(output, **kwargs) - return output +class ExecutionExpression(Expression): + """Expression that executes all previous once it is found on a pipeline - def append_prev_expns(self, *expns: "Expression"): - """Appends expressions that should be computed before this one. + Raises: + NotImplementedError: when `>>` is used after it. + """ - Args: - expns: the previous expressions to add to the queue - """ - self._queue += expns + def __rshift__(self, nxt: Any): + """rshift is not supported for this. - def clear_prev_expns(self): - """Clears all previous expressions in queue.""" - self._queue.clear() + This is a terminal expression that expects no other expression + after it on the pipeline. + """ + raise NotImplementedError("terminal pipeline expression: `>>` not supported") class MatchExpression(Expression): @@ -138,7 +168,7 @@ class MatchExpression(Expression): """ def __init__(self, arg: Optional[Any] = None): - super().__init__(f=Operation(self)) + super().__init__() self._matches: List[Tuple[Callable, Expression]] = [] self.__arg = arg @@ -146,7 +176,7 @@ def case(self, pattern: Union[MLType, Any], do: Callable) -> "MatchExpression": """Adds a case to a match statement. This is chainable, allowing multiple cases to be added to the same - match expression. + match pipeline. Args: pattern: the pattern to match against. @@ -164,12 +194,12 @@ def case(self, pattern: Union[MLType, Any], do: Callable) -> "MatchExpression": self.__add_match(check=check, expn=expn) return self - def __add_match(self, check: Callable, expn: Expression): - """Adds a match set to the list of matches. + def __add_match(self, check: Callable, expn: "Expression"): + """Adds a match set to the list of match sets A match set comprises a checker function and an expression. The checker function checks if a given argument matches this case. - The expression that is called when the case is matched. + The expression is called when the case is matched. Args: check: the checker function @@ -188,7 +218,8 @@ def __add_match(self, check: Callable, expn: Expression): def __call__(self, arg: Optional[Any] = None) -> Any: """Applies the matched case and returns the output. - The match cases are surveyed for any that matches the given argument until one that matches is found. + The match cases are surveyed for any that matches the given argument + until one that matches is found. Then the expression of that case is run and its output returned. Args: @@ -203,11 +234,6 @@ def __call__(self, arg: Optional[Any] = None) -> Any: if arg is None: arg = self.__arg - args = [] if arg is None else [arg] - prev_output = self._run_prev_expns(*args) - if prev_output is not None: - arg = prev_output - for check, expn in self._matches: if check(arg): return expn(arg) @@ -215,36 +241,6 @@ def __call__(self, arg: Optional[Any] = None) -> Any: raise errors.MatchError(arg) -class ExecutionExpression(Expression): - """Expression that executes all previous once it is found on a pipeline - - Args: - args: any arguments to run on the pipeline - kwargs: any key-word arguments to run on the pipeline. - - Raises: - NotImplementedError: when `>>` is used after it. - """ - - def __init__(self, *args, **kwargs): - op = Operation(lambda *a, **kwd: a[0] if len(a) > 0 else None) - super().__init__(f=op) - self.__args = args - self.__kwargs = kwargs - - def __call__(self, *args, **kwargs): - """Computes value of most recent expression, using args on object plus any new ones""" - return self._run_prev_expns(*self.__args, *args, **self.__kwargs, **kwargs) - - def __rshift__(self, nxt: Any): - """rshift is not supported for this. - - This is a terminal expression that expects no other expression - after it on the pipeline. - """ - raise NotImplementedError("terminal pipeline expression: `>>` not supported") - - class Operation: """A computation. @@ -289,16 +285,3 @@ def to_expn(v: Union["Expression", Callable, Any]) -> "Expression": return Expression(Operation(v)) # return a noop expression return Expression(Operation(func=lambda: v)) - - -def _append_expn( - first: Union["Expression", Callable, Any], - other: Union["Expression", Callable, Any], -): - """Returns a new combined Expression where the current expression runs before the passed expression""" - other = to_expn(other) - first = to_expn(first) - - other.append_prev_expns(*first._queue, first) - first.clear_prev_expns() - return other diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 238023a..12cb248 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -1,3 +1,5 @@ +from typing import Callable, Any + from funml import val @@ -31,21 +33,32 @@ def test_val_piping(): assert v() == "18200" -def test_expressions_pure_by_default(): - """expressions have no hidden side effects by default""" - accum_factrl = lambda v, accum: accum if v <= 0 else accum_factrl(v - 1, v * accum) +def test_expressions_are_pure(): + """expressions have no hidden side effects""" + unit = lambda v: v + tester = lambda v, *args: v <= 0 + if_else = lambda check=unit, do=unit, else_do=unit: lambda *args, **kwargs: ( + do(*args, **kwargs) if check(*args, **kwargs) else else_do(*args, **kwargs) + ) + + accum_factrl = if_else( + check=tester, + do=lambda v, accum: accum, + else_do=lambda v, accum: accum_factrl(v - 1, v * accum), + ) pure_factorial = lambda v: accum_factrl(v, 1) unit_expn = val(lambda v: v) if_else_expn = val( lambda check=unit_expn, do=unit_expn, else_do=unit_expn: lambda *args, **kwargs: ( do(*args, **kwargs) if check(*args, **kwargs) else else_do(*args, **kwargs) - )() + ) ) - tester = lambda v: v <= 0 - accum_factrl_expn = lambda v, accum: if_else_expn( - check=tester, do=val(accum), else_do=accum_factrl_expn(v - 1, v * accum) + accum_factrl_expn = if_else_expn( + check=val(tester), + do=val(lambda v, accum: accum), + else_do=val(lambda v, accum: accum_factrl_expn(v - 1, v * accum)), ) factorial_expn = val(lambda v: accum_factrl_expn(v, 1)) diff --git a/tests/test_match.py b/tests/test_match.py index 730abdf..c9711f6 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -188,10 +188,10 @@ def test_match_are_pure_by_default(): unit_expn = val(pure_unit) if_else_expn = val( - lambda check=unit_expn, do=unit_expn, else_do=unit_expn: lambda *args, **kwargs: ( - match(check(*args, **kwargs)) - .case(True, do=do(*args, **kwargs)) - .case(False, do=else_do(*args, **kwargs)) + lambda check_expn=unit_expn, do_expn=unit_expn, else_do_expn=unit_expn: lambda *args, **kwargs: ( + match(check_expn(*args, **kwargs)) + .case(True, do=lambda: do_expn(*args, **kwargs)) + .case(False, do=lambda: else_do_expn(*args, **kwargs)) )() ) @@ -224,4 +224,7 @@ def test_match_are_pure_by_default(): for check, do, else_do, value, expected in test_data: assert if_else(check=check, do=do, else_do=else_do)(value) == expected - assert if_else_expn(check=check, do=do, else_do=else_do)(value) == expected + assert ( + if_else_expn(check_expn=check, do_expn=do, else_do_expn=else_do)(value) + == expected + ) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 43789f8..d42a505 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1,6 +1,8 @@ +from datetime import date + import pytest -from funml import val, execute +from funml import val, execute, match, ireduce, l, imap def test_execute(): @@ -13,8 +15,8 @@ def test_execute(): ), ] - for pipe, expected in test_data: - assert (pipe >> execute()) == expected + for pipeline, expected in test_data: + assert (pipeline >> execute()) == expected def test_execute_rshift_error(): @@ -22,13 +24,43 @@ def test_execute_rshift_error(): with pytest.raises(TypeError): val(90) >> (lambda x: x**2) >> execute() >> (lambda v: v / 90) - with pytest.raises(TypeError): + with pytest.raises(ValueError): val("hey") >> execute() >> (lambda x: f"{x} you") >> (lambda g: f"{g}, John") with pytest.raises(NotImplementedError): execute() >> val("hey") >> (lambda x: f"{x} you") >> (lambda g: f"{g}, John") -def test_execute_are_pure_by_default(): - """execute is a pure function by default""" - assert False +def test_pipelines_can_be_combined(): + """pipelines can be combined as though they were expressions""" + get_month = val(lambda value: value.month) + add = val(lambda x, y: x + y) + sum_of_months_pipeline = imap(get_month) >> ireduce(add) + test_data = [ + ( + val( + [ + date(200, 3, 4), + date(2009, 1, 16), + date(1993, 12, 29), + ] + ), + 16, + ), + ( + val( + [ + date(2004, 10, 13), + date(2020, 9, 5), + date(2004, 5, 7), + date(1228, 8, 18), + ] + ), + 32, + ), + ] + + for data, expected in test_data: + new_pipeline = data >> sum_of_months_pipeline + got = new_pipeline >> execute() + assert got == expected From 0b904affa33bfeadd3cfaba1ba553f4a7bf32802 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 30 Jan 2023 17:35:11 +0300 Subject: [PATCH 5/6] Update documentation --- CHANGELOG.md | 15 ++ README.md | 221 +++++----------- docs/change-log.md | 15 ++ docs/index.md | 42 +-- docs/tutorial.md | 369 +++++++++++++++++++++++++++ docs_src/{index => tutorial}/main.py | 177 +++---------- funml/types.py | 7 + mkdocs.yml | 3 +- tests/test_pipeline.py | 28 +- 9 files changed, 541 insertions(+), 336 deletions(-) create mode 100644 docs/tutorial.md rename docs_src/{index => tutorial}/main.py (51%) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb5680..87f3cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.3.0] - 2023-01-30 + +### Added + +- Added `Pipeline`'s to move all piping to them + +### Changed + +- Removed `Context` +- Removed `let` and `Assignment`'s as these had side effects + +### Fixed + +- Made expressions pure to avoid unexpected outcomes. + ## [0.2.0] - 2023-01-28 ### Added diff --git a/README.md b/README.md index 8dbfb45..08c2fbf 100644 --- a/README.md +++ b/README.md @@ -43,157 +43,77 @@ pip install funml - Add the following code in `main.py` ```python +from copy import copy from datetime import date import funml as ml -def main(): - """Main program""" +class Date(ml.Enum): + January = date + February = date + March = date + April = date + May = date + June = date + July = date + August = date + September = date + October = date + November = date + December = date - """ - Data Types: - === - - Using the `Enum` base class and the - `@record` decorator, one can create custom - data types in form of Enums and records respectively. - """ - class Date(ml.Enum): - January = date - February = date - March = date - April = date - May = date - June = date - July = date - August = date - September = date - October = date - November = date - December = date - - @ml.record - class Color: - r: int - g: int - b: int - a: int +@ml.record +class Color: + r: int + g: int + b: int + a: int - """ - Expressions - === - - The main construct in funml is the expression. - As long as anything is an expression, it can be piped - i.e. added to a pipeline. - - Anything can be turned into an expression using - `funml.val`. - functions, static values, variables, name it. - - Expressions are the building blocks for more expressions. - Combining multiple expressions creates new expressions - - It may have: - - - `ml.Result`, `ml.Option` and their helpers like `ml.is_ok`, `ml.if_ok` - - `IList` and its helpers `ireduce`, `imap`, `ifilter` etc. - - `Enum`'s, `Record`'s - - pattern matching with `ml.match().case(...)` - - lambda functions wrapped in `ml.val` to make them expressions - - Even piping with the `>>` to move data from LEFT to RIGHT through a number of expressions - etc. - """ + +def main(): + """Main program""" """ Primitive Expressions - --- - - We can start with a few primitive expressions. - These we will use later to build more complex expressions. - - A typical primitive expression is `ml.val()` - But one can also wrap functions/classes from external modules - - e.g. - MlDbConnection = ml.val(DbConnection) - # then later, use it as though it was a funml expression. - conn = ( - ml.val(config) - >> MlDbConnection - >> ml.execute()) - - We have some builtin primitive expressions like - - ml.val - - ml.match - - ml.execute - - ml.ireduce - - ml.ifilter - - ml.imap - - ml.if_ok - - ml.is_ok - - ml.if_err - - ml.is_err - - ml.if_some - - ml.is_some - - ml.if_none - - ml.is_none """ unit = ml.val(lambda v: v) is_even = ml.val(lambda v: v % 2 == 0) - mul = ml.val(lambda x, y: x * y) + mul = ml.val(lambda args: args[0] * args[1]) superscript = ml.val(lambda num, power: num**power) get_month = ml.val(lambda value: value.month) is_num = ml.val(lambda v: isinstance(v, (int, float))) is_exp = ml.val(lambda v: isinstance(v, BaseException)) + is_zero_or_less = ml.val(lambda v, *args: v <= 0) + if_else = lambda check=unit, do=unit, else_do=unit: ml.val( + lambda *args, **kwargs: ( + ml.match(check(*args, **kwargs)) + .case(True, do=lambda: do(*args, **kwargs)) + .case(False, do=lambda: else_do(*args, **kwargs)) + )() + ) """ - Higher-level Expressions - --- - - Here we combine the primitive expressions into more - complex ones using: - - - normal function calls - e.g. `if_else(some_stuff)` where `if_else` is a primitive expression - - pipes `>>` - pipes let one start with data then define the steps that operate on the - data. - e.g. `output = records >> remove_nulls >> parse_json >> ml.execute()` - - chaining primitives that have methods on their outputs that return expressions. - e.g. `output = ml.match(data).case(1, do=...).case(2, do=...).case(3, ...)` - - We can combine these complex expressions into even more complex ones - to infinite complexity. - - That is the main thing about functional programming i.e. - composing simpler functions into more complex functions - to an indefinite level of complexity BUT while keeping the - complex functions readable and predictable (pure) - - NOTE: - --- - Avoid calling expressions recursively. Each expression has state - and unexpected things happen when hidden state is maintained during - recursion. + High Order Expressions """ - accum_factorial = ml.val(lambda num, accum: ( - accum if num <= 0 else accum_factorial(num - 1, num * accum) - )) + accum_factorial = if_else( + check=is_zero_or_less, + do=lambda v, ac: ac, + else_do=lambda v, ac: accum_factorial(v - 1, v * ac), + ) cube = ml.val(lambda v: superscript(v, 3)) factorial = ml.val(lambda x: accum_factorial(x, 1)) get_item_types = ml.ireduce(lambda x, y: f"{type(x)}, {type(y)}") - num_type_err = ml.val( - lambda *args: TypeError(f"expected numbers, got {get_item_types(args)}") + nums_type_err = ml.val( + lambda args: TypeError(f"expected numbers, got {get_item_types(args)}") ) is_seq_of_nums = ml.ireduce(lambda x, y: x and is_num(y), True) to_result = ml.val(lambda v: ml.Result.ERR(v) if is_exp(v) else ml.Result.OK(v)) - try_multiply = ml.val( - lambda x, y: num_type_err(x, y) if is_seq_of_nums([x, y]) else mul(x, y) - ) >> to_result + try_multiply = ( + if_else(check=is_seq_of_nums, do=mul, else_do=nums_type_err) >> to_result + ) result_to_option = ml.if_ok(ml.Option.SOME, strict=False) >> ml.if_err( lambda *args: ml.Option.NONE, strict=False @@ -233,21 +153,6 @@ def main(): """ Data - === - - We have a number of data types that are work well with ml - - - IList: an immutable list, with pattern matching enabled - - Enum: an enumerable data type, with pattern matching enabled - - Record: a record-like data type, with pattern matching enabled - - Using our Higher level expressions (and lower level ones if they can), - we operate on the data. - - In order to add data variables to pipelines, we turn them into expressions - using `ml.val` - - e.g. `ml.val(90)` becomes an expression that evaluates to `lambda: 90` """ dates = [ date(200, 3, 4), @@ -264,19 +169,7 @@ def main(): blue = Color(r=0, g=0, b=255, a=1) """ - Execution - === - - To mimic pipelines, we use - `>>` as pipe to move data from left to right - and `ml.execute()` to execute the pipeline and return - the results - - Don't forget to call `ml.execute()` at the end of the - pipeline or else you will get just a callable object. - - It is more like not calling `await` on a function that - returns an `Awaitable`. + Pipeline Creation and Execution """ dates_as_enums = dates >> ml.imap(to_date_enum) >> ml.execute() print(f"\ndates as enums: {dates_as_enums}") @@ -288,18 +181,32 @@ def main(): print(f"\ncube of 5: {cube(5)}") + even_nums_pipeline = nums >> ml.ifilter(is_even) + # here `even_nums_pipeline` is a `Pipeline` instance + print(even_nums_pipeline) + + factorials_list = ( + copy(even_nums_pipeline) + >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") + >> ml.execute() + ) + # we created a new pipeline by coping the previous one + # otherwise we would be mutating the old pipeline. + # Calling ml.execute(), we get an actual iterable of strings + print(factorials_list) + factorials_str = ( - nums - >> ml.ifilter(is_even) + even_nums_pipeline >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") >> ml.ireduce(lambda x, y: f"{x}\n{y}") >> ml.execute() ) + # here after calling ml.execute(), we get one string as output print(factorials_str) print(f"blue: {blue}") - data = ml.val(data) >> ml.imap(lambda x: try_multiply(*x)) >> ml.execute() + data = ml.val(data) >> ml.imap(try_multiply) >> ml.execute() print(f"\nafter multiplication:\n{data}") data_as_options = ml.val(data) >> ml.imap(result_to_option) >> ml.execute() @@ -321,6 +228,8 @@ if __name__ == "__main__": python main.py ``` +- For more details, visit the [docs](https://sopherapps.github.io/funml) + ## Contributing Contributions are welcome. The docs have to maintained, the code has to be made cleaner, more idiomatic and faster, @@ -328,10 +237,6 @@ and there might be need for someone else to take over this repo in case I move o Please look at the [CONTRIBUTIONS GUIDELINES](./CONTRIBUTING.md) -## Benchmarks - -TBD - ## License Licensed under both the [MIT License](./LICENSE) diff --git a/docs/change-log.md b/docs/change-log.md index 5c2e303..05422bc 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.3.0] - 2023-01-30 + +### Added + +- Added `Pipeline`'s to move all piping to them + +### Changed + +- Removed `Context` +- Removed `let` and `Assignment`'s as these had side effects + +### Fixed + +- Made expressions pure to avoid unexpected outcomes. + ## [0.2.0] - 2023-01-28 ### Added diff --git a/docs/index.md b/docs/index.md index 291aa1a..1ae265b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,50 +66,14 @@ In order to ensure functions do not mutate their inputs, the data used once init - [python 3.7+](https://docs.python.org/) ### Installation -
-```console -$ pip install funml - ----> 100% -``` -
- -### Example - -#### Create a module - -- Create a file `main.py` with: - -```Python -{!../docs_src/index/main.py!} -``` - -#### Run it - -Run the example with: +Install funml with your package manager
```console -$ python main.py - -dates as enums: [, , , , , , ] - -first date enum: - -months of dates as str: -[MAR, JAN, DEC, OCT, SEP, MAY, AUG] - -cube of 5: 125 -factorial for 12: 479001600 -factorial for 8: 40320 -factorial for 6: 720 -blue: {'r': 0, 'g': 0, 'b': 255, 'a': 1} - -after multiplication: -[, , "),)>, , "),)>, ] +$ pip install funml -data as options: [, , , ] +---> 100% ```
diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..50fbaf0 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,369 @@ +# Getting Started + +In this tutorial, we will build a simple script that showcases most of the features of FunML. + +## Create the Script File + +Create a file `main.py`. + +
+ +```console +$ touch main.py +``` +
+ +## Add Some Custom Data Types + +FunML allows us to create two types of custom data types: + +- Enums: by subclassing the `ml.Enum` class +- Records: by wrapping [dataclass](https://docs.python.org/3/library/dataclasses.html)-like classes with the `@record` decorator + +!!! note + We have shortened `funml` to `ml` so wherever you see `ml` take it as `funml` + +```python +from copy import copy +from datetime import date + +import funml as ml + + +class Date(ml.Enum): + January = date + February = date + March = date + April = date + May = date + June = date + July = date + August = date + September = date + October = date + November = date + December = date + + +@ml.record +class Color: + r: int + g: int + b: int + a: int +``` + +## Expressions + +The main construct in funml is the expression. +As long as anything is an expression, it can initialize a pipeline +i.e. added to the beginning of a pipeline. + +Anything can be turned into an expression using `ml.val`. +Functions, static values, variables, name it. + +Expressions are the building blocks for more expressions. +Combining multiple expressions creates new expressions + +An expression can contain: + +- `ml.Result`, `ml.Option` and their helpers like `ml.is_ok`, `ml.if_ok` +- `IList` and its helpers `ireduce`, `imap`, `ifilter` etc. +- `Enum`'s, `Record`'s +- pattern matching with `ml.match().case(...)` +- lambda functions wrapped in `ml.val` to make them expressions +- Even piping with the `>>` to move data from LEFT to RIGHT through a number of expressions etc. + +### Add Some Primitive Expressions + +We can start with a few primitive expressions. +These we will use later to build more complex expressions. + +A typical primitive expression is `ml.val()` +But one can also wrap functions/classes from external modules + +e.g. + +```python +MlDbConnection = ml.val(DbConnection) +# then later, use it as though it was a funml expression. +conn = ( + ml.val(config) + >> MlDbConnection + >> ml.execute()) +``` + +We have some builtin primitive expressions like + +- `ml.val` +- `ml.match` +- `ml.execute` +- `ml.ireduce` +- `ml.ifilter` +- `ml.imap` +- `ml.if_ok` +- `ml.is_ok` +- `ml.if_err` +- `ml.is_err` +- `ml.if_some` +- `ml.is_some` +- `ml.if_none` +- `ml.is_none` + +So in our script, let's add a `main` function and in it, add the primitive expressions: + +```python +def main(): + """Main program""" + + """ + Primitive Expressions + """ + unit = ml.val(lambda v: v) + is_even = ml.val(lambda v: v % 2 == 0) + mul = ml.val(lambda args: args[0] * args[1]) + superscript = ml.val(lambda num, power: num**power) + get_month = ml.val(lambda value: value.month) + is_num = ml.val(lambda v: isinstance(v, (int, float))) + is_exp = ml.val(lambda v: isinstance(v, BaseException)) + is_zero_or_less = ml.val(lambda v, *args: v <= 0) + if_else = lambda check=unit, do=unit, else_do=unit: ml.val( + lambda *args, **kwargs: ( + ml.match(check(*args, **kwargs)) + .case(True, do=lambda: do(*args, **kwargs)) + .case(False, do=lambda: else_do(*args, **kwargs)) + )() + ) +``` + +### Compose Some High Order Expressions + +Here we combine the primitive expressions into more complex ones using: + +- normal function calls e.g. `if_else(some_stuff)` where `if_else` is a primitive expression +- pipelines using the pipeline operator (`>>`). + Pipelines let one start with data followed by the steps that operate on that + data e.g. `output = records >> remove_nulls >> parse_json >> ml.execute()` +- chaining primitives that have methods on their outputs that return expressions. + e.g. `output = ml.match(data).case(1, do=...).case(2, do=...).case(3, ...)` + +We can combine these complex expressions into even more complex ones to infinite complexity. + +!!! info + That is the main thing about functional programming i.e. composing simpler functions into more complex functions + to an indefinite level of complexity BUT while keeping the complex functions readable and predictable (pure). + +In our `main` function in our script `main.py`, let's add the following high order expressions. + +```python + """ + High Order Expressions + """ + accum_factorial = if_else( + check=is_zero_or_less, + do=lambda v, ac: ac, + else_do=lambda v, ac: accum_factorial(v - 1, v * ac), + ) + cube = ml.val(lambda v: superscript(v, 3)) + factorial = ml.val(lambda x: accum_factorial(x, 1)) + get_item_types = ml.ireduce(lambda x, y: f"{type(x)}, {type(y)}") + nums_type_err = ml.val( + lambda args: TypeError(f"expected numbers, got {get_item_types(args)}") + ) + is_seq_of_nums = ml.ireduce(lambda x, y: x and is_num(y), True) + to_result = ml.val(lambda v: ml.Result.ERR(v) if is_exp(v) else ml.Result.OK(v)) + + try_multiply = ( + if_else(check=is_seq_of_nums, do=mul, else_do=nums_type_err) >> to_result + ) + + result_to_option = ml.if_ok(ml.Option.SOME, strict=False) >> ml.if_err( + lambda *args: ml.Option.NONE, strict=False + ) + to_date_enum = ml.val( + lambda v: ( + ml.match(v.month) + .case(1, do=ml.val(Date.January(v))) + .case(2, do=ml.val(Date.February(v))) + .case(3, do=ml.val(Date.March(v))) + .case(4, do=ml.val(Date.April(v))) + .case(5, do=ml.val(Date.May(v))) + .case(6, do=ml.val(Date.June(v))) + .case(7, do=ml.val(Date.July(v))) + .case(8, do=ml.val(Date.August(v))) + .case(9, do=ml.val(Date.September(v))) + .case(10, do=ml.val(Date.October(v))) + .case(11, do=ml.val(Date.November(v))) + .case(12, do=ml.val(Date.December(v))) + )() + ) + get_month_str = get_month >> ( + ml.match() + .case(1, do=ml.val("JAN")) + .case(2, do=ml.val("FEB")) + .case(3, do=ml.val("MAR")) + .case(4, do=ml.val("APR")) + .case(5, do=ml.val("MAY")) + .case(6, do=ml.val("JUN")) + .case(7, do=ml.val("JUL")) + .case(8, do=ml.val("AUG")) + .case(9, do=ml.val("SEP")) + .case(10, do=ml.val("OCT")) + .case(11, do=ml.val("NOV")) + .case(12, do=ml.val("DEC")) + ) +``` + +## Add the Data to Work On + +We have a number of data types that are work well with FunML + +- IList: an immutable list, with pattern matching enabled +- Enum: an enumerable data type, with pattern matching enabled +- Record: a record-like data type, with pattern matching enabled + +Using our High order expressions (and primitive ones, your choice), we operate on the data. + +In order to add data variables to pipelines, we turn them into expressions using `ml.val` +e.g. `ml.val(90)` becomes an expression that evaluates to `lambda: 90`. + +Remember, when evaluating pipelines, we start with the data, then the steps of transformation +it has to go through. + +Let's add some data to the `main` function in our script `main.py`. + +```python + """ + Data + """ + dates = [ + date(200, 3, 4), + date(2009, 1, 16), + date(1993, 12, 29), + date(2004, 10, 13), + date(2020, 9, 5), + date(2004, 5, 7), + date(1228, 8, 18), + ] + dates = ml.val(dates) + nums = ml.val(ml.l(12, 3, 45, 7, 8, 6, 3)) + data = ml.l((2, 3), ("hey", 7), (5, "y"), (8.1, 6)) + blue = Color(r=0, g=0, b=255, a=1) +``` + +## Create Some Pipelines and Execute Them + +To construct pipelines, we use `>>` starting with an expression or data wrapped in `ml.val`. + +!!! note + If we don't add data before the pipeline, we will have to add it in another step later. + +!!! info + We prefer putting data before transformations in pipelines because it is more readable (_usually_). + + However, you might discover that it is also possible to save a pipeline and invoke it on the data like a normal function. + + Try not to do that. + +Pipelines move data from left to right, transforming it from one step to the next. + +Pipelines are lazily evaluated. To evaluate/execute a pipeline, we add `ml.execute()` as the last step of the pipeline. +This transforms the inputs into some output and returns the output. + +Otherwise, we can keep adding steps to such a pipeline across different sections of the code. + +Alright, let's create and evaluate some pipelines. + +```python + dates_as_enums = dates >> ml.imap(to_date_enum) >> ml.execute() + print(f"\ndates as enums: {dates_as_enums}") + + print(f"\nfirst date enum: {dates_as_enums[0]}") + + months_as_str = dates >> ml.imap(get_month_str) >> ml.execute() + print(f"\nmonths of dates as str:\n{months_as_str}") + + print(f"\ncube of 5: {cube(5)}") + + even_nums_pipeline = nums >> ml.ifilter(is_even) + # here `even_nums_pipeline` is a `Pipeline` instance + print(even_nums_pipeline) + + factorials_list = ( + copy(even_nums_pipeline) + >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") + >> ml.execute() + ) + # we created a new pipeline by coping the previous one + # otherwise we would be mutating the old pipeline. + # Calling ml.execute(), we get an actual iterable of strings + print(factorials_list) + + factorials_str = ( + even_nums_pipeline + >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") + >> ml.ireduce(lambda x, y: f"{x}\n{y}") + >> ml.execute() + ) + # here after calling ml.execute(), we get one string as output + print(factorials_str) + + print(f"blue: {blue}") + + data = ml.val(data) >> ml.imap(try_multiply) >> ml.execute() + print(f"\nafter multiplication:\n{data}") + + data_as_options = ml.val(data) >> ml.imap(result_to_option) >> ml.execute() + print(f"\ndata as options: {data_as_options}") + + data_as_actual_values = ( + ml.val(data) >> ml.ifilter(ml.is_ok) >> ml.imap(ml.if_ok(unit)) >> ml.execute() + ) + print(f"\ndata as actual values: {data_as_actual_values}") +``` + +## Run the Script + +To run the `main` function, we need to add the following code at the end of the `main.py` script. + +```python +if __name__ == "__main__": + main() +``` + +The final script should look like: + +```Python +{!../docs_src/tutorial/main.py!} +``` + +And then run the script in the terminal with: + +
+ +```console +$ python main.py + +dates as enums: [<Date.March: (datetime.date(200, 3, 4),)>, <Date.January: (datetime.date(2009, 1, 16),)>, <Date.December: (datetime.date(1993, 12, 29),)>, <Date.October: (datetime.date(2004, 10, 13),)>, <Date.September: (datetime.date(2020, 9, 5),)>, <Date.May: (datetime.date(2004, 5, 7),)>, <Date.August: (datetime.date(1228, 8, 18),)>] + +first date enum: <Date.March: (datetime.date(200, 3, 4),)> + +months of dates as str: +[MAR, JAN, DEC, OCT, SEP, MAY, AUG] + +cube of 5: 125 +<funml.types.Pipeline object at 0x1039ce690> +[factorial for 12: 479001600, factorial for 8: 40320, factorial for 6: 720] +factorial for 12: 479001600 +factorial for 8: 40320 +factorial for 6: 720 +blue: {'r': 0, 'g': 0, 'b': 255, 'a': 1} + +after multiplication: +[<Result.OK: (6,)>, <Result.ERR: (TypeError("expected numbers, got <class 'str'>, <class 'int'>"),)>, <Result.ERR: (TypeError("expected numbers, got <class 'int'>, <class 'str'>"),)>, <Result.OK: (48.599999999999994,)>] + +data as options: [<Option.SOME: (6,)>, <Option.NONE: ('NONE',)>, <Option.NONE: ('NONE',)>, <Option.SOME: (48.599999999999994,)>] + +data as actual values: [6, 48.599999999999994] +``` +
diff --git a/docs_src/index/main.py b/docs_src/tutorial/main.py similarity index 51% rename from docs_src/index/main.py rename to docs_src/tutorial/main.py index 4e6efcf..eb4362a 100644 --- a/docs_src/index/main.py +++ b/docs_src/tutorial/main.py @@ -1,100 +1,37 @@ +from copy import copy from datetime import date import funml as ml -def main(): - """Main program""" - - """ - Data Types: - === - - Using the `Enum` base class and the - `@record` decorator, one can create custom - data types in form of Enums and records respectively. - """ - - class Date(ml.Enum): - January = date - February = date - March = date - April = date - May = date - June = date - July = date - August = date - September = date - October = date - November = date - December = date - - @ml.record - class Color: - r: int - g: int - b: int - a: int - - """ - Expressions - === - - The main construct in funml is the expression. - As long as anything is an expression, it can be piped - i.e. added to a pipeline. +class Date(ml.Enum): + January = date + February = date + March = date + April = date + May = date + June = date + July = date + August = date + September = date + October = date + November = date + December = date - Anything can be turned into an expression using - `funml.val`. - functions, static values, variables, name it. - Expressions are the building blocks for more expressions. - Combining multiple expressions creates new expressions +@ml.record +class Color: + r: int + g: int + b: int + a: int - It may have: - - `ml.Result`, `ml.Option` and their helpers like `ml.is_ok`, `ml.if_ok` - - `IList` and its helpers `ireduce`, `imap`, `ifilter` etc. - - `Enum`'s, `Record`'s - - pattern matching with `ml.match().case(...)` - - lambda functions wrapped in `ml.val` to make them expressions - - Even piping with the `>>` to move data from LEFT to RIGHT through a number of expressions - etc. - """ +def main(): + """Main program""" """ Primitive Expressions - --- - - We can start with a few primitive expressions. - These we will use later to build more complex expressions. - - A typical primitive expression is `ml.val()` - But one can also wrap functions/classes from external modules - - e.g. - MlDbConnection = ml.val(DbConnection) - # then later, use it as though it was a funml expression. - conn = ( - ml.val(config) - >> MlDbConnection - >> ml.execute()) - - We have some builtin primitive expressions like - - ml.val - - ml.match - - ml.execute - - ml.ireduce - - ml.ifilter - - ml.imap - - ml.if_ok - - ml.is_ok - - ml.if_err - - ml.is_err - - ml.if_some - - ml.is_some - - ml.if_none - - ml.is_none """ unit = ml.val(lambda v: v) is_even = ml.val(lambda v: v % 2 == 0) @@ -113,28 +50,7 @@ class Color: ) """ - Higher-level Expressions - --- - - Here we combine the primitive expressions into more - complex ones using: - - - normal function calls - e.g. `if_else(some_stuff)` where `if_else` is a primitive expression - - pipes `>>` - pipes let one start with data then define the steps that operate on the - data. - e.g. `output = records >> remove_nulls >> parse_json >> ml.execute()` - - chaining primitives that have methods on their outputs that return expressions. - e.g. `output = ml.match(data).case(1, do=...).case(2, do=...).case(3, ...)` - - We can combine these complex expressions into even more complex ones - to infinite complexity. - - That is the main thing about functional programming i.e. - composing simpler functions into more complex functions - to an indefinite level of complexity BUT while keeping the - complex functions readable and predictable (pure) + High Order Expressions """ accum_factorial = if_else( check=is_zero_or_less, @@ -192,21 +108,6 @@ class Color: """ Data - === - - We have a number of data types that are work well with ml - - - IList: an immutable list, with pattern matching enabled - - Enum: an enumerable data type, with pattern matching enabled - - Record: a record-like data type, with pattern matching enabled - - Using our Higher level expressions (and lower level ones if they can), - we operate on the data. - - In order to add data variables to pipelines, we turn them into expressions - using `ml.val` - - e.g. `ml.val(90)` becomes an expression that evaluates to `lambda: 90` """ dates = [ date(200, 3, 4), @@ -223,19 +124,7 @@ class Color: blue = Color(r=0, g=0, b=255, a=1) """ - Execution - === - - To mimic pipelines, we use - `>>` as pipe to move data from left to right - and `ml.execute()` to execute the pipeline and return - the results - - Don't forget to call `ml.execute()` at the end of the - pipeline or else you will get just a callable object. - - It is more like not calling `await` on a function that - returns an `Awaitable`. + Pipeline Creation and Execution """ dates_as_enums = dates >> ml.imap(to_date_enum) >> ml.execute() print(f"\ndates as enums: {dates_as_enums}") @@ -247,13 +136,27 @@ class Color: print(f"\ncube of 5: {cube(5)}") + even_nums_pipeline = nums >> ml.ifilter(is_even) + # here `even_nums_pipeline` is a `Pipeline` instance + print(even_nums_pipeline) + + factorials_list = ( + copy(even_nums_pipeline) + >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") + >> ml.execute() + ) + # we created a new pipeline by coping the previous one + # otherwise we would be mutating the old pipeline. + # Calling ml.execute(), we get an actual iterable of strings + print(factorials_list) + factorials_str = ( - nums - >> ml.ifilter(is_even) + even_nums_pipeline >> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}") >> ml.ireduce(lambda x, y: f"{x}\n{y}") >> ml.execute() ) + # here after calling ml.execute(), we get one string as output print(factorials_str) print(f"blue: {blue}") diff --git a/funml/types.py b/funml/types.py index 2865648..3bb10bf 100644 --- a/funml/types.py +++ b/funml/types.py @@ -88,6 +88,13 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: return output + def __copy__(self): + """Helps call copy on a pipeline""" + new_pipeline = Pipeline() + new_pipeline._queue += self._queue + new_pipeline._is_terminated = self._is_terminated + return new_pipeline + def __update_queue(self, nxt): """Appends a pipeline or an expression to the queue.""" if self._is_terminated: diff --git a/mkdocs.yml b/mkdocs.yml index 801fcdf..64d45fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,8 @@ repo_name: sopherapps/funml repo_url: https://github.com/sopherapps/funml nav: - - Funml: index.md + - FunML: index.md + - tutorial.md - reference.md - change-log.md diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index d42a505..8130794 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1,8 +1,9 @@ +from copy import copy from datetime import date import pytest -from funml import val, execute, match, ireduce, l, imap +from funml import val, execute, ireduce, imap, l, ifilter def test_execute(): @@ -64,3 +65,28 @@ def test_pipelines_can_be_combined(): new_pipeline = data >> sum_of_months_pipeline got = new_pipeline >> execute() assert got == expected + + +def test_pipeline_copy(): + """copy copies the pipeline's state and returns a new pipeline.""" + is_even = val(lambda v: v % 2 == 0) + minus_one = val(lambda v: v - 1) + square = val(lambda v: v**2) + + test_data = [ + (l(7, 8, 6, 3), l(7, 5), "sq 8: 64\nsq 6: 36"), + (l(4, 2, 6, 3), l(3, 1, 5), "sq 4: 16\nsq 2: 4\nsq 6: 36"), + ] + + for nums, first_expected, second_expected in test_data: + even_nums_pipeline = val(nums) >> ifilter(is_even) + nums_less_1_list = copy(even_nums_pipeline) >> imap(minus_one) >> execute() + assert list(nums_less_1_list) == list(first_expected) + + squares_str = ( + even_nums_pipeline + >> imap(lambda v: f"sq {v}: {square(v)}") + >> ireduce(lambda x, y: f"{x}\n{y}") + >> execute() + ) + assert squares_str == second_expected From 8337b82aa5dfda3e5283cf55be7d131a694a6603 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 30 Jan 2023 17:36:08 +0300 Subject: [PATCH 6/6] Bump to version 0.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7334937..29bd130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "funml" -version = "0.2.0" +version = "0.3.0" description = "A collection of utilities to help write python as though it were an ML-kind of functional language like OCaml" authors = ["Martin "] readme = "README.md"