8.7 KiB
title | description | date | draft | tags |
---|---|---|---|---|
Easy after you "get it" | 2025-01-25 | true |
I feel like there's a bunch of things out there which aren't "easy" until you get some crucial insight(s). Am I the first person to realize this? No, it's even probable that I read/heard this somewhere before and forgot. Either way I wanted to ramble about some things which I find easy but seem to not be and things which I find hard but think will become easy with some critical insight(s).
Easy for me and not(?) for people not in the loop
Recursion
Solving a problem with recursion can fundamentally be described with the following steps.
- Find a case where you know the solution without recursion and solve it. Often this will be the simplest case.
- Figure out a way to solve the problem if you know the solution for a slightly simpler case
Well... actually, while that is useful for solving arbitrary homework problems it's not how I use recursion in practice. Generally when I do recursion it's as a convenient way to perform a depth first search. Okay might lose some people with that so here's an example.
Example
A few months ago I participated in a coding competition hosted by my university's branch of ACM. One of the problems in the competition can be paraphrased as follows.
Write a function that receives 3 inputs,
obstacles
,minJump
andmaxJump
. Whereobstacles
is a string cosisting of 0s and 1s andminJump
andmaxJump
are positive integers. Write a function to determine if there is a sequence of integers where all the integers are greater than or equal tominJump
, less than or equal to maxJump and where making that sequence of skips (ie removing the first n chars from the string) will always have the first char of the string be "0" and will end with the final string being only a single char "0".
Solving this problem was pretty easy, it was this
# Scaffolding code provided wanted this function to return
# "T" or "F" instead of True or False
def escape(patch, minHops, maxHops):
if patch[0] == "1":
return "F"
if len(patch) == 1:
return "T"
for i in range(minHops, maxHops+1):
if i >= len(patch):
break
if escape(patch[i:],minHops, maxHops) == "T":
return "T"
return "F"
All this does is
- check if we're in a known failure state and return appropriately
- check if we're in a known success state and return appropriately
- check to see if each of the jump lengths we can make can reach a success state and if so return appropriately
- if none of the lengths match return that this is a failure
I don't know if the above insight or this example helps anyone or not.
Haskell Monads
Not to be confused with Category Theory Monads. There's a running joke about how everyone who figures out Haskell Monads writes a tutorial on them, potentially including their own (probably incorrect) analogy. Fundamentally Haskell monads have 3 properties.
- They have an associated type, I'll be using
m<t>
to represent a typem
with an associated typet
- They have a mechanism to turn a function with an input of type
a
and output of typeb
into a function with an input of typem<a>
and output of typem<b>
(derived from them being Haskell Functors, not to be confused with Category Theory Functors or ML Functors) - They have a mechanism to turn a value of type
m<m<t>>
into a value of typem<t>
If you're staring at Haskell documentation and confused on that third point, here's a definition of a flatten function.
flatten :: (Monad m) => m (m a) -> m a
flatten v = v >>= id
Of course none of the above is the insight that I'm referring to, it's just to give the formally correct definition. The insight can't be written into words unfortunately so instead I'm going to give a description of my intuition and maybe it helps, maybe it doesn't.
Haskell Monads are just the shape you appease to be allowed to write procedural looking code via do
syntax.
Yeah that's it, they aren't really useful in other languages unless you want to generalize a procedural algorithm so it applies to data across many shapes.
Example(s)
IO
IO
is a special case in Haskell so it's worth understanding what it is separate from Monads generally.
An IO
object is basically a program that you can run which results in some value.
main
is just a special name for the program that gets run when you run the executable.
The way a function is made to work on IO
values is to just make it so the resulting IO
value is now a program which takes the prior result and shoves it into the function you gave to give a new result.
Flattening is just running the outer program to generate the inner program.
For optimization reasons that's not how things actually work (I hope) but I don't want to dig into the compiler to give a more accurate answer.
functions
Yeah, normal functions are monads, specificaly the associated type is the return/output type. This fact is a counter example to the whole "Monads are types which wrap a value" thing btw. The way you do the whole function conversion thing is just function composition. The way you do flattening is
flatten f = \a -> (f a) a
that.
lists
A funny funky monad example which basically turns the do
syntax into for loops.
-- basically turns [1,2] and [True, False] into
-- [(1, True), (1, False), (2, True), (2, False)]
myProduct :: [a] -> [b] -> [(a,b)]
myProduct l1 l2 = do
elem1 <- l1
elem2 <- l2
-- return is just a function to turn a non-monad value into a monad value
return (elem1, elem2)
Changing functions is just a map
and flattening is just well... exactly what you think when you flatten a 2d list into a 1d list.
Writer
Writer is interesting, I only got it (as in understood how it's useful) fairly recently (read as the day this was initially written).
Writer has 2 associated types.
The second type is the associated type for it's monad.
However the first type is interesting.
The first type has to fit a specific interface, that interface is the Monoid
interface.
The Monoid
interface requires that a type have 2 properties.
First it must have a way of combining 2 values of that type.
Second it must have a value where when you use the combination of that value with any other value it's equivalent to the identity.
Examples being the integers via multiplication with the identity being 1 and the integers again via addition with the identity being 0.
Now when I say "get it" I mean, realized how it can be useful. The initial spark of this was this blog post, albeit the presentation is difficult to process. Before that post my main exposure to Writer was via Learn You a Haskell for Great Good. This exposure communicated a technically correct explanation but did a poor job motivating the usefulness.
So, how is Writer useful?
Well, entirely for using Haskell's do
notation.
You see sometimes you have a value that you want to construct and you don't necessarily want to have all the data needed to build it in one place in the source code or want to configure things on multiple lines.
In procedural programming this is what the builder pattern is for.
let my_foo = FooBuilder::new()
.bar()
.baz()
.finish();
But it isn't obvious how you'd do this in Haskell without pain.
-- naive method
let foo_builder = FooBuilder
let foo_builder' = bar foo_builder
let foo_builder'' = baz foo_builder
let my_foo = finish foo_builder''
But we can have FooBuilder
as a monoid and do the above with less pain.
-- assuming that FooBuilder is a newtype of Writer PartialFoo
-- or something like that with the prior functions being instances
-- with PartialFoo values modified appropriately
-- finish does runWriter, pulls the second element of the tuple
-- and if we have a monoid type specifically for constructing a
-- Foo like this it converts it to Foo
let my_foo = finish $ runWriter $ do
FooBuilder
bar
baz
So yeah it's cool. Intermediary Writer
s having an associated value with them is just a side benefit. Here's an example of the finish function for a Writer (Product a)
that I wrote while playing with Writer
.
finish :: (Num a) => Writer (Product a) b -> a
finish = getProduct . snd . runWriter
Not easy for me, still need the insight
Finding a job
This one