A collection of utilities to help write python as though it were an ML-kind of functional language like OCaml
Project description
FunML
A collection of utilities to help write python as though it were an ML-kind of functional language like OCaml
The API is still unstable. Use at your own risk.
Documentation: https://sopherapps.github.io/funml
Source Code: https://github.com/sopherapps/funml
Most Notable Features are:
- Immutable data structures like enums, records, lists
- Piping outputs of one function to another as inputs. That's how bigger functions are created from smaller ones.
- Pattern matching for declarative conditional control of flow instead of using 'if's
- Error handling using the
Result
monad, courtesy of rust. Instead of usingtry-except
all over the place, functions return aResult
which has the right data when successful and an exception if unsuccessful. The result is then pattern-matched to retrieve the data or react to the exception. - No
None
. Instead, we use theOption
monad, courtesy of rust. When an Option has data, it isOption.SOME
, or else it isOption.NONE
. Pattern matching helps handle both scenarios.
Dependencies
Getting Started
- Ensure you have python 3.7 and above installed.
- Install
FunML
pip install funml
- Add the following code in
main.py
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.
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.
"""
"""
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)
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))
"""
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.
"""
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
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)}")
)
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
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"))
)
"""
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),
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)
"""
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`.
"""
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)}")
factorials_str = (
nums
>> ml.ifilter(is_even)
>> ml.imap(lambda v: f"factorial for {v}: {factorial(v)}")
>> ml.ireduce(lambda x, y: f"{x}\n{y}")
>> ml.execute()
)
print(factorials_str)
print(f"blue: {blue}")
data = ml.val(data) >> ml.imap(lambda x: try_multiply(*x)) >> 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}")
if __name__ == "__main__":
main()
- Run the script
python main.py
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
Benchmarks
TBD
License
Licensed under both the MIT License
Copyright (c) 2023 Martin Ahindura
Gratitude
"...and His (the Father's) incomparably great power for us who believe. That power is the same as the mighty strength He exerted when He raised Christ from the dead and seated Him at His right hand in the heavenly realms, far above all rule and authority, power and dominion, and every name that is invoked, not only in the present age but also in the one to come."
-- Ephesians 1: 19-21
All glory be to God.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.