01-Rebindable-Ado.purs

module Syntax.Modification.RebindableAdo where

-- I assume you are already familiar with how 'ado notation' desugars.
-- If not, go read through that explanation again.

import Prelude

-- We'll use a qualified import to make it easier to see
-- when we're referring to the REAL 'apply' and 'map' as defined
-- in Prelude and not our customized versions.
import Data.Functor as NormalMap
import Control.Apply as NormalApply

-- Given this monad (type class instances are at bottom of file)
data Box a = Box a

{-
"ado notation" works by
- desugaring the "<-" notation via the "apply" function within scope
- desugaring the "in <function>" notation via the "map" function within scope

Thus, to change how these two things desugar, we change what 'apply' and 'map'
mean via a let binding or a where clause.

Note: rebinding 'ado' will produce the following compiler warnings:
"Name `apply` was shadowed."
"Name `map` was shadowed."
-}

-- This is how we would use "rebindable syntax" via a 'let binding'
-- to write normal "ado notation" (i.e. apply and map are unchanged)
normalApply_let_in :: Box Int
normalApply_let_in =
  let
    {-
    These do not work

    -- Compiler error: "The value of apply is undefined here,
    -- so this reference is not allowed."
    -- The compiler thinks 'apply' is defined as itself.
    apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = apply
    -}

    -- These do work.
    -- apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = NormalApply.apply

    -- map :: forall f a b. Functor f => (a -> b) -> f a -> f b
    map = NormalMap.map
  in ado
    three <- Box 3
    two <- Box 2
    in three + two

-- Redefining them in a 'where' clause is more readable.
normalApply_where :: Box Int
normalApply_where = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    apply :: forall f a b. Apply f => f (a -> b) -> f a -> f b
    apply = NormalApply.apply

    map :: forall f a b. Functor f => (a -> b) -> f a -> f b
    map = NormalMap.map

-- Now, let's change how `<-` gets desugared by actuallying changing how
-- `apply` is defined. Whenever `apply` gets called, it will add 1
-- to the result of apply by using `map (_ + 1)`.
plusApply_where :: Box Int
plusApply_where = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    -- Our modified 'apply' will add 1 each time it gets called.
    -- To support this modification, we'll modify the type signature
    -- to force this to only work on boxes of Ints.
    apply :: forall f. Apply f => f (Int -> Int) -> f Int -> f Int
    apply boxedF boxedArg =
      NormalMap.map (_ + 1) (NormalApply.apply boxedF boxedArg)         {-
                             ^ Warning: if we used `apply` here instead of
                               `NormalApply.apply`, we would have
                               created an infinite loop                  -}

    -- Since `map` isn't defined here, the closest "in-scope" definition
    -- is the `map` that is imported from Data.Functor in the 'import Prelude'
    -- line.

{-
Here's the graph reduction of the above `plus1Apply` code:
ado
  three <- Box 3
  two <- Box 2
  in three + two

((\three two -> three + two)
  `NormalMap.map` Box 3)
  `apply` Box 2 -- i.e. our modified apply

(NormalMap.map (\three two -> three + two) (Box 3)) `apply` Box 2

Box (\two -> 3 + two) `apply` Box 2

apply (Box (\two -> 3 + two)) (Box 2)

-- now reduces 'apply' with our modified definition
-- Note that "<*>" refers to the normal apply definition.

map (_ + 1) (NormalApply.apply (Box (\two -> 3 + two)) (Box 2))
map (_ + 1) (                  (Box (\2   -> 3 + 2  ))        )
map (_ + 1) (                  (Box (        5      ))        )
map (_ + 1) (Box 5)
Box 6
-}

-- Applicative types allow us to use things like `*>` and `<*`
-- Why not rebind ado notation to use that for apply?
realWorldExample :: Box Int
realWorldExample = ado
  three <- Box 3
  two <- Box 2
  in three + two
  where
    -- Our modified `apply` will run the normal computation
    -- and then return the result, Box 4, rather than the actual computation.
    -- A better example would be using "ado notation" to validate
    -- data and use something like, `boxedF <*> boxedArg <* pure unit`,
    -- to reduce boilerplate.
    apply :: forall f. Applicative f => f (Int -> Int) -> f Int -> f Int
    apply boxedF boxedArg = boxedF <*> boxedArg *> pure 4

{-
The above graph reduction is:
ado
  three <- Box 3
  two <- Box 2
  in three + two

(\three two -> three + two)
  `NormalMap.map` (Box 3)
  `apply` (Box 2)

NormalMap.map (\three two -> three + two) (Box 3) `apply` (Box 2)

Box (\two -> 3 + two) `apply` (Box 2)

-- here we use replace 'apply' with our modified definition

Box (\two -> 3 + two) <*> (Box 2) *> Box 4
NormalApply.apply (Box (\two -> 3 + two)) <*> (Box 2) *> Box 4
                  (Box (\    -> 3 + 2  ))             *> Box 4
                  (Box          5       )             *> Box 4
                  applyRight (Box 5) (Box 4)

applyRight (Box 5) (Box 4)
(\left right -> right) <$> (Box 5) <*> (Box 4)
Box 4
-}

-- I don't have an example of remapping `map` because I couldn't find a way
-- to do that without getting a compiler error.
-- Still, this file demonstrates how to do this.

{-
Lastly, one can change `apply` multiple times throughout the 'ado notation'
if one were to use lets. For example,

ado
  -- apply not set, so use normal definition
  a <- someA
  let apply = definition -- apply now uses modified version
  b <- someB
  c <- someC
  let apply = definition2 -- apply now uses a second modified version
  d <- someD
  in computation a b c d

For readability, this is not recommended. I only include it here to be
complete in this explanation.

-}

-- Type class instances

instance Functor Box where
  map :: forall a b. (a -> b) -> Box a -> Box  b
  map f (Box a) = Box (f a)

instance Apply Box where
  apply :: forall a b. Box (a -> b) -> Box a -> Box  b
  apply (Box f) (Box a) = Box (f a)

instance Applicative Box where
  pure :: forall a. a -> Box a
  pure a =  Box a

instance (Show a) => Show (Box a) where
  show (Box a) = "Box(" <> show a <> ")"