by Elliot Cameron / 3noch
Follow along
3noch.github.io/fp.py
Note: Alt+Click on an element to zoom in.
Ask questions!
ref
FP is starting to gain momentum
type Seconds = Int
secs :: Int -> Seconds; secs = (* 1000000)
wait :: Seconds -> IO (); wait = threadDelay . secs
schedule :: Seconds -> IO () -> IO ThreadId; schedule s a = forkIO $! wait s >> a
(<>) :: ByteString -> ByteString -> ByteString; (<>) = B.append
(//) :: a -> (a -> b) -> b; x // f = f x
(|>) :: IO () -> IO () -> IO (); a |> b = forkIO a >> b
infixr 0 =>>; (=>>) :: Monad m => m a -> (a -> m b) -> m a
a =>> f = do r <- a; _ <- f r; return r
type ErrorIO = IO
att :: IO a -> IO (Maybe a); att a = tryWith (const $! return Nothing) (Just <$> a)
tryRun :: IO () -> IO (); tryRun a = tryWith (\x -> do print x; wait 2) a
(???) :: ErrorIO a -> [IO a] -> IO a; e ??? as = foldr (?>) e as
where x ?> y = x `X.catch` (\(_ :: X.SomeException) -> y)
ref
\[ \begin{aligned} \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\ \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ \nabla \cdot \vec{\mathbf{B}} & = 0 \end{aligned} \] |
![]() |
he knows something I don't | he doesn't know what he's doing (and neither do I) |
learn | probably only if you're already used to something else* |
read | same deal* |
test | to the contrary, FP tends to be much easier to test* |
debug | to the contrary, expressions are the easiest types of things to debug* |
* courteously suspend disbelief if possible
To become a competent realist you must start with ideals.
- Anonymous
Alan Turing: Invented the "Turing Machine" (1936)
Alonzo Church: Formulated λ-calculus (1928-1929) and Turing's Ph.D. Advisor
![]() |
For every input there is precisely one output. | |
The Vertical Line Test |
An expression is said to be referentially transparent if it can be replaced with its value without changing the behavior of a program (in other words, yielding a program that has the same effects and output on the same input). The opposite term is referential opaqueness.
While in mathematics all function applications are referentially transparent, in programming this is not always the case. The importance of referential transparency is that it allows the programmer and the compiler to reason about program behavior. ref
ord
function
ord("P")
can be replaced with 80
in every case, and vice versa, without any change to a program's behavior.
|
Python Functions? Yes
Math Functions? Yes |
|
Python Functions? Yes
Math Functions? No |
global_state = 5
def get_next_state(steps):
global global_state
global_state += steps # <-- NOPE!
return global_state
def save_file(contents):
with open("file.txt", "w") as file_handle:
file_handle.write(contents) # <-- NOPE!
def change_arg(arg):
arg = arg + 1 # <-- NOPE!
def hello_world():
print("Hello world") # <-- NOPE!
Oh but it is. Turing and Church proved that Turing machines and λ-calculus are equivalent models of computation!
More on that later.
"First-class" means you can pass it around just like anything else (numbers, strings, objects).
# map takes another function
last_names = map(lambda full_name: full_name.partition(" ")[2], names)
# filter does too
capitalized_names = filter(lambda name: name[0] == name[0].upper(), last_names)
def compose(f, g): # takes two functions
return lambda x: f(g(x)) # gives a new function
# no
age = 30
if date > birth_date:
age += 1
# yes
age = 30
corrected_age = age + 1 if date > birth_date else age
Some algorithms actually go faster with immutable structures,
especially ones that can run in parallel.
* Available to Python through a library.
the_sum = 0
for i in range(10):
the_sum += i # NOPE!
def get_sum(values, start=0):
if not values:
return start
the_rest = values[1:] # why is this ok?
return get_sum(the_rest, start + values[0])
the_sum = get_sum(range(10)) # 45
# or
get_sum = lambda values, start=0: get_sum(values[1:], start + values[0]) if values else start
the_sum = get_sum(range(10)) # 45
# or
the_sum = sum(range(10)) # 45
Python doesn't implement any optimizations for recursion, so recursion bloats your stack really fast, runs very slowly, and can easily hit the recursion limit.
get_sum(range(1000000)) # takes forever
sum
is an example of a particular type of loop: fold.
map(lambda x: x * 2, range(10)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
filter(lambda x: x < 5, range(10)) # [0, 1, 2, 3, 4]
reduce(lambda x, y: x + y, range(10), 0) # 45
# mixup
map(lambda x: x * 2, filter(lambda x: x < 5, range(10))) # [0, 2, 4, 6, 8]
# or
[x * 2 for x in range(10) if x < 5] # [0, 2, 4, 6, 8]
We can rely on these tools to skip the need for explicit recursion.
def add_them(a, b):
return a + b
add_them(2)
# TypeError: add_them() takes exactly 2 arguments (1 given)
from functools import partial
partial(add_them, 2)
# <functools.partial at 0x322bf48>
partial(add_them, 2)(3) # 5
Currying treats every function as a single-argument function.
Given its one input, it returns a new function that takes the next one.
Currying is very nice, but not essential.
In Python, we can use libraries to give us some of these features.
Push as much "impure" code into the crust, including:
Avoiding mutation essentially forces you to build parametric components,
i.e. pieces that require their dependencies before they can be used.
Don't confuse your experience of functional programming in Python with functional programming in general.
Python's design is not intentionally supportive of FP, so the experience suffers to some degree.
fn.py is a great start.
Go forth and code.