It has classes
Consider this: if they have been designed correctly, they are working classes.
Procedural programming is counter-revolutionary because it has a global state.
It's just a monoid in the category of endofunctors, what's the problem?
Understanding abstract algebra is the reading theory of functional programming
Amateur! Category theory is the true way (or homotopy type theory if you wanna get wild).
TBH I consider category theory to be a branch of algebra. At least that's the class where it was first formally presented to me.
in theory, yes, but pedagogically it makes more sense as a branch of algebra
Man everytime I asked my Programming Languages professor what a monad was he would make punting motion.
It's really just that. A monad is really just a type that admits lawful definitions of
map
(so it's a functor),pure
andap
(so it's an applicative functor) andbind
(orflatmap
, though depending on your language this may be too powerful compared to a classicalbind
, cf. Scala). There's a lot of cool stuff that falls out of that, but if you understand "given a value in some context (of typem a
, say) and a function that lifts values into that context (possibly at a different type, i.e.,a -> m b
), I can produce a value in the same context (m b
, again, possibly at a different type)", there's not much else to look into. The functor, applicative, and monad laws dictate some of the operational characteristics of such definitions on a particular type, but that's really only relevant if you're making your own.Lifting is just a fancy word for (in this specific case, anyway) taking a concrete value and putting it some kind of computational context.
I have a value
1 :: Int
. There are a lot of contexts I can put that concrete value in! That is, I can make lots of values of typem Int
that represent different views on the same value.Let's start by looking at how functors (types that admit a lawful definition of
map
) lift values.map :: Functor f => (a -> b) -> (f a -> f b) -- equivalently (and more usually) (a -> b) -> f a -> f b
Normally, this is explained by saying "given a function
a -> b
and a value of typef a
for some functorf
, apply the function on eacha
inf a
and give me the result wrapped up inf
". Another way of explaining it (which I've written the type for preferentially) is "given a functiona -> b
, give me a functionf a -> f b
. We're lifting the entire function into whatever our functorial context is.Examples:
1 :: Int map (+1) :: Functor f => f Int -> f Int map show :: Functor f => f Int -> f String Just 1 :: Maybe Int map (+1) (Just 1) = Just 2 :: Maybe Int map show (Just 1) = Just "1" :: Maybe String [1] :: [Int] map (+1) [1] = [2] :: [Int] map show [1] = ["1"] :: [String]
Skipping directly up to monads, we really just need
pure :: Monad m => a -> m a
(a function which does nothing but put a value into a monadic [technically applicative] context) andbind :: Monad m => m a -> (a -> m b) -> m b
. To keep it simple, we'll just reproducemap
.-- We compose our function with pure to get their types into a shape that bind will understand incM :: Monad m => (Int -> Int) -> (Int -> m Int) incM = pure . (+1) showM :: Monad m => (Int -> String) -> (Int -> m String) showM = pure . show -- worth noting, Just 1 and [1] are both the same as pure 1 :: f Int with f fixed at Maybe and [], respectively bind (Just 1) incM = Just 2 bind (Just 1) showM = Just "1" bind [1] incM = [2] bind [1] showM = ["1"] bind (bind (Just 1) incM) showM = Just "2" -- bind (Just 2) showM bind (bind [1,2,3] incM) showM = ["2", "3", "4"] -- bind [2,3,4] showM
So you can see that this is a way of getting a value "out of" some context (like pulling the value of a Maybe or all the values out of a list), doing some sort of transformation on it, then wrapping it back up in the initial context; it also lets you chain these transformations, finally wrapping everything back up when you're done.
flatmap
is called that becausebind
for the list monad is exactly "map a function over this list and then flatten the list".bind ([[1,2,3],[4,5,6]]) id = [1,2,3,4,5,6] bind ([[1,2,3],[4,5,6]]) (map (+1)) = [2,3,4,5,6,7]
nah, they just pop up in a lot of contexts like futures/promises, errors, optionality, etc.. there's also a couple of neat tricks where you take a functor that represents a set of operations you'd like to provide and use an automatic construction to create a (free) monad out of it, thereby getting an interpreter for your DSL with no extra work.
I'll attempt a more thorough explanation, let me know if this makes any sense.
so I've got a type that represents some operations I want to provide:
data Op = Plus Int Int | Mul Int Int
I can turn that into a Functor by swapping the concrete values for a type variable:
data Op a = Plus a a | Mul a a
I'm doing this because I want to be able to compose these operations together - I should be able to freely sequence them however I like. so I can pass
Op
values in fora
and nest them as deep as I like. I can also write an interpreter forOp
values by breaking it down by cases and doing the obvious thing:eval :: OP Int ->Int eval (Plus a b) = a + b eval (Mul a b) = a * b
I give that type the obvious, dumb
Functor
instance, nothing special (exercise left for the reader). then, I can passOp
to a function (liftFree
) that turns it into a monad:liftFree :: Functor f => f a -> Free f a
(I'm going to skip the actual definition of
Free
as it's just a type out of the standard library)so I can use
liftFree
to turn the basic operations onOp
(Plus
andMul
) into monadic operations that are allowed to use do-notation:plus :: a -> a -> Free Op a plus a b = liftFree (Plus a b) mul :: a -> a -> Free Op a mul a b = lift Free (Mul a b) calculation :: Free Op Int calculation = do a <- plus 2 3 b <- mul a 5 plus a b
foldFree
then allows me to pass it an interpreter function that evaluates myOp
and turn it back into a regular value (like the obvious one I mentioned previously).foldFree :: Functor f => (f r -> r) -> Free f r -> r (foldFree eval calculation) :: Int
BUT because I can pass any interpreter I want, I've decoupled evaluation from the definition of the actions I'd like to take. so I could, instead of using an interpreter that calculated the final value, pass in one that pretty-printed it instead, or does a dry-run, etc..
prettyPrint :: Op String -> String foldFree prettyPrint (fmap show calculation)
so I can define actions that do a bunch of crazy IO stuff when called with a regular interpreter and run them instead with an interpreter that just sequences the operations and their arguments such that I can unit test that code without doing a bunch of mocking, etc.
I use a version of this trick wherever I can get away with it, even where I can't actually give a monad instance (like rust), because the decoupling alone is super powerful.
Along this line, I want to state that nobody has permission to downvote any of my posts.
Weakly typed languages are the way to go. Why should I have to declare a data type? Just let everything be a string and let the language figure out the context!
the only good hierarchy is a filesystem
Functional; haskell and AGDA are the way to go