Bartosz Milewski's neat defunctionalization talk
Bartosz kept a cool talk just recently. He also published a blogpost about it, "Defunctionalization and Freyd's Theorem".
In the video Bartosz tells how to defunctionalize arbitrary functions. This operation reveals the contents that these operations hold in their return stack during the evaluation. Bartosz also presents how he reasoned about this technique in category theory. I didn't understand much of the category-theory side, except that an infinite sum forming a state space for a function was brought up.
The video is part of the Haskell Love Conference, and what I can gather up from what's popping up in my Youtube feed there have been big names and interesting presentations. Outright I recognize and remember Wadler, Jones, Elliot, Milewski.
I've learned important things about category theory through the video lectures Bartosz Milewski has in his youtube channel. It took me few tries before I figured it out and Bartosz' lectures helped a lot.
Now, lets defunctionalize a program like Bartosz did in the video. We're going to write a program in Purescript, here's the import declarations.
import Data.Either (Either(..))import Data.Foldable (for_)import Data.Generic.Rep (class Generic)import Data.Generic.Rep.Show (genericShow)import Data.Maybe (Maybe(..))import Data.Tuple (Tuple(..))import Effect (Effect)import Effect.Ref (Ref, new, read, write)import Prelude (class Show, Unit, bind, discard, pure, show, unit, (+), (-), (<>), (>>=))import Web.DOM.Document as Dimport Web.DOM.Element as Eimport Web.DOM.HTMLCollection as Cimport Web.DOM.Node as Nimport Web.Event.Event (Event, EventType(..))import Web.Event.EventTarget (addEventListener, eventListener)import Web.HTML.HTMLDocument (HTMLDocument, toDocument)import Web.HTML (window)import Web.HTML.Window as W
I take an example only slightly different from what Bartosz used. Here's a fibonacci function, written in Purescript. I'm using Purescript because it's been designed to use Javascript as a backend and it's easy to present the finished program this way.
fibonacci :: Int -> Intfibonacci 0 = 0fibonacci 1 = 1fibonacci n = fibonacci (n-2) + fibonacci (n-1)
Bartosz perhaps picked a better example. Now you'll keep thinking about how there's a better algorithm to evaluating fibonacci than the recursive one presented here.
Like in Bartosz' example, we're recursively calling fibonacci and they are not tail calls. We'll transform the program such that they become tail calls.
fibonacci' :: Int -> Intfibonacci' n = fibonacciCont n (\v -> v)fibonacciCont :: forall k. Int -> (Int -> k) -> kfibonacciCont 0 k = k 0fibonacciCont 1 k = k 1fibonacciCont n k= fibonacciCont (n-2) (\v ->fibonacciCont (n-1) (\w ->k (v + w)))
He additionally split it into functions like this:
fibonacci'' :: Int -> Intfibonacci'' n = fibonacciCont' n fibonacciP0fibonacciCont' :: forall k. Int -> (Int -> k) -> kfibonacciCont' 0 k = k 0fibonacciCont' 1 k = k 1fibonacciCont' n k= fibonacciCont' (n-2) (fibonacciP1 {n, k})fibonacciP0 :: forall k. k -> kfibonacciP0 x = xfibonacciP1 :: forall k. {n :: Int, k :: (Int -> k)} -> Int -> kfibonacciP1 {n, k} v = fibonacciCont' (n-1)(fibonacciP2 {v, k})fibonacciP2 :: forall k. {v :: Int, k :: (Int -> k)} -> Int -> kfibonacciP2 {v,k} w = k (v + w)
The split into functions allow determining what each function closes into itself. It allows to defunctionalize the whole thing.
data Kont = Done| Next {n :: Int, k :: Kont}| Conc {v :: Int, k :: Kont}fibonacci''' :: Int -> Intfibonacci''' n = fibonacciCont'' n DonefibonacciCont'' :: Int -> Kont -> IntfibonacciCont'' 0 k = apply k 0fibonacciCont'' 1 k = apply k 1fibonacciCont'' n k = fibonacciCont'' (n-2) (Next {n,k})apply :: Kont -> Int -> Intapply Done s = sapply (Next {n, k}) v = fibonacciCont'' (n-1) (Conc {v,k})apply (Conc {v, k}) w = apply k (v + w)
Now, what can you do with this? Well functions are generally a bit hard to serialize and this transformation turns them into simple data structures. Lets first derive the show instance, I did find it difficult to locate how to use Purescript's generics. The up-to-date documentation seem to be located in the generics-rep. The pursuit links to the repository of each module.
derive instance genericKont :: Generic Kont _instance showKont :: Show Kont whereshow x = genericShow x
Now we're going to implement the fibonacci for the fourth time.
fibonacci'''' :: Int -> (Tuple Kont Int)fibonacci'''' n = fibonacciCont''' n DonefibonacciCont''' :: Int -> Kont -> Tuple Kont IntfibonacciCont''' 0 k = Tuple k 0fibonacciCont''' 1 k = Tuple k 1fibonacciCont''' n k = fibonacciCont''' (n-2) (Next {n,k})apply' :: Kont -> Int -> Either Int (Tuple Kont Int)apply' Done s = Left sapply' (Next {n, k}) v = Right (fibonacciCont''' (n-1) (Conc {v,k}))apply' (Conc {v, k}) w = Right (Tuple k (v + w))
That's it. The only thing we need is bit of a driver code to show the results. I'd guess there's a better way to do this in Purescript, but I haven't learned it yet.
main :: Effect Unitmain = dow <- windowd <- W.document wloadEvent <- eventListener (onLoadEvent d)addEventListener (EventType "load") loadEvent false (W.toEventTarget w)onLoadEvent :: forall a. HTMLDocument -> a -> Effect UnitonLoadEvent d e = dodialogs <- D.getElementsByClassName "fibonacci-dialog" (toDocument d) >>= C.toArrayfor_ dialogs setupDialogsetupDialog :: E.Element -> Effect UnitsetupDialog element = domaybe_output <- E.getElementsByClassName "output" element >>= C.item 0maybe_button <- E.getElementsByClassName "next" element >>= C.item 0case Tuple maybe_output maybe_button ofTuple (Just output) (Just button) -> dolet setTexts a b = doN.setTextContent a (E.toNode output)N.setTextContent b (E.toNode button)setTexts "Program output goes here" "fibonacci 5"buttonRef <- new ((Tuple 5 Nothing) :: Tuple Int (Maybe (Either Int (Tuple Kont Int))))clickEvent <- eventListener (onButtonEvent buttonRef setTexts)addEventListener (EventType "click") clickEvent true (E.toEventTarget button)pure unitTuple _ _ -> pure unitonButtonEvent :: Ref (Tuple Int (Maybe (Either Int (Tuple Kont Int))))-> (String -> String -> Effect Unit) -> Event -> Effect UnitonButtonEvent var update _ = doTuple n state <- read varcase state ofNothing -> dolet res = fibonacci'''' nwrite (Tuple n (Just (Right res))) varupdate (show res) "next"Just (Right (Tuple k i)) -> dolet res = apply' k icase res ofLeft _ -> dolet n' = n + 1update (show res) ("fibonacci " <> show n')write (Tuple n' (Just res)) varRight _ -> doupdate (show res) "next"write (Tuple n (Just res)) varJust (Left i) -> dolet res = fibonacci'''' nwrite (Tuple n (Just (Right res))) varupdate (show res) "next"
2020-08-24 update: It may be Miles Frain just hinted how to do this better by using the signals library. He's got an example of it in purescript-cookbook.
So... that's it. Now we can show the intermediate computation steps of the fibonacci function.
Program output goes here
Here's the defunctionalized fibonacci again to illustrate that it remains to be a fairly small program.
data Kont = Done| Next {n :: Int, k :: Kont}| Conc {v :: Int, k :: Kont}fibonacci'''' :: Int -> (Tuple Kont Int)fibonacci'''' n = fibonacciCont''' n DonefibonacciCont''' :: Int -> Kont -> Tuple Kont IntfibonacciCont''' 0 k = Tuple k 0fibonacciCont''' 1 k = Tuple k 1fibonacciCont''' n k = fibonacciCont''' (n-2) (Next {n,k})apply' :: Kont -> Int -> Either Int (Tuple Kont Int)apply' Done s = Left sapply' (Next {n, k}) v = Right (fibonacciCont''' (n-1) (Conc {v,k}))apply' (Conc {v, k}) w = Right (Tuple k (v + w))
Here's the source code: fibonacci.zip
I got quite few questions about this method.
- Could this kind of defunctionalization be automated?
- Can you build a typeclass around the concept and vary how the program returns a value?
- Can it be used for implementing memoization/dynamic programming algorithms?
On the typeclass, I guess it'd be something like:
class Defunctionalized k a r | k -> a whereap :: k -> a -> rinstance direct :: Defunctionalized Kont Int Intinstance stepped :: Defunctionalized Kont Int (Tuple (Maybe Kont) Int)
2020-08-24 update: After reading the post, HakimSquirrel @ slack told me that he found a different post a bit easier to understand about defunctionalization itself. It can be found in James Koppel's blog, titled "The Best Refactoring You've Never Heard of". He kept his Compose 2019 talk from defunctionalization as well and the post is a transcript of that talk. It's a really long post but I think it may be helpful already simply because James explains what defunctionalization is. I also like the Hacker News example he gives, as an example of where he thinks defunctionalization can be useful.