input-output-hk / input-output-hk/nothunks

Generic instance does not work for single-constructor-single-argument types

Open
#24 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Haskell
Stars
51
Forks
15
PR merge metrics
No merged PRs in 30d

Description

The default definition of `wNoThunks` currently is

```haskell
wNoThunks ctxt x = gwNoThunks (Proxy @'[]) ctxt fp
where
-- Force the result of @from@ to WHNF: we are not interested in thunks
-- that arise from the translation to the generic representation.
fp :: Rep a x
!fp = from x
```

The problem is with the bang on the `fp`, comment notwithstanding. First, let's consider the case of a datatype with a single constructor with two fields:

```haskell
data T = MkT Int Int
```

Evaluation would proceed something like this:

```haskell
gwNoThunks .. $! from (MkT x y)
== gwNoThunks .. $! case MkT x y of MkT x y -> M1 (K x :*: K y)
== gwNoThunks .. $! M1 (K x :*: K y)
== gwNoThunks .. $ (K x :*: K y)
== allNoThunks [gwNoThunks .. $ K x, gwNoThunks .. $ K y]
== allNoThunks [noThunks .. $ x, noThunks .. $ y]
```

In this case the `$!` actually is not making any difference: the instance for products will pattern match on the `:*:` constructor, which will force the evaluation of the `case MkT ..` anyway.

The case for a datatype with multiple constructors is similar:

```haskell
data T = MkA Int | MkB Int
```

Now evaluation proceeds something like this:

```haskell
gwNoThunks .. $! from (MkA x)
== gwNoThunks .. $! case MkA x of MkA x -> M1 (L1 (K x))
MkB x -> ..
== gwNoThunks .. $! M1 (L1 (K x))
== gwNoThunks .. $ L1 (K x)
== gwNoThunks .. $ K x
== noThunks .. $ x
```

Again, the `$!` makes no difference here: the pattern match on `L1` in the instance for sums will force the `case MkA x ..` anyway.

But now consider the case for a datatype with a single constructor and a single field:

```haskell
data T = MkT Int
```

Now we are doomed if we do and doomed if we don't. In the library as it stands, with the `$!`, we get

```haskell
gwNoThunks $! from (MkT x)
== gwNoThunks $! case MkT x of MkT x -> M1 (K x)
== gwNoThunks $! M1 (K x)
== ..
```

Notice what is happening here: since the `x` is not protected by any constructor (both `M1` and `K` are newtypes), this will end up forcing `x`. This is bad, because `nothunks` should not result in any evaluation happening. But we cannot remove the `!` either; if we do, we get something like

```haskell
gwNoThunks $ from (MkT x)
== gwNoThunks $ case MkT x of MkT x -> M1 (K x)
== ..
== noThunks (case MkT x of MkT x -> ..)
```

In other words, we will report thunks when there aren't any.

I'm not entirely sure what the right solution is here, or if there even is one. Ideally the `K` constructor from GHC generics would not be a newtype; that would solve the problem, but is of course hardly a practical solution.

I think for now we should probably just document that the generic instance does not work for single-constructer-single-argument types. Whether or not that means changing the code (removing the bang in the definition of `fp`) I'm not totally sure about. As it stands, `nothunks` may force some evaluation to happen for such types, but will not result in confusing error messages. If we remove it, then using the generic instance for such types will result in thunks being found where there aren't really any, but at least you would be aware that something is amiss. Would appreciate an independent opinion here.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.