[Haskell-cafe] [ANN] data-forced 0.1.0.0 released!

Ruben Astudillo ruben.astud at gmail.com
Tue Apr 11 20:32:29 UTC 2023


Hello everyone,

I am excited to release the first version of data-forced 0.1.0.0. You can
get it on

    <https://hackage.haskell.org/package/data-forced-0.1.0.0>
    <https://github.com/RubenAstudillo/data-forced>

This is a library to avoid reference leaks (liveness leaks) on long lived
data structures close to main. It does so without using explicit `seq` or
bang patterns, instead using CBV functions, unlifted data type and tagging
evaluated values with a unexported newtype constructor. It is a bang pattern
you cannot forget!

# The big idea

We should not throw the information that a value was forced to WHNF or NF.
We neither should be required to remember to put a `seq` or BangPattern at
the use site to maintain this invariant.

We accomplish this using two newtypes:

- `newtype ForcedWHNF a = ForcedWHNF a`
- `newtype ForcedNF a = ForcedNF a`

that don't have their constructors exported. Only unidirectional patterns
are provided so you can de-structure them.

The *only* way to construct values of these types is via two CBVs
functions:

- strictlyWHNF :: forall a. a -> Strict (ForcedWHNF a)
- strictlyNF :: forall a. NFData a => a -> Strict (ForcedNF a)

The Strict data type is inspired by its analog on the data-elevator
package <https://hackage.haskell.org/package/data-elevator>. It is a
UnliftedDatatype, which means it cannot contain bottoms. The dynamic
semantics of bound values of unlifted data types is that *before being
bound, these are evaluated to WHNF*. It is a BangPattern you cannot forget!

Once you obtain the `Strict (ForcedWHNF a)` value, you de-structurate it to
obtain the *lifted* `ForceWHNF a` and store that. You are guaranteed that
the references necessary to force it to WHNF are now free. Hopefully you
will store it on a data structure that requires values to have been forced
before like

    import Data.Map.Lazy

    type Accum1 = Map Char (ForcedWHNF Int)
    type Accum2 = Map Char (ForcedNF (Maybe Text))

# How to use it

You should tag your long lived data structures with the correct type as in
the previous example. It is less ergonomic than bang patterns, but you
cannot forget to put them at the call site, parse not validate.

    import Data.Map.Lazy as ML -- Spine strict

    -- No references on added leafs even though it is a lazy map.
    basicEvent
      :: ML.Map Char (ForcedWHNF Int)
      -> IO (ML.Map Char (ForcedWHNF Int))
    basicEvent map0 = do
      let val0 :: Strict (ForcedWHNF Int)
          -- val0 = strictlyWHNF (error "argument evaluated") -- would fail
          val0 = strictlyWHNF (2 + 2)
          -- CBV function, 2 + 2 reduced before val0 is bound.
          Strict val1 = val0  -- De-structure
          map1 = ML.insert 'a' val1 map0
      pure map1

    -- Same as before, but not references to values that were deleted and
    -- not forced.
    basicEventPromptDeletion
      :: Strict (ML.Map Char (ForcedNF (Maybe Int)))
      -> IO (Strict (ML.Map Char (ForcedNF (Maybe Int))))
    basicEventPromptDeletion (Strict map0) = do
      -- map0 evaluated before being bound.
      let val0 :: Strict (ForcedNF (Maybe Int))
          val0 = strictlyWHNF ((+1) <$> Just 28)
          Strict val1 = val0  -- De-structure
          map1 :: Strict (ML.Map Char (ForcedNF (Maybe Int)))
          -- forced when bound as map1
          map1 = Strict (ML.insert 'a' val1 map0)
      pure map1

# Acknowledgment

Sebastian Graf (sgraf812) should take all the credit. I was inspired by his
data-elevator package that solves a sightly different problem. He infact
pushed the proposal for unlifted data types and made the work to integrate
them to GHC.


More information about the Haskell-Cafe mailing list