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
.
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-like classes with the
@record
decorator
Note
We have shortened funml
to ml
so wherever you see ml
take it as funml
Add the following code to main.py
.
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 = 1
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 likeml.is_ok
,ml.if_ok
IList
and its helpersireduce
,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(<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.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
ml.to_dict
ml.to_json
ml.from_json
So in our script, let's add a main
function and in it, add the primitive expressions:
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=1: 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))
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)
whereif_else
is a primitive expression - a form of currying e.g.
add3 = add(3)
whereadd = lambda x, y: x+y
- 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.
"""
High Order Expressions
"""
factorial = lambda v, accum=1: (
ml.match(v <= 0)
.case(True, do=ml.val(accum))
.case(False, do=lambda num, ac=0: factorial(num - 1, accum=num * ac)())
)
# currying expressions is possible
cube = superscript(power=3)
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
.
"""
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)
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.
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.
The final script should look like:
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 = 1
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=1: 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))
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))
)()
)
"""
High Order Expressions
"""
factorial = lambda v, accum=1: (
ml.match(v <= 0)
.case(True, do=ml.val(accum))
.case(False, do=lambda num, ac=0: factorial(num - 1, accum=num * ac)())
)
# currying expressions is possible
cube = superscript(power=3)
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"))
)
"""
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)
"""
Pipeline Creation and Execution
"""
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}")
if __name__ == "__main__":
main()
And then run the script in the terminal with:
$ 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: {'a': 1, 'r': 0, 'g': 0, 'b': 255}
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]