I've thought about this a bunch. So have other people:
Now my own speculation.
In the above discussions, the basic idea most of the time is that you could stick a !
on any type to get a strict, definitely evaluated, WHNF version of that type. So Int
might be a thunk, while !Int
is definitely not one. That raises interesting questions for the typechecker. Does !Int ~ Int
hold? If it doesn't -- the two are completely separate, incompatible types -- then working with them would be very painful. On the other hand, if it does hold, there would be nothing to prevent passing an unevaluated Int
where an !Int
is expected -- after all, they are the same type. What you would ideally want is to be able to supply an !Int
where an Int
is expected, but not vice versa. That sounds like a subtype relationship: !a <: a
, !a
is a subtype of a
, a
is inhabited by both evaluated and unevaluated values (thunks), while !a
only by evaluated ones. GHC's type system doesn't have subtyping, and probably couldn't, because the other parts of the type system don't interact well with it. This is a highly restricted, specific instance of subtyping though: the programmer doesn't get to declare arbitrary subtype relationships, rather there exists a single, hardcoded relationship: forall a. !a <: a
. I have no clue whether or not that makes it more reasonable to implement.
Suppose it could be done. You get further questions. What if you do try to supply an Int
where an !Int
is expected? Type error? Ideally, I think, it wouldn't be, instead the compiler would insert code to evaluate it, and carry on. Okay. How about supplying [Int]
where [!Int]
is expected, or f a
and f !a
in the general case? How could the compiler possibly know how to traverse any given structure to find those points where it contains an a
, to evaluate those, and only those? So would that be a type error? Let's say it is. How does the programmer do the conversion manually -- get an f !a
from an f a
? Perhaps by mapping a function eval :: a -> !a
? But that's nonsensical. Haskell is pervasively lazy. If you apply it to an argument, eval x
, then until its value is needed, it will be a thunk. So eval x
can't possibly have type !a
. Strictness annotations in the return type position don't make any sense. So what about: data Wrap a = Wrap { unwrap :: a }
, eval' :: a -> Wrap !a
, with the semantics that Wrap !a
might be a thunk, but the compiler inserts code so that when you evaluate it, the !a
inside will definitely be evaluated as well? Actually, here's a simpler formulation: data Wrap' a = Wrap' { unwrap' :: !a }
(eval' = Wrap'
). Which is existing legal Haskell, subsumed by our new strictness typing. That's nice, I guess. But as soon as you try to use it, you get problems again. How would you get from f a
to f !a
, again -- fmap unwrap . fmap Wrap
? But unwrap
has the exact same problem as eval
. So all of this seems not so trivial. And what about the seemingly innocuous reverse case: supplying f !a
where f a
is expected? Does that work? (In other words, f !a <: f a
?) It depends on how a
is used inside of f
. The compiler would have to have knowledge of covariant, contravariant, and invariant type argument positions -- another thing that comes with subtyping.
This is as far as I've thought it through. It seems harder than it seems.
One more interesting thing. You may or may not have heard about the notion of unlifted types: types which aren't inhabited by bottoms. That's, as far as I can tell, the same thing as this is. Types which are guaranteed to be evaluated to WHNF; types which are guaranteed to not be bottom. No difference, right? GHC actually already has a bunch of unlifted types as primitives (Int#
, etc.), but they're wired in (you can't add new ones) and also unboxed in addition to being unlifted, so they have a different kind (#
instead of *
) and can't mix with normal types. Whereas !a
would be an unlifted, but boxed, type, of kind *
. Unlifted types are something I've seen mentioned a few times in type theoretic contexts, so maybe there's been some research into what it would take to implement them in a more general way. I haven't looked, yet.
this
is not a smart pointer, and cannot be wrapped. Anything to do with non-trivial closures becomes a major nightmare as well. Or concurrency. And many abstractions become extremely leaky with manual memory management, to the point of rendering them useless. – Bashemath