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 IntInt | Mul IntInt
I can turn that into a Functor by swapping the concrete values for a type variable:
data Opa= 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 for a and nest them as deep as I like. I can also write an interpreter for Op 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 pass Op to a function (liftFree) that turns it into a monad:
liftFree :: Functorf=> 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 on Op (Plus and Mul) into monadic operations that are allowed to use do-notation:
plus :: a -> a -> Free Op a
plus ab= liftFree (Plus a b)
mul :: a -> a -> Free Op a
mul ab= lift Free(Mul a b)
calculation :: Free Op Intcalculation=do
a <- plus 23
b <- mul a 5
plus a b
foldFree then allows me to pass it an interpreter function that evaluates my Op 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.
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.