Skip to content

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

Closed
@Anton-Latukha

Description

@Anton-Latukha

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

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