- passing value as an argument: reference vs value
- reference: function has access to original value and can modify it
- value: function only has access to copy, changes to copy within function don't affect the original
- higher-order function: function that does at least one of the following:
- Takes one or more functions as arguments
- Returns a function as its result
- memoization: caching the result of a computation to avoid computing it again in the future
- it's a tradeoff between memory and speed
- referential transparency: function can be replaced by its value without changing the program's behavior, without side effects
example of referential transparency:
def add_nums(a, b):
return a + bfunctions that are treated as first-class citizens
- first-class citizens are entities that can be:
- Created at runtime
- Assigned to a variable or data structure
- Passed as a parameter to a function
- Returned as the result of a function
pure functions have the 2 following properties:
- deterministic: always returns the same value given the same arguments
- no side effects: the function does not alter any state or interact with the outside world
- no modifying global variables
- no input/output operations, etc
- exceptions are considered side effects
- in FP, represent error as data (e.g. the ParseError class), rather than raising exceptions
modifying a function's input and/or output to create a new function
- higher-order function: a function that receives one or more functions as arguments or returns a function as a result
def multiply(x, y):
return x * y
def add(x, y):
return x + y
# self_math is a higher order function
# input: a function that takes two arguments and returns a value
# output: a new function that takes one argument and returns a value
def self_math(math_func):
def inner_func(x):
return math_func(x, x)
return inner_func
square_func = self_math(multiply)
double_func = self_math(add)
print(square_func(5)) # prints 25
print(double_func(5)) # prints 10small, one-line functions that do not require formal definition
- in Python, use the lambda keyword to create a lambda function
square = lambda x: x ** 2
print(square(5)) # Output: 25functions that have access to the parent scope, even after the parent function has closed
properties of closures:
-
can remember and access variables and arguments of its outer function even after that function has finished execution
-
often used to keep state from function call to function call
-
in python, use
nonlocalkeyword to access variables from parent scope- don't use
nonlocalwhen you are modifying a variable that is immutable (lists, dictionaries or sets)- you can use
nonlocalif you are reassigning the immutable variable
- you can use
- don't use
function makeCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2Note
in the code above, the inner function returned by makeCounter is the closure
in the code below, the closure is word_count
def word_count_aggregator():
count = 0
def word_count(doc):
nonlocal count
words = doc.split()
count += len(words)
return count
return word_countexample of closure without using nonlocal keyword
def new_collection(initial_docs):
docs = initial_docs.copy()
def add_doc(new_word):
docs.append(new_word)
return docs
return add_doc
# Create a new collection
add_doc = new_collection(["doc1", "doc2", "doc3"])
# Test the closure
print(add_doc("doc4"))
# Output: ['doc1', 'doc2', 'doc3', 'doc4']
print(add_doc("doc5"))
# Output: ['doc1', 'doc2', 'doc3', 'doc4', 'doc5']convert single function that receives multiple arguments into multiple functions that each receives a single argument
def add(x, y, z):
return x + y + z
def curried_add(x):
def add_y(y):
def add_z(z):
return x + y + z
return add_z
return add_y
# Using the curried function
result = curried_add(1)(2)(3)
print(result) # Output: 6
# We can also partially apply the function
add_one = curried_add(1)
add_one_and_two = add_one(2)
final_result = add_one_and_two(3)
print(final_result) # Output: 6- why would I ever want to do this?
- flexibility, by breaking down a multi-argument function into smaller, single-argument functions, you can:
- Use the function in different contexts, such as with map, filter, or reduce
- Create higher-order functions that take other functions as arguments
- Easily compose functions together to create more complex operations
- flexibility, by breaking down a multi-argument function into smaller, single-argument functions, you can:
design pattern that allows you to add functionality to a class without changing it
- structural design pattern
- syntatic sugar for higher-order functions
- higher-order function: function that does at least one of the following:
- takes one or more functions as arguments
- returns a function as its result
# prefix is the decorator
def prefix(func_to_decorate):
def wrapper():
return "Hello " + func_to_decorate()
return wrapper
# printer is the same as prefix(printer)
@prefix
def printer():
return "World"
print(printer()) # Output: Hello World- explanation:
def prefix(func):: function that receives another functionfunc_to_decorateas input@prefix: equivalent toprinter = prefix(printer)- passes the
printer()function as argument toprefix() - also renames
prefix(printer)toprinter()
type that can hold different kinds of values, but only one at a time
- also known as tagged unions or variant types
- lambda functions: anonymous functions that can have any number of arguments, but only one expression
lambda x: x + 1
- nonlocal var: used in closures to access variables from parent scope
- map(): applies a function to each item in an iterable
squared_numbers = list(map(lambda x: x**2, [1, 2, 3, 4, 5]))
- filter(): filter elements from an iterable based on a function that returns
TrueorFalse- returns a new iterable containing only the elements for which the function returns
True - use
list()to convert the iterator to a list
- returns a new iterable containing only the elements for which the function returns
- reduce(): applies a cumulative operation to reduce an iterable to a single value
- arguments that allow a function to accept a variable number of arguments
*args: collects positional arguments into a tuple**kwargs: collects keyword/named arguments into a dictionary
numbers = [1, 2, 3, 4, 5, 6]
# Using a lambda function to filter out even numbers
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers) # Output: [2, 4, 6]
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# Using a lambda function to calculate the product of all numbers
product = reduce(lambda x, y: x * y, numbers)
print(product) # Output: 120
def print_arguments(*args, **kwargs):
print(f"Positional arguments: {args}")
print(f"Keyword arguments: {kwargs}")
print_arguments("hello", "world", a=1, b=2)
# Positional arguments: ('hello', 'world')
# Keyword arguments: {'a': 1, 'b': 2}