├── .gitignore ├── 1_0_start_here_basic.py ├── 1_1_do_onething_well.py ├── 1_2_fnc_as_first_class_citizen.py ├── 2_0_higher_order_function.py ├── 2_1_currying.py ├── 3_extract_column.py ├── 4_currying_lib.py ├── 5_function_composition_std.py ├── 6_compose_toolz_example.py ├── 7_0_data_toolz.py ├── 8_error_toolz.py ├── 9_0_data_try_with_toolz.py ├── 9_1_error_pymonad.py ├── 9_2_finish_here_monad.py ├── 9_3_undone_pymonad_ultimate.py ├── README.md ├── example.csv ├── ref1_toolz_currying.py ├── ref2_pymonad_compose.py ├── ref3_pymonad_io.py ├── ref4_function_composition.py ├── ref5_pymonad_compose.py ├── ref6_pymonad_lift.py ├── ref8_try_monad.py ├── requirements.txt ├── rf7_pymonad_open_file.py └── rf9_monad_manual.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | bin/ 7 | pyvenv.cfg 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ -------------------------------------------------------------------------------- /1_0_start_here_basic.py: -------------------------------------------------------------------------------- 1 | import csv 2 | 3 | # Function to handle file reading 4 | def read_csv_file(file_path): 5 | try: 6 | with open(file_path, 'r') as csvfile: 7 | reader = csv.reader(csvfile) 8 | data = [row for row in reader] 9 | return data 10 | except FileNotFoundError as e: 11 | return None 12 | 13 | # Function to extract a colum 14 | def extract_column(column_index, data): 15 | try: 16 | column_values = [float(row[column_index]) for row in data[1:]] 17 | return column_values 18 | except (ValueError, IndexError) as e: 19 | return None 20 | 21 | # Function to calculate average 22 | def calculate_average(column_values): 23 | if column_values is None or not column_values: 24 | return None, "Cannot calculate average due to empty or missing column" 25 | try: 26 | average = sum(column_values) / len(column_values) 27 | return average 28 | except ZeroDivisionError as e: 29 | return None 30 | 31 | # Data pipeline 32 | csv_file_path = 'example.csv' 33 | column_index = 1 # Assuming the second column (index 1) needs to be processed 34 | 35 | # Step 1: Read CSV file 36 | data = read_csv_file(csv_file_path) 37 | 38 | if data == None: 39 | print("Error reading CSV file") 40 | else: 41 | # Step 2: Extract column 42 | score_column_values = extract_column(column_index, data) 43 | if score_column_values == None: 44 | print("Error extracting column") 45 | else: 46 | # Step 3: Calculate average 47 | result = calculate_average(score_column_values) 48 | 49 | if result == None: 50 | print("Error calculating average") 51 | else: 52 | print(f"The average is: {result}") -------------------------------------------------------------------------------- /1_1_do_onething_well.py: -------------------------------------------------------------------------------- 1 | import csv 2 | # Function to handle file reading 3 | def read_csv_file(file_path): 4 | try: 5 | with open(file_path, 'r') as csvfile: 6 | return [row for row in csv.reader(csvfile)] 7 | except FileNotFoundError as e: 8 | return None 9 | 10 | # Extract 3 functions that do one thing and do it well 11 | def extract_column(column_index, data): 12 | try: 13 | return [row[column_index] for row in data] 14 | except (ValueError, IndexError) as e: 15 | return None 16 | 17 | def remove_row(row_index, data): 18 | try: 19 | return data[row_index:] 20 | except IndexError as e: 21 | return None 22 | 23 | def convert_to(converter, data): 24 | try: 25 | return [converter(item) for item in data] 26 | except ValueError as e: 27 | return None 28 | 29 | # Function to calculate average 30 | def calculate_average(column_values): 31 | try: 32 | return sum(column_values) / len(column_values) 33 | except ZeroDivisionError as e: 34 | return None 35 | 36 | # Data pipeline 37 | csv_file_path = 'example.csv' 38 | score_column_index = 1 39 | header_row_index = 1 40 | data = read_csv_file(csv_file_path) 41 | 42 | if data == None: 43 | print("Error reading CSV file") 44 | else: 45 | score_column_values = extract_column(score_column_index, data) 46 | removed_header_data = remove_row(header_row_index, score_column_values) 47 | score_column_as_float = convert_to(float, removed_header_data) 48 | 49 | if score_column_as_float == None: 50 | print("Error extracting column") 51 | else: 52 | result = calculate_average(score_column_as_float) 53 | if result == None: 54 | print("Error calculating average") 55 | else: 56 | print(f"The average is: {result}") -------------------------------------------------------------------------------- /1_2_fnc_as_first_class_citizen.py: -------------------------------------------------------------------------------- 1 | import csv 2 | # Function to handle file reading 3 | def read_csv_file(file_path): 4 | try: 5 | with open(file_path, 'r') as csvfile: 6 | return [row for row in csv.reader(csvfile)] 7 | except FileNotFoundError as e: 8 | return None 9 | 10 | # Extract 3 functions that do one thing and do it well 11 | def extract_column(column_index, data): 12 | try: 13 | return [row[column_index] for row in data] 14 | except (ValueError, IndexError) as e: 15 | return None 16 | 17 | def remove_row(row_index, data): 18 | try: 19 | return data[row_index:] 20 | except IndexError as e: 21 | return None 22 | 23 | def convert_to(converter, data): 24 | try: 25 | return [converter(item) for item in data] 26 | except ValueError as e: 27 | return None 28 | 29 | # Function to calculate average 30 | def calculate_average(column_values): 31 | try: 32 | return sum(column_values) / len(column_values) 33 | except ZeroDivisionError as e: 34 | return None 35 | 36 | # Data pipeline 37 | csv_file_path = 'example.csv' 38 | score_column_index = 1 39 | header_row_index = 1 40 | data = read_csv_file(csv_file_path) 41 | 42 | if data == None: 43 | print("Error reading CSV file") 44 | else: 45 | score_column_values = extract_column(score_column_index, data) 46 | removed_header_data = remove_row(header_row_index, score_column_values) 47 | score_column_as_float = convert_to(float, removed_header_data) 48 | 49 | if score_column_as_float == None: 50 | print("Error extracting column") 51 | else: 52 | result = calculate_average(score_column_as_float) 53 | if result == None: 54 | print("Error calculating average") 55 | else: 56 | print(f"The average is: {result}") -------------------------------------------------------------------------------- /2_0_higher_order_function.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------- 2 | #Higher order function 3 | #------------------------------------------------------------- 4 | def apply_operation(operation, x, y): 5 | return operation(x, y) 6 | 7 | # Binary operation functions 8 | def add(x, y): 9 | return x + y 10 | 11 | def multiply(x, y): 12 | return x * y 13 | 14 | # Usage of the higher-order function 15 | result_addition = apply_operation(add, 3, 4) 16 | result_multiplication = apply_operation(multiply, 3, 4) 17 | -------------------------------------------------------------------------------- /2_1_currying.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------- 2 | #Currying 3 | #------------------------------------------------------------- 4 | def plus(a): 5 | def add(b): 6 | return a + b 7 | return add 8 | # Usage 9 | result = plus(2)(3) 10 | # Partial apply, transform binary function to unary function 11 | plusTwo = plus(2) 12 | print(plusTwo(3)) 13 | -------------------------------------------------------------------------------- /3_extract_column.py: -------------------------------------------------------------------------------- 1 | # Sample data for testing extract_column function 2 | sample_data = [ 3 | ['Alice', '90', 'A'], 4 | ['Bob', '85', 'B'], 5 | ['Charlie', '78', 'C'], 6 | ['David', '92', 'A'], 7 | ['Eva', '88', 'B'], 8 | ['Frank', '79', 'C'], 9 | ['Grace', '95', 'A'] 10 | ] 11 | 12 | # Function to extract a column 13 | def extract_column_currying_standard_python(column_index): 14 | def curried(data): 15 | try: 16 | score_column_values = [row[column_index] for row in data] 17 | return score_column_values 18 | except (ValueError, IndexError): 19 | return None 20 | return curried 21 | 22 | # Assuming you want to extract the second column (index 1) 23 | name_column_index = 0 24 | score_column_index = 1 25 | 26 | # Testing extract_column function 27 | name_list = extract_column_currying_standard_python(name_column_index) 28 | score_list = extract_column_currying_standard_python(score_column_index) 29 | # Displaying the result 30 | print(f"Extracted Column: {score_list(sample_data)}") 31 | print(f"Extracted Column: {name_list(sample_data)}") 32 | 33 | 34 | # Function to extract a column 35 | def extract_column_currying(column_index, data): 36 | try: 37 | score_column_values = [row[column_index] for row in sample_data] 38 | return score_column_values 39 | except (ValueError, IndexError) as e: 40 | return None -------------------------------------------------------------------------------- /4_currying_lib.py: -------------------------------------------------------------------------------- 1 | from toolz import curry 2 | import csv 3 | from typing import Final 4 | 5 | # Function to handle file reading 6 | def read_csv_file(file_path): 7 | try: 8 | with open(file_path, 'r') as csvfile: 9 | return [row for row in csv.reader(csvfile)] 10 | except FileNotFoundError as e: 11 | return None 12 | 13 | # Extract 3 functions that do one thing and do it well 14 | @curry 15 | def extract_column(column_index, data): 16 | try: 17 | return [row[column_index] for row in data] 18 | except (ValueError, IndexError) as e: 19 | return None 20 | 21 | @curry 22 | def remove_row(row_index, data): 23 | try: 24 | return data[row_index:] 25 | except IndexError as e: 26 | return None 27 | 28 | @curry 29 | def convert_to(converter, data): 30 | try: 31 | return [converter(item) for item in data] 32 | except ValueError as e: 33 | return None 34 | 35 | # Function to calculate average 36 | def calculate_average(column_values): 37 | try: 38 | return sum(column_values) / len(column_values) 39 | except ZeroDivisionError as e: 40 | return None 41 | 42 | # Data pipeline 43 | csv_file_path = 'example.csv' 44 | score_column_index = 1 45 | header_row_index = 1 46 | score_column = extract_column(score_column_index) 47 | removed_header = remove_row(header_row_index) 48 | score_as_float = convert_to(float) 49 | data = read_csv_file(csv_file_path) 50 | 51 | if data == None: 52 | print("Error reading CSV file") 53 | else: 54 | score_column_values = score_column(data) 55 | removed_header_data = removed_header(score_column_values) 56 | score_column_as_float = score_as_float(removed_header_data) 57 | 58 | if score_column_as_float == None: 59 | print("Error extracting column") 60 | else: 61 | result = calculate_average(score_column_as_float) 62 | if result == None: 63 | print("Error calculating average") 64 | else: 65 | print(f"The average is: {result}") -------------------------------------------------------------------------------- /5_function_composition_std.py: -------------------------------------------------------------------------------- 1 | def compose(*functions): 2 | """ 3 | Compose multiple functions: f(g(...(h(x)))) 4 | """ 5 | def composed_function(x): 6 | result = x 7 | for func in reversed(functions): 8 | result = func(result) 9 | return result 10 | return composed_function 11 | 12 | # Example functions 13 | def square(x): 14 | return x ** 2 15 | 16 | def add_one(x): 17 | return x + 1 18 | 19 | def double(x): 20 | return x * 2 21 | 22 | def to_s(s): 23 | return f"final result {str(s)}" 24 | 25 | # Compose functions to create a pipeline 26 | pipeline = compose(to_s, square, add_one, double) 27 | result = pipeline(3) 28 | 29 | print(f"Result: {result}") 30 | -------------------------------------------------------------------------------- /6_compose_toolz_example.py: -------------------------------------------------------------------------------- 1 | from toolz import pipe 2 | 3 | # Define functions 4 | def add_two(x): 5 | return x + 2 6 | 7 | def multiply_by_three(x): 8 | return x * 3 9 | 10 | def subtract_five(x): 11 | return x - 5 12 | 13 | # Compose functions 14 | basic = subtract_five(multiply_by_three(add_two(2))) 15 | result = pipe(2, add_two, multiply_by_three, subtract_five) 16 | print(basic) 17 | print(result) 18 | -------------------------------------------------------------------------------- /7_0_data_toolz.py: -------------------------------------------------------------------------------- 1 | # This code works perfectly for success path. 2 | # It will break in the middel of execution if there is something wrong 3 | # Lets move to the next section that we will handle exception in a proper way 4 | import csv 5 | from toolz import curry, pipe 6 | from typing import Final 7 | 8 | # Function to handle file reading 9 | def read_csv_file(file_path): 10 | try: 11 | with open(file_path, 'r') as csvfile: 12 | return [row for row in csv.reader(csvfile)] 13 | except FileNotFoundError as e: 14 | return None 15 | 16 | # Extract 3 functions that do one thing and do it well 17 | @curry 18 | def extract_column(column_index, data): 19 | try: 20 | return [row[column_index] for row in data] 21 | except (ValueError, IndexError) as e: 22 | return None 23 | 24 | @curry 25 | def remove_row(row_index, data): 26 | try: 27 | return data[row_index:] 28 | except IndexError as e: 29 | return None 30 | 31 | @curry 32 | def convert_to(converter, data): 33 | try: 34 | return [converter(item) for item in data] 35 | except ValueError as e: 36 | return None 37 | 38 | # Function to calculate average 39 | def calculate_average(column_values): 40 | try: 41 | return sum(column_values) / len(column_values) 42 | except ZeroDivisionError as e: 43 | return None 44 | 45 | # Data pipeline 46 | csv_file_path = 'example.csv' 47 | score_column_index = 1 48 | header_row_index = 1 49 | score_column = extract_column(score_column_index) 50 | removed_header = remove_row(header_row_index) 51 | score_as_float = convert_to(float) 52 | 53 | average_result = pipe( 54 | read_csv_file(csv_file_path), 55 | score_column, 56 | removed_header, 57 | score_as_float, 58 | calculate_average 59 | ) 60 | 61 | print(f"An average score is {average_result}") -------------------------------------------------------------------------------- /8_error_toolz.py: -------------------------------------------------------------------------------- 1 | from toolz import curry 2 | # Define a curried function for division 3 | @curry 4 | def divide(a, b): 5 | try: 6 | result = a / b 7 | return result 8 | except ZeroDivisionError as e: 9 | raise ValueError(f"Error: {e}") 10 | 11 | try: 12 | result = divide(10)(0) 13 | except ValueError as e: 14 | print(f"Error handling: {e}") 15 | -------------------------------------------------------------------------------- /9_0_data_try_with_toolz.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from toolz import curry, pipe 3 | from typing import Final 4 | 5 | # Function to handle file reading 6 | def read_csv_file(file_path): 7 | try: 8 | with open(file_path, 'r') as csvfile: 9 | return [row for row in csv.reader(csvfile)] 10 | except FileNotFoundError as e: 11 | raise FileNotFoundError(f"Error reading file: {e}") 12 | 13 | # Extract 3 functions that do one thing and do it well 14 | @curry 15 | def extract_column(column_index, data): 16 | try: 17 | return [row[column_index] for row in data] 18 | except (IndexError) as e: 19 | raise IndexError(f"Error extracting column: {e}") 20 | 21 | @curry 22 | def remove_row(row_index, data): 23 | try: 24 | return data[row_index:] 25 | except IndexError as e: 26 | raise IndexError(f"Error removing header: {e}") 27 | 28 | @curry 29 | def convert_to(converter, data): 30 | try: 31 | return [converter(item) for item in data] 32 | except ValueError as e: 33 | raise ValueError(f"Error converting to float: {e}") 34 | 35 | # Function to calculate average 36 | def calculate_average(column_values): 37 | try: 38 | return sum(column_values) / len(column_values) 39 | except ZeroDivisionError as e: 40 | raise ZeroDivisionError(f"Error zero division: {e}") 41 | 42 | #============== Data pipeline ================================ 43 | csv_file_path = 'example.csv' 44 | score_column_index = 1 45 | header_row_index = 1 46 | score_column = extract_column(score_column_index) 47 | remove_header = remove_row(header_row_index) 48 | convert_score_to_float = convert_to(float) 49 | 50 | try: 51 | average_result = pipe( 52 | read_csv_file(csv_file_path), 53 | score_column, 54 | remove_header, 55 | convert_score_to_float, 56 | calculate_average 57 | ) 58 | print(f"An average score is {average_result}") 59 | except (FileNotFoundError, ValueError, IndexError, ZeroDivisionError) as e: 60 | print(f"Exception caught: {e}") 61 | 62 | #It work, however toolz didn't provide us the way to hanle exception in functional style 63 | #We need -------------------------------------------------------------------------------- /9_1_error_pymonad.py: -------------------------------------------------------------------------------- 1 | from pymonad.either import Left, Right 2 | from pymonad.tools import curry 3 | 4 | @curry(2) 5 | def divide(a, b): 6 | return ( 7 | Right(a/b) 8 | if b != 0 9 | else Left("Error: Division by zero") 10 | ) 11 | 12 | result = Right(10).then(divide(2)).then(divide(5)) 13 | 14 | print(result) -------------------------------------------------------------------------------- /9_2_finish_here_monad.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from pymonad.tools import curry 4 | from pymonad.either import Left, Right 5 | 6 | def read_csv_file(file_path): 7 | if os.path.isfile(file_path): 8 | with open(file_path, 'r') as csvfile: 9 | return Right([row for row in csv.reader(csvfile)]) 10 | else: 11 | return Left("Error: File not found") 12 | 13 | @curry(2) 14 | def remove_row(row_index, data): 15 | if len(data) > 1: 16 | return Right(data[row_index:]) 17 | else: 18 | return Left("Error: Unable to remove header") 19 | 20 | @curry(2) 21 | def extract_column(column_index, data): 22 | if len(data) > column_index: 23 | return Right([row[column_index] for row in data]) 24 | else: 25 | return Left("Error: Unable to extract column") 26 | 27 | @curry(2) 28 | def convert_to(converter, data): 29 | converted_data = [converter(item) if item.isdigit() else None for item in data] 30 | if all(x is not None for x in converted_data): 31 | return Right(converted_data) 32 | else: 33 | return Left("Error: Unable to convert to float") 34 | 35 | def calculate_average(column_values): 36 | if len(column_values) > 0: 37 | return Right(sum(column_values) / len(column_values)) 38 | else: 39 | return Left("Error: Division by zero") 40 | 41 | # Data pipeline using the Either monad and custom sequencing operator 42 | csv_file_path = 'example.csv' 43 | score_column_index = 1 44 | header_row_index = 1 45 | # Partial apply 46 | remove_header = remove_row(header_row_index) 47 | extract_score_column = extract_column(score_column_index) 48 | convert_score_to_float = convert_to(float) 49 | 50 | result = ( 51 | read_csv_file(csv_file_path) 52 | .then (extract_score_column) 53 | .then (remove_header) 54 | .then (convert_score_to_float) 55 | .then (calculate_average) 56 | ) 57 | 58 | if result.is_right(): 59 | print(f"An average score is {result}") 60 | else: 61 | print(f"Error processing data: {result}") 62 | -------------------------------------------------------------------------------- /9_3_undone_pymonad_ultimate.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from typing import Final 4 | from pymonad.tools import curry 5 | from pymonad.either import Left, Right 6 | 7 | # Function to handle file reading 8 | def read_csv_file(file_path): 9 | with open(file_path, 'r') as csvfile: 10 | reader = csv.reader(csvfile) 11 | data = [row for row in reader] 12 | return data 13 | return IO(read_file) 14 | 15 | def remove_header(data): 16 | return ( 17 | Right(data[1:]) 18 | if len(data) > 1 19 | else Left("Error: Unable to remove header") 20 | ) 21 | 22 | @curry(2) 23 | def extract_column(column_index, data): 24 | return ( 25 | Right(data).bind(lambda rows: 26 | Right(list(map(lambda row: row[column_index], rows)))) 27 | ) 28 | 29 | extract_score_column = extract_column(1) 30 | extract_name_column = extract_column(0) 31 | 32 | def convert_to_float(data): 33 | return ( 34 | Right(list(map(float, data))) 35 | if data 36 | else Left("Error: Unable to convert to float") 37 | ) 38 | 39 | 40 | def calculate_average(column_values): 41 | return ( 42 | Right(sum(column_values) / len(column_values)) 43 | if column_values 44 | else Left("Error: Division by zero") 45 | ) 46 | 47 | # Data pipeline using the Either monad and custom sequencing operator 48 | csv_file_path = '1example.csv' 49 | 50 | # data = read_csv_file(csv_file_path) 51 | 52 | names = ( 53 | read_csv_file(csv_file_path) 54 | .then (remove_header) 55 | .then (extract_name_column) 56 | ) 57 | 58 | result = ( 59 | read_csv_file(csv_file_path) 60 | .then (remove_header) 61 | .then (extract_score_column) 62 | .then (convert_to_float) 63 | .then (calculate_average) 64 | ) 65 | 66 | 67 | if result.is_right and names.is_right: 68 | print(f"An average score of {', '.join(names.value[:-1])} and {names.value[-1]} is {result.value}") 69 | else: 70 | print(f"Error processing data: {result.value}") 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functional Programming Concepts in Python (PyCon Thatiland 2023 Talk) 2 | Welcome to my talk in PyCon Thatiland 2023 which is about Functional Programming Concepts in Python! 🐍 3 | 4 | ## Concepts 5 | - Arity: Understanding the number of arguments a function takes. 6 | - Function Composition: Combining two or more functions to create a new function. 7 | - Monads: Exploring this powerful design pattern in functional programming. 8 | - Single Responsibility Principle: Why a function should do one thing well. 9 | - Higher-Order Functions: Leveraging functions as first-class citizens. 10 | Feel free to explore the code examples and dive deeper into these functional programming concepts. Contributions, feedback, and questions are highly encouraged! Let’s embrace the functional paradigm and write Python code that’s both efficient and expressive. 11 | 12 | ## Getting Started 13 | - Clone this repository to get started. 14 | - Check out the slides/ directory for presentation materials. https://1drv.ms/p/s!Avm4-TXSbYZzgUuh0awwDYUmouvA?e=RGbM5Z 15 | - Check out the talk here in youtube. https://www.youtube.com/watch?v=mhxEm1aYvAY&t=712s 16 | - 17 | ## Contact 18 | If you have any questions or want to discuss functional programming further, feel free to reach out via GitHub issues or connect with me on discord "ruufimon". 19 | Happy coding! 🚀 20 | 21 | ## To install dependencies from requirements.txt 22 | pip install -r requirements.txt 23 | -------------------------------------------------------------------------------- /example.csv: -------------------------------------------------------------------------------- 1 | Name,Score,Grade 2 | Alice,90,A 3 | Bob,85,B 4 | Charlie,78,C 5 | David,92,A 6 | Eva,88,B 7 | Frank,79,C 8 | Grace,95,A -------------------------------------------------------------------------------- /ref1_toolz_currying.py: -------------------------------------------------------------------------------- 1 | from toolz import curry 2 | 3 | # Uncurried function 4 | def add_three_numbers(x, y, z): 5 | return x + y + z 6 | 7 | # Curried function using toolz.curry 8 | curried_add = curry(add_three_numbers) 9 | 10 | # Partial application 11 | add_five = curried_add(2)(3) 12 | 13 | # Full application 14 | result = add_five(5) 15 | 16 | print(result) # Output: 10 -------------------------------------------------------------------------------- /ref2_pymonad_compose.py: -------------------------------------------------------------------------------- 1 | from pymonad.either import Left, Right 2 | from pymonad.tools import curry 3 | # Function to parse an integer 4 | def parse_int(value): 5 | try: 6 | return Right(int(value)) 7 | except ValueError as e: 8 | return Left(f"Error parsing integer: {e}") 9 | 10 | # Function to square a number 11 | def square(value): 12 | return value ** 2 13 | 14 | # Function to convert a string to uppercase 15 | def string_template(value): 16 | return f"FINAL RESULT {value}" 17 | 18 | # Example usage 19 | result = Right('5').then(parse_int).then(square).then(string_template) 20 | 21 | print(result) 22 | -------------------------------------------------------------------------------- /ref3_pymonad_io.py: -------------------------------------------------------------------------------- 1 | from pymonad.io import IO 2 | 3 | def read_csv(filename): 4 | # IO Monad for reading a CSV file 5 | return IO(lambda: open(filename, 'r').read()) 6 | 7 | 8 | def process_csv(content): 9 | # IO Monad for processing the CSV content (you can replace this with your logic) 10 | lines = content.split('\n') 11 | headers = lines[0].split(',') 12 | return IO(lambda: {'headers': headers, 'data': [line.split(',') for line in lines[1:]]}) 13 | 14 | # Example program using IO Monad to read and process CSV 15 | filename = 'example.csv' 16 | csv_program = read_csv(filename).then(process_csv) 17 | 18 | # Execute the program 19 | result = csv_program.run() 20 | print("CSV Headers:", result['headers']) 21 | print("CSV Data:", result['data']) 22 | -------------------------------------------------------------------------------- /ref4_function_composition.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pymonad.tools import curry 3 | from typing import Final 4 | # Function to handle file reading 5 | def read_csv_file(file_path): 6 | try: 7 | with open(file_path, 'r') as csvfile: 8 | reader = csv.reader(csvfile) 9 | data = [row for row in reader] 10 | return data 11 | except FileNotFoundError as e: 12 | return None 13 | 14 | def remove_header(data): 15 | # print(f"remove_header: {data}") 16 | try: 17 | data = data[1:] 18 | return data 19 | except IndexError as e: 20 | return None 21 | 22 | @curry(2) 23 | def extract_column(column_index, data): 24 | try: 25 | column_values = [row[column_index] for row in data] 26 | return column_values 27 | except (ValueError, IndexError) as e: 28 | return None 29 | 30 | def convert_to_float(data): 31 | converted_data = [float(item) for item in data] 32 | return converted_data 33 | 34 | def calculate_average(column_values): 35 | if column_values is None or not column_values: 36 | return None, "Cannot calculate average due to empty or missing column" 37 | try: 38 | average = sum(column_values) / len(column_values) 39 | return average 40 | except ZeroDivisionError as e: 41 | return None 42 | 43 | def compose(*functions): 44 | def composed_function(x): 45 | result = x 46 | for func in functions: 47 | result = func(result) 48 | return result 49 | return composed_function 50 | 51 | # Data pipeline using function composition 52 | csv_file_path = 'example.csv' 53 | SCORE_COLUMN_INDEX: Final[int] = 1 54 | extract_score_columns = extract_column(SCORE_COLUMN_INDEX) 55 | 56 | data_pipeline = compose( 57 | read_csv_file , 58 | remove_header, 59 | extract_score_columns, 60 | convert_to_float, 61 | calculate_average 62 | ) 63 | 64 | # all_data = read_csv_file(csv_file_path) 65 | result = data_pipeline(csv_file_path) 66 | 67 | if result is not None: 68 | print(f"An average score of this class is {result}") 69 | else: 70 | print("Error processing data") 71 | -------------------------------------------------------------------------------- /ref5_pymonad_compose.py: -------------------------------------------------------------------------------- 1 | from pymonad.either import Left, Right 2 | 3 | def then(either, func): 4 | return either.bind(func) 5 | 6 | def to_int(s): 7 | try: 8 | return Right(int(s)) 9 | except ValueError: 10 | return Left("Error: Cannot convert to int") 11 | 12 | def add_five(x): 13 | return x + 5 14 | 15 | def double(x): 16 | return x * 2 17 | 18 | def to_string(x): 19 | return f"A{x}" 20 | 21 | # Example usage using >> 22 | result = ( 23 | to_int("10") 24 | .then(double) 25 | .then(add_five) 26 | .then(to_string) 27 | ) 28 | 29 | # Print the result or handle errors 30 | print(result) -------------------------------------------------------------------------------- /ref6_pymonad_lift.py: -------------------------------------------------------------------------------- 1 | from pymonad.either import Left, Right 2 | 3 | def extract_column(column_index, data): 4 | return Right(data).bind(lambda rows: Right(list(map(lambda row: row[column_index], rows)))) 5 | 6 | data = [ 7 | ['Name', 'Score'], 8 | ['Alice', '90'], 9 | ['Bob', '85'], 10 | ['Charlie', '92'] 11 | ] 12 | 13 | result = extract_column(1, data) 14 | 15 | if result.is_right: 16 | print(f"Column values: {result.value}") 17 | else: 18 | print(f"Error: {result.value}") 19 | -------------------------------------------------------------------------------- /ref8_try_monad.py: -------------------------------------------------------------------------------- 1 | from pymonad.either import Left, Right 2 | 3 | def convert_to_float(data): 4 | converted_data = [float(item) if item.isdigit() else None for item in data] 5 | if all(x is not None for x in converted_data): 6 | return Right(converted_data) 7 | else: 8 | return Left("Error: Unable to convert to float") 9 | 10 | # Example usage 11 | data = ["45", "33", "28", "55"] 12 | result = convert_to_float(data) 13 | 14 | print(result) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | awscli==1.31.10 2 | botocore==1.33.10 3 | colorama==0.4.4 4 | distlib==0.3.7 5 | docutils==0.16 6 | filelock==3.13.1 7 | jmespath==1.0.1 8 | pbr==6.0.0 9 | platformdirs==3.11.0 10 | pyasn1==0.5.1 11 | pylambda==0.2.1 12 | PyMonad==2.4.0 13 | python-dateutil==2.8.2 14 | PyYAML==6.0.1 15 | rsa==4.7.2 16 | s3transfer==0.8.2 17 | six==1.16.0 18 | stevedore==5.1.0 19 | toolz==0.12.0 20 | urllib3==2.0.7 21 | virtualenv==20.25.0 22 | virtualenv-clone==0.5.7 23 | virtualenvwrapper==4.8.4 24 | -------------------------------------------------------------------------------- /rf7_pymonad_open_file.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pymonad.either import Left, Right 3 | import os 4 | 5 | def open_csv_file(file_path): 6 | if os.path.isfile(file_path): 7 | file = open(file_path, 'r') 8 | return Right(file) 9 | else: 10 | return Left("Error: File not found") 11 | 12 | def read_csv_content(csvfile): 13 | return ( 14 | Right(csvfile) 15 | .bind(lambda file: Right(list(map(list, csv.reader(file))))) 16 | ) 17 | 18 | # Example usage: 19 | csv_file_path = 'example.csv' 20 | 21 | result = ( 22 | open_csv_file(csv_file_path) 23 | .bind(lambda file: read_csv_content(file)) 24 | ) 25 | 26 | if result.is_right: 27 | print(f"CSV content read successfully: {result.value}") 28 | else: 29 | print(f"Error: {result.value}") 30 | -------------------------------------------------------------------------------- /rf9_monad_manual.py: -------------------------------------------------------------------------------- 1 | class Maybe: 2 | def is_some(self): 3 | return self.__class__ == Some 4 | 5 | class Some(Maybe): 6 | def __init__(self, value): 7 | self.value = value 8 | 9 | def bind(self, func): 10 | return func(self.value) 11 | 12 | def __str__(self): 13 | return f"Some({self.value})" 14 | 15 | class Nothing(Maybe): 16 | def bind(self, func): 17 | return self 18 | 19 | def __str__(self): 20 | return f"Nothing" 21 | 22 | def divide(a, b): 23 | if b == 0: 24 | return Nothing() 25 | else: 26 | return Some(a / b) 27 | 28 | result = ( 29 | Some(10) 30 | .bind(lambda y: divide(y, 2)) 31 | .bind(lambda y: divide(y, 0)) # This in Nothing 32 | ) 33 | 34 | if result.is_some: 35 | print(f"Result is Right: {result}") 36 | else: 37 | print(f"Result is {result}") 38 | 39 | result = ( 40 | Some(10) 41 | .bind(lambda y: divide(y, 0)) 42 | .bind(lambda y: divide(y, 2)) 43 | ) 44 | 45 | if result.is_some: 46 | print(f"Result is Right: {result}") 47 | else: 48 | print(f"Result is {result}") 49 | 50 | result = ( 51 | Some(10) 52 | .bind(lambda y: divide(y, 0)) # This in Nothing 53 | .bind(lambda y: divide(y, 2)) 54 | ) 55 | 56 | # Expression: m >>= (\x -> f x >>= g) 57 | result_left = ( 58 | Some(10) 59 | .bind(lambda y: divide(y, 2).bind(lambda z: divide(z, 0))) 60 | ) 61 | 62 | # Expression: (m >>= f) >>= g 63 | result_right = ( 64 | Some(10) 65 | .bind(lambda y: divide(y, 2)) 66 | .bind(lambda z: divide(z, 0)) 67 | ) 68 | 69 | print(f"Result left: {result_left} Result right: {result_right}") --------------------------------------------------------------------------------