Notes about Functional Programing with Julia
Notes about Functional Programing with Julia
I am writing here some general ideas that were taken from some sources like boot.dev about functional programming, many of these sources were written in Python and I just rewrote Julia in most of the cases. Because Julia is a program more suitable for FP I considered a good exercise in the long run to translate the concepts that I am learning about this paradigm.
Index
\toc
What is Functional Programming
- compose functions instead of mutating states,
- What you want to happen rather than how you want to happen
Inmutability
Once the Value is created it can’t be changed, this can be easier to debug
Declarative
Functional aims to be declarative rather than imperative
Math Style
imperative style
function get_average(nums)
total = 0
for num in nums
total += num
end
return total / length(nums)
end
functional style
function get_average(nums)
return sum(nums) / length(nums)
end
In general to make a bit more functional style, we should avoid loops and mutate any variable
Classes encourage you to think about the world as a hierarchical collection of objects. Objects bundle behavior, data, and state together in a way that draws boundaries between instances of things, like chess pieces on a board.
Functions encourage you to think about the world as a series of data transformations. Functions take data as input and return a transformed output. For example, a function might take the entire state of a chess board and a move as inputs, and return the new state of the board as output.
OOP is not quite the opposite with FP, but the 4 pillars of the first one (abstraction, encapsulation, inheritance and polymorphism) inheritance is the one that can produce changes in classes, so break the rule of inmutability in FP
Functions are First Class
We can treat functions as values
function add(x,y)
return x+ y
end
addition = add
println(addition(2,7)
# print 9
Anonymous Functions
Basically functions that doesn’t have name, similar like python use lambda functions
function filter_var(df, value)
return filter!(row -> row.colum != value , df)
end
in the last case row -> row.colum != value
is an anonymous function
Higher Order Functions
In the case that the programming language threat functions like any other variable, so Functions are first class then we can pass functions as an arguments to other functions.
function square(x)
return x * x
end
function my_map(func, arg_list)
result = []
for i in arg_list
push!(result, func(i))
end
return result
end
squares = my_map(square, [1, 2, 3, 4, 5])
println(squares)
# [1, 4, 9, 16, 25]
In the last case my_map() is a higher order function
Map, Filter and Reduce
Map, filter and reduce are three typical examples of Higher order functions that are quite useful, for a map function you need an iterable (An object capable of returning its members one at a time.) and a function, and apply the function to all the elements of this iterable
function say_hello(name)
return "Hello " * name
end
list_names = ["Chris", "Hector", "Benito"]
map(say_hello, list_names)
# ["Hello Chris, "Hello Hector", "Hello Benito"]
Filter was already shown in an example before, but basically takes an iterable, a function and return also an iterable that is a subset of the original.
Finally the Reduce function take same arguments but now it reduce everything to a single value, like the following example
function add(sum_so_far, x)
prinln("sum_so_far: $sum_so_far, x: $x")
return sum_so_far + x
end
numbers = [1, 2, 3, 4]
sum = reduce(add, numbers)
# sum_so_far: 1, x: 2
# sum_so_far: 3, x: 3
# sum_so_far: 6, x: 4
# 10
println(sum)
# 10
This higher order functions allow us to write functions without using loops in some cases avoiding stateful iterations and mutation of variables.
Pure Functions
Pure functions has to accomplish two properties:
- They always return the same value given the same arguments.
- Running them causes no side effects
function findMax(nums)
max_val = -Inf
for num in nums
if max_val < num
max_val = num
end
end
return max_val
end
Let’s compare with this other case
# instead of returning a value
# this function modifies a global variable
global_max = -Inf
function findMax(nums)
global global_max
for num in nums
if global_max < num
global_max = num
end
end
end
In the first case we keep a function which clearly define an input and return and output while in the second case we produce a global variable that change the state of this (breaking the rule of inmutability) and does not return anything but our global variable has changed. In summary, pure functions:
- Return the same result if given the same input, so they are deterministic (which no randomness is involved in the development of future states of the system.). Also there is the term referentially transparent
- Do not change the external state of the program. For example, they do not change any variables outside of their scope.
- Do not perform any I/O operation like printing, accessing to data via HTTP or reading files.
Reference and Value
There are functions that allow you to pass by references, this are mutable, you can see this when appending values in a list. In this case the function has access to the original value. For other side a function that receive variables as values are receiving copy of the original and do not attemt to change the original (inmutability), you can do in Julia using deepcopy(var)
to create copies
Pass by Reference Impurity
To avoid side effects we can create copies of the variables inside of a function without changing any variables that is out of the scope (this includes the input of the function)
function remove_format(default_formats, old_format)
new_formats = deepcopy(default_formats)
new_formats[old_format] = false
return new_formats
end
With this we avoid mutating any input or global variable making it easier to debug and test.
Input and Output
While I/O operations are part of impure functions, these are necessaries (or our program is completely useless) so It tries to use only when is neccesary.
NO-OP
Functions that does nothing, or better said doesn’t return anything, probably are impure functions
function square(x)
x * x
end
That function doesn’t do anything, but also there are functions that perform some side effect:
y = 5
function add_to_y(x)
global y
y += x
end
add_to_y(3)
# y = 8
Even the print() function technically has an impure side effect
Memoization
This is storing a copy of a result a computation so we don’t have it to compute it again in the future, it holds a trade-off between memory and speed. This only can be achieved with pure functions.
const fibmem = Dict{Int,Int}()
function fib(n)
get!(fibmem, n) do
n < 3 ? 1 : fib(n-1) + fib(n-2)
end
end
Recursion
Function that define itself, for example the classic factorial. This kind of functions are quite useful for unknown tree structure
function factorial_rec(x)
if x == 0
return 1
else
return x * factorial_rec(x - 1)
end
end
julia> factorial_rec(0)
1
julia> factorial_rec(3)
6
A recursive function should have some dangerous edge case that deserve attention:
- Requires base case to avoid infinite loops.
- Each function call requires a bit of memory, so in long trees structures can cause a stack overflow and will crash your program
- In some languages recursion is slow, like python where is even slower than loops. Use of Tail call Optimizations can deal with that
Function Transformations
Specific type of Higher order functions that receive functions as input and return functions as output, special for some cases of code reusability
function multiply(x, y)
return x * y
end
function add(x, y)
return x + y
end
# 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
function self_math(math_func)
function inner_func(x)
return math_func(x, x)
end
return inner_func
end
square_func = self_math(multiply)
double_func = self_math(add)
println(square_func(5))
# prints 25
println(double_func(5))
# prints 10
Closures
A closure is a function that references variables from outside its own function body. The function definition and its environment are bundled together into a single entity so a closure can change the value outside its body
julia> function make_adder(amount)
function add(x)
return x + amount
end
end;
julia> add_one = make_adder(1);
julia> add_two = make_adder(2);
julia> 10 |> add_one
11
julia> 10 |> add_two
12
In the case of Julia, generate global variables can cause Type Instability and there are some discussions about avoiding closures when performance is required, However that doesn’t mean that using closures should be avoided completely, a lot of discussions are here also interesting content here
Naturally if a function can change a a non local variable then is not a pure function, so many cases closures are not pure functions because they can mutate outside of their scope and have side effects.
Notice that also there are concept of Decorators in some languages like Python, that are just syntactic sugar for higher order functions
Currying
Function currying is a specific kind of function transformation where we translate a single function that accepts multiple arguments into multiple functions that each accept a single argument.
This is a normal function without currying
function sum(a,b)
return a+b
end
With currying
function sum(a)
function inner_sum(b)
return a + b
end
return inner_sum
end
With this option now we can return a function as a value (inner_sum) and change it’s signature to make it conform to specific parameter
Wrapping up
These are just basic ideas about functional programming, there are more concepts to deal with, but at least here is an starting point for people like me who is not a cs person…