From e3da28b3b40cca1ff9a75b8e459f334e3802c146 Mon Sep 17 00:00:00 2001
From: Anders Papitto <anderspapitto@gmail.com>
Date: Tue, 22 Nov 2016 13:55:31 -0800
Subject: [PATCH] add renderSumType to efficiently render sum types

See inline comment on renderSumType.

This function provides an alternative to two approaches to sum types
that already exist

- using dyn/widgetHold. This approach has the downside that the Dynamic
  is lost while rendering everything under the sum type, which means
  that every change causes a full re-render.

- rendering every constructor, and having a dynamic attribute with
  'display:none' set for everything except the currently active
  constructor. This works but is less principled - you are rendering
  something that doesn't actually exist, and then hiding it, instead of
  simply not rendering it in the first place (the approach of
  renderSumType).
---
 default.nix                     |   3 +-
 reflex-dom.cabal                |   1 +
 src/Reflex/Dom.hs               |   1 +
 src/Reflex/Dom/RenderSumtype.hs | 145 ++++++++++++++++++++++++++++++++
 4 files changed, 149 insertions(+), 1 deletion(-)
 create mode 100644 src/Reflex/Dom/RenderSumtype.hs

diff --git a/default.nix b/default.nix
index e26e511c..51deff69 100644
--- a/default.nix
+++ b/default.nix
@@ -4,7 +4,7 @@
 , ghc, webkitgtk3-javascriptcore, exception-transformers
 , webkitgtk24x, dependent-sum-template, bifunctors, bimap
 , raw-strings-qq, zenc, random, monad-control, keycode, hlint
-, unbounded-delays
+, unbounded-delays, generics-sop
 }:
 
 mkDerivation {
@@ -34,6 +34,7 @@ mkDerivation {
     transformers
     zenc
     unbounded-delays
+    generics-sop
   ] ++ (if ghc.isGhcjs or false then [] else [
     raw-strings-qq
     webkitgtk3-javascriptcore
diff --git a/reflex-dom.cabal b/reflex-dom.cabal
index eafbefc6..3f3be111 100644
--- a/reflex-dom.cabal
+++ b/reflex-dom.cabal
@@ -41,6 +41,7 @@ library
     dependent-sum-template >= 0.0.0.4 && < 0.1,
     directory == 1.2.*,
     exception-transformers == 0.4.*,
+    generics-sop >= 0.2.2 && < 0.3,
     ghcjs-dom >= 0.2.1 && < 0.3,
     -- keycode-0.2 has a bug on firefox
     keycode >= 0.2.1 && < 0.3,
diff --git a/src/Reflex/Dom.hs b/src/Reflex/Dom.hs
index 7c681a8a..641915c8 100644
--- a/src/Reflex/Dom.hs
+++ b/src/Reflex/Dom.hs
@@ -13,3 +13,4 @@ import Reflex.Dom.Time as X
 import Reflex.Dom.WebSocket as X
 import Reflex.Dom.Widget as X
 import Reflex.Dom.Xhr as X
+import Reflex.Dom.RenderSumtype as X
diff --git a/src/Reflex/Dom/RenderSumtype.hs b/src/Reflex/Dom/RenderSumtype.hs
new file mode 100644
index 00000000..0242752d
--- /dev/null
+++ b/src/Reflex/Dom/RenderSumtype.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE
+  DataKinds
+, DeriveGeneric
+, GADTs
+, KindSignatures
+, LambdaCase
+, RankNTypes
+, ScopedTypeVariables
+, TypeOperators
+#-}
+
+module Reflex.Dom.RenderSumtype
+  ( GTag, renderSumType )
+
+where
+
+import Data.Dependent.Sum
+import Data.GADT.Compare
+import Data.Maybe (isJust)
+import Generics.SOP
+
+import Reflex hiding (tag)
+import Reflex.Dom.Widget.Basic
+import Reflex.Dom.Old
+
+type GTag t = GTag_ (Code t)
+newtype GTag_ t (xs :: [*]) = GTag (NS ((:~:) xs) t)
+
+instance GEq (GTag_ t) where
+  geq (GTag (Z Refl)) (GTag (Z Refl)) = Just Refl
+  geq (GTag (S x))    (GTag (S y))    = GTag x `geq` GTag y
+  geq _               _               = Nothing
+
+toDSum :: Generic t => t -> DSum (GTag t) (NP I)
+toDSum = foo (\f b -> GTag f :=> b) . unSOP . from
+  where
+    foo :: (forall a . (NS ((:~:) a) xs) -> NP I a -> r)
+        -> NS (NP I) xs
+        -> r
+    foo k (Z x) =     (k . Z) Refl x
+    foo k (S w) = foo (k . S)      w
+
+data WrapDyn t m a = WrapDyn (m (Dynamic t (NP I a)))
+
+-- TODO consider using 'fan', which may bring a perf benefit
+-- | Efficiently render a Dynamic containing a sum type.
+--   As inputs, this takes the Dynamic to render, as well as a
+--   renderAnything function which is capable of rendering
+--   from a Dynamic containing any of the constructors of the
+--   sum type. In contrast to dyn/widgetHold, rendering of the
+--   constructors is done from a Dynamic, which avoids a full
+--   re-render on every update.
+--
+--   In this extended example, the user provides an arbitrary datatype,
+--   UsersSumType, as well as the function renderUsersSumType.
+-- > data SubState2
+-- > data SubState3a
+-- > data SubState3b
+-- > data SubState3c
+-- >
+-- > data UsersSumType
+-- >   = First
+-- >   | Second SubState2
+-- >   | Third SubState3a SubState3b SubState3c
+-- >   deriving GHC.Generic
+-- >
+-- > instance Generic UsersSumType
+-- >
+-- > data UsersEventType
+-- >
+-- > renderFirst
+-- >   :: forall t m. MonadWidget t m
+-- >   => Dynamic t (NP I '[])
+-- >   -> m (Event t UsersEventType)
+-- > renderFirst d = undefined
+-- >
+-- > renderSecond
+-- >   :: forall t m. MonadWidget t m
+-- >   => Dynamic t (NP I '[SubState2])
+-- >   -> m (Event t UsersEventType)
+-- > renderSecond d =
+-- >   let dynState = ((\(I x :* Nil) -> x) <$> d) :: Dynamic t SubState2
+-- >   in undefined
+-- >
+-- > renderThird
+-- >   :: forall t m. MonadWidget t m
+-- >   => Dynamic t (NP I '[SubState3a, SubState3b, SubState3c])
+-- >   -> m (Event t UsersEventType)
+-- > renderThird d =
+-- >   let dynA = ((\(I a :* I b :* I c :* Nil) -> a) <$> d) :: Dynamic t SubState3a
+-- >       dynB = ((\(I a :* I b :* I c :* Nil) -> b) <$> d) :: Dynamic t SubState3b
+-- >       dynC = ((\(I a :* I b :* I c :* Nil) -> c) <$> d) :: Dynamic t SubState3c
+-- >   in undefined
+-- >
+-- > renderUsersSumType
+-- >   :: MonadWidget t m
+-- >   => GTag UsersSumType a
+-- >   -> Dynamic t (NP I a)
+-- >   -> m (Event t UsersEventType)
+-- > renderUsersSumType = \case
+-- >   GTag       (Z Refl)   -> renderFirst
+-- >   GTag    (S (Z Refl))  -> renderSecond
+-- >   GTag (S (S (Z Refl))) -> renderThird
+-- >   _                     -> error "this is impossible"
+-- >
+-- > render
+-- >   :: MonadWidget t m
+-- >   => Dynamic t UsersSumType
+-- >   -> m (Event t UsersEventType)
+-- > render = renderSumType renderUsersSumType
+renderSumType
+  :: forall t m u r. (MonadWidget t m, Generic u)
+  => (forall b. GTag u b -> Dynamic t (NP I b) -> m (Event t r))
+  -> Dynamic t u
+  -> m (Event t r)
+renderSumType renderAnything dynState =
+  switchPromptly never =<<
+    ( dyn
+      . fmap toAction
+      . uniqDynBy sameConstructor
+      . toNestedDyn
+      . fmap toDSum
+    ) dynState
+  where
+    toAction :: DSum (GTag u) (WrapDyn t m) -> m (Event t r)
+    toAction (t :=> WrapDyn x) = x >>= renderAnything t
+
+    sameConstructor :: DSum (GTag u) a -> DSum (GTag u) a -> Bool
+    sameConstructor (t1 :=> _) (t2 :=> _) = isJust (t1 `geq` t2)
+
+    toNestedDyn
+      :: Dynamic t (DSum (GTag u) (NP I))
+      -> Dynamic t (DSum (GTag u) (WrapDyn t m))
+    toNestedDyn d = makeNestedDyn <$> d
+      where
+        makeNestedDyn :: DSum (GTag u) (NP I) -> DSum (GTag u) (WrapDyn t m)
+        makeNestedDyn (t :=> x) =
+          t :=> WrapDyn (holdDyn x (eventsForTag t d))
+
+    eventsForTag
+      :: GTag u a
+      -> Dynamic t (DSum (GTag u) (NP I))
+      -> Event t (NP I a)
+    eventsForTag tag = fmapMaybe tagToJust . updated
+      where tagToJust (t :=> x) = (\Refl -> x) <$> t `geq` tag