An Explanation

The following explanation builds upon and modifies this article's explanation:

  • Name of original author: Gabriel Gonzalez
  • License: CC 4.0 International License
  • Changes:
    • Convert code examples to Purescript
    • Renamed attackUnit to attack to reduce characters per line in code sections
    • Omitted section on Algebraic Data Types
    • Omitted section on Kleisli Composition

Why Callbacks Exist

When writing a library (e.g. GUI toolkit, game engine, etc.), one may want the end-developer to be able to run their own custom code at some point. In the example below, the custom code is represented by the type hole, ?doSomething:

attack :: Target -> Effect Unit
attack target = do
  valid <- isTargetValid target
  if valid
  then ?doSomething target
  else ignoreAttack

Since the library developer does not know how the end-developer will use this function, they can convert ?doSomething into a callback function that the end-developer supplies:

-- library developer's code
attack :: Target -> (Target -> Effect Unit) -> Effect Unit
attack target doSomething = do
  valid <- isTargetValid target
  if valid
  then doSomething target
  else ignoreAttack

-- end-developer's code
attack orc reduceLifeBy50

This makes life easy for the library-developer, but not for the end-developer as a large number of nested callbacks can make code very unreadable. (We only need to look at Node.js for an example)

The Continutation Solution

The problem is not the callback function; there is no other solution for the library-developer. The problem is "where" that function appears in attackUnit. In short, the type appears in the argument part of the function (attackUnit_arg) instead of in the return part of the function (attackUnit_return):

-- original version (curried function)
attack ::           Target ->  (Target -> Effect Unit)  -> Effect Unit

-- original version (uncurried function)
attack ::          (Target ->  (Target -> Effect Unit)) -> Effect Unit

-- desugar the last "->" into "Function"
attack :: Function (Target ->  (Target -> Effect Unit))    Effect Unit

-- make function appear in return type
attack :: Function  Target    ((Target -> Effect Unit)  -> Effect Unit)

-- resugar "->"
attack ::           Target -> ((Target -> Effect Unit)  -> Effect Unit)

Effect is a monad, so we can chain multiple sequential computations like that together using bind/>>=. If we can do that for Effect, why not do so for every monad? This changes our type signature of attack:

attack_no_monad ::  Target -> ((Target -> Effect Unit) -> Effect Unit)
attack_monad    ::  Target -> ((Target -> monad  Unit) -> monad  Unit)

That's a lot of code to write each time, so it can be converted into a newtype:

newtype ContT return monad input =
  ContT ((input -> monad return) -> monad return)

attack_no_monad ::  Target -> ((Target -> Effect Unit) -> Effect Unit)
attack_monad    ::  Target -> ((Target -> monad  Unit) -> monad  Unit)
attack_cont     ::  Target -> ContT Unit Effect Target

To create a ContT, we create a function whose only argument is the callback function:

ContT (\callbackFunction -> do
  -- everything else we did beforehand...
  callbackFunction arg
  -- everything else we did afterwards...
)

Let's implement it for attack and compare the two approaches:

attack_original :: Target -> (Target -> Effect Unit) -> Effect Unit
attack_original target doSomething = do
  valid <- isTargetValid target
  if valid
  then doSomething target
  else ignoreAttack

attack_contT ::  Target -> ContT Unit Effect Target
attack_contT target = ContT (\doSomething -> do
    valid <- isTargetValid target
    if valid
    then doSomething target
    else ignoreAttack
  )

And if we didn't want to use a monad, we could use Identity as a placeholder monad:

type Cont return input = ContT return Identity input

Comparing ContT to Another Function

Let's play with ContT for a bit and see what it reminds us of:

-- Original version
newtype ContT return monad input =
  ContT ((input -> monad return) -> monad return)

-- Let's newtype a version of `ContT` when `Identity` is its monad:
newtype ContIdentity return input =
  ContIdentity ((input -> Identity return) -> Identity return)

-- Since `Identity` is merely a placeholer monad,
-- let's remove it, and see what the resulting function's signature is:
contDesugared :: forall input return. ((input -> return) -> return)

-- and if we wanted to run `contDesugared`,
-- we'd need an initial `input` value:
runCont :: forall i r. ((i -> r) -> r) -> (i -> r) -> r
runCont contDesugared callbackFunction = contDesugared callbackFunction

-- Hmm... Doesn't that function's type and body look familiar?
--                   ((i -> r) -> r) -> (i -> r) -> r
apply :: forall a b. (a        -> b) -> a        -> b
apply function arg = function arg

infixr 0 apply as $

Exactly. ContT is just a monad transformer for the apply/$ function. Let's compare them further:

print   10
-- ==
print $ 10
-- ==
apply                print (10)
-- ==
runCont (Cont (\f -> f     (10))) print
-- which reduces to
              (\f -> f     (10))  print
-- which reduces to
                     print (10)

When You Need Two or More Callback Functions

If attack is modified, so that it requires two callback functions, ?doSomethingWithDamage and doSomethingWithBoth, it would seem that our nice solution from above would no longer work since we can only specify one of the two functions:

attackWith :: Target -> Weapon -> ContT Unit Effect Target
attackWith target weapon = ContT (\only1CallbackFunction -> do
    damage <- calculateDamageFor weapon (modifiedBy 1.5)
    ?doSomethingWithDamage damage
    valid <- isTargetValid target
    if valid
    then ?doSomethingWithBoth target damage
    else ignoreAttack
  )

The solution is to pass in a callback function that takes a sum type as its argument. When using ContT/Cont, the callback function is usually called k, so we'll do that here, too:

data AllPossibleInputs
  -- where each constructor wraps the arguments
  -- that will be used in a function
  = DoSomethingWithDamage Damage
  | DoSomethingWithBoth Target Damage

-- Note: This approach requires the callback function to return the same
-- `monadType returnType` type for each output.
callbackFunction :: AllPossibleInputs -> Effect Unit
callbackFunction (DoSomethingWithBoth t d) = doSomethingWithBoth t d
callbackFunction (DoSomethingWithDamage d) = doSomethingWithDamage d

doSomethingWithBoth :: Target -> Damage -> Effect Unit
-- implementation

doSomethingWithDamage :: Target -> Effect Unit
-- implementation

attackWith :: Target -> Weapon -> ContT Unit Effect Target
attackWith target weapon = ContT (\callback -> do
    damage <- calculateDamageFor weapon (modifiedBy 1.5)
    callback (DoSomethingWithDamage damage)
    valid <- isTargetValid target
    if valid
    then callback (DoSomethingWithBoth target damage)
    else ignoreAttack
  )

(I think one might be able to get around the runtime box overhead imposed by AllPossibleInputs by using type-level programming and Variant, the open Either type.)

Consider Your Perspective

If you are...... and you come across a situation where...... then you should...
a library developeryou want to use a callback functionmove it from the function's LHS to its RHS using ContT. If it should take different kinds of input, define a sum type as demonstrated above.
a developer using a libraryyou need to use a function that requires or returns a ContTunderstand that you need to specify what to do at specific points by passing in a callback function via runCont/runContT. You may also need to write a callback function that takes a sum type like that demonstrated above as its input