Nothing so esoteric! By analogy with making invalid state unrepresentable, while it’s easy to statically exclude (Nothing, Nothing)
by using Either
instead of a pair of Maybe
s, and there are many other moderately more challenging cases where some more cleverness is required but sufficient to the task, in my experience it’s a rare Haskell program in the wild that doesn’t have a few cheeky fromJust
s (or equivalent) here and there, because of at least one of two things:
- Some invalid states correspond to things like an
Int
in one place in a data structure being less than one somewhere else, for which it’s difficult, in a non-esoteric way, to get a static exclusion. (Liquid Haskell and replacingInt
with singletons for type-level naturals are examples of things I’m considering ‘esoteric’.) - Some invalid states are simply dynamic in nature rather than static—for example, after a user-provided filter is applied to a list of results, it’s invalid to have any element in that list that doesn’t match the filter. (Sometimes a package like
justified-containers
exists to address a teeny tiny corner of this space, and I love things like that, but if they generalize without a lot of case-by-case effort I haven’t learned how.)
The same bullets apply to making invalid laziness unrepresentable, I argue, and I anticipate that the PureScript example is going to be an illustrative example of at least one of them. And conversely, if you manage to show me a non-esoteric way to solve the PureScript problem without deepseq
, I’ll consider that good evidence that taking an absolute stance against invalid laziness doesn’t have the same practical challenges that an absolute stance against invalid state often does.
I’m delighted that you want to take a crack! Sadly we never got a reproducing case from the reporter; having tested it now compiling the standard package set (as the comment you linked does) doesn’t demonstrate a difference in behavior with the NFData
patch. We need code that generates a lot of specific warnings, and we never figured out exactly what those warnings are. So we’re stuck with reasoning about the code—where you’ve already made good progress.
Yup, that’s what I figure. Unfortunately SimpleErrorMessage
can hold references to Expr
and SourceType
, both of which are recursive, lazy types that are extensively transformed with traversals throughout the compiler, and that’s the real problem. Making SimpleErrorMessage
’s constructors have shallow strict fields doesn’t keep an inner Expr
from holding a giant thunk.
Assume we don’t want to make Expr
and SourceType
strict everywhere in the compiler. I think we in fact don’t want to do this for valid reasons but I haven’t recently done the investigation, and it would be a big change for a questionable actual advantage relative to NFData
-ing everything and adding a force
in the right place.
So here’s a situation where thunks in Expr
and SourceType
are valid in the bulk of the compilation pipeline but not valid inside SimpleErrorMessage
constructors. We could, theoretically, make parallel ExprStrict
and SourceTypeStrict
types that are only used in SimpleErrorMessage
, but the maintenance cost there is much larger and the traversal to convert an Expr
to an ExprStrict
is just as expensive as the call to force
. We could also, I imagine, parameterize those types somehow with a type constructor that is instantiated as either a strict box or a lazy box, which has much the same problems (instead of having two near-identical types to maintain, we have a box that needs to be negotiated everywhere). Or we could store something else in those constructors, like the text to be printed that describes the expressions and types, but generating that text would need more state threaded into the core of the type checker and increase the coupling between parts of the system that should be disconnected.
And elegance aside, placing a force
at the end of a worker thread’s task is a very direct way to express that we don’t want thunks surviving past the life of a task. It won’t miss anything, even if our analysis of the code is incorrect or if some aspect of the code changes in a way that would cause a regression on this issue. That’s a big argument in favor, IMO, though maybe th-deepstrict
would be a suitable substitute for this in a static approach.
What would you do?