Skip to content

Commit

Permalink
Merge pull request #2 from sopherapps/ft-pure-expressions
Browse files Browse the repository at this point in the history
Make expressions pure
  • Loading branch information
Tinitto authored Jan 30, 2023
2 parents 8d41d31 + 8337b82 commit bdfdc2a
Show file tree
Hide file tree
Showing 20 changed files with 995 additions and 846 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
224 changes: 64 additions & 160 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,158 +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(<lambda function>)`
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.let
- 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)
))
cube = ml.let(int, power=3) >> superscript
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
Expand Down Expand Up @@ -234,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),
Expand All @@ -265,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}")
Expand All @@ -289,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()
Expand All @@ -322,17 +228,15 @@ 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,
and there might be need for someone else to take over this repo in case I move on to other things. It happens!

Please look at the [CONTRIBUTIONS GUIDELINES](./CONTRIBUTING.md)

## Benchmarks

TBD

## License

Licensed under both the [MIT License](./LICENSE)
Expand Down
15 changes: 15 additions & 0 deletions docs/change-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 3 additions & 39 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div class="termy">

```console
$ pip install funml

---> 100%
```
</div>

### 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

<div class="termy">

```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
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,)>]
$ pip install funml

data as options: [<Option.SOME: (6,)>, <Option.NONE: ('NONE',)>, <Option.NONE: ('NONE',)>, <Option.SOME: (48.599999999999994,)>]
---> 100%
```
</div>
Loading

0 comments on commit bdfdc2a

Please sign in to comment.