Skip to content

Maybe reimplement the demand and force for MonadValue, MonadThunk and such #850

Closed
@Anton-Latukha

Description

Progress:

Proper order the arguments in:
class MonadThunk t m a | t -> m, t -> a implementations & all uses of:

  • ✔️ force
  • ✔️ forceEff
  • ✔️ further
  • ✔️ queryM

class MonadValue v m:

  • ✔️ demand

With #864 factor-out the Kleisli arrows from implementation, and do a new refactor over the code.

  • ✔️ force
  • ✔️ forceEff
  • ✔️ further
  • ✔️ queryM

class MonadValue v m:

  • ✔️ demand

The thunk family of functions are what are the most computationally costly in the system.

demand does the force (also known as forseThunk) - which is one of the most computational-intensive parts of the project:

Currently, they look as:

instance ...
  => MonadThunk (StdThunk m) m (StdValue m) where
  force    = force . _stdCited . _stdThunk
  
instance ...
  => MonadValue (Judgment s) (InferT s m) where
  demand = flip ($)
  inform j f = f (pure j)

instance ...
  => MonadValue (StdValue m) m where
  demand (Pure v) f = force v (flip demand f)
  demand (Free v) f = f (Free v)

  inform (Pure t) f = Pure <$> further t f
  inform (Free v) f = Free <$> bindNValue' id (flip inform f) v

Notice how thunk gets passed first into the function.

But the thunk (v) - is what these functions were created to change, the thunk never gets reused in them.


And, as I belabored everywhere already, - the source code does a lot of flipping of the arguments for them, and the implementation of them flips the arguments internally & to recurse on itself, which suggests that implementation itself should be flipped.

The resulting code:

instance ...
  => MonadThunk (StdThunk m) m (StdValue m) where
  force df v = force df (_stdCited $ _stdThunk v)  -- notice

instance ...
  => MonadValue (Judgment s) (InferT s m) where
  demand f v = f v  -- superflous
  inform f v = f $ pure v  -- superfluous
  -- becomes completely superfluous instance, reduction of it preserves code from superfluous type class overhead, simplifies the code and its readability.

instance ...
  => MonadValue (StdValue m) m where
  demand f (Pure v) = force (demand f) v
  demand f t = f t  -- is superflous
  
  inform f (Pure t) = Pure <$> further f t  -- ... to be investigated during refactor
  inform f (Free v) = Free <$> bindNValue' id (inform f) v  -- ... to be investigated during refactor

That looks both a lot more intuitive and efficient. demand f passes demand f, inform f - inform f - stack reuse, and more importantly the most costly one of them force df now does a tail call of itself.

  -- Because before it really was:
  force       = force . _stdCited . _stdThunk   -- is:
  force v df  = force (_stdCited $ _stdThunk v) df  -- passes thunk as 1-st arg that always gets changed.
  --- now
  force df v  = force df (_stdCited $ _stdThunk v)

We get a tail recursion on force - and reduce all arg flipping at once in the codebase. Which makes code more understandable.
Is this sound thing to do, or am I tripping?


So, Thunk.Basic also flips force which is an alias of forceThunk, and now it becomes:

forceThunk
  :: forall m v a
   . (MonadVar m, MonadThrow m, MonadCatch m, Show (ThunkId m))
  => (v -> m a)
  -> NThunkF m v
  -> m a
forceThunk k (Thunk n active ref) = do
  eres <- readVar ref
  case eres of
    Computed v      -> k v
    Deferred action -> do
      nowActive <- atomicModifyVar active (True, )
      if nowActive
        then throwM $ ThunkLoop $ show n
        else do
          v <- catch action $ \(e :: SomeException) -> do
            _ <- atomicModifyVar active (False, )
            throwM e
          _ <- atomicModifyVar active (False, )
          writeVar ref (Computed v)
          k v

Notice, in this forceThunk the very same thing - the first arg passed is never modified, and k gets returned first, what forceThunk operates on - is the thunk.
This way, probably means that constant k being the first argument - it is probably would get memory reuse.


And moreover, we see that 1-st arguments passed do not get used, they just get passed-through the whole (*sic) chain. This means there should be an elegant way to not pass them, but just apply them to the result of all this. Which would save a lot of computations.

The forceThunk ends everywhere in k v - shows that is just a passed-around function application that asks to become exterior, so after refactor the forceThunk just returns the v.

Recently ByteString, which seen as a pretty optimized library, in the 0.11 did so, bodigrim rewrote 1-3 HOFs so that they do not pass args superfluously, and by that ByteString suddenly got ~25% performance increase, probably that args passing was the ByteString bottleneck.


(Judgment s) (InferT s m) infer wraps in Pure and demand is just f a. -- established as vacuous
(StdValue m) m demand just unwraps the Pure and otherwise is just f a . -- seems also vacuous, maybe it can be simplified even further, especially since any force use always wrapped inside of demand (~100 cases), or where it is used raw - surrounding code simulates demand (all other cases). Since all force use gets wrapped with 1 logic gate - it seems logical to put it inside the force, and go do current recursion internally, and reduce demand to force everywhere. Anyway "demand" is a synonym to "force", which is in itself a suggestion. Reduction of demand also means the reduction of type class inference search in those ~100 cases.

Are these sound things, or am I tripping?

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions