Continuing the discussion from Levity-Polymorphic Type Classes by Icelandjack · Pull Request #30 · ghc-proposals/ghc-proposals · GitHub
A recap of my position:
Personally, I’d like to see a future where you can program almost normally as you can today, but using unlifted data types exclusively. Thus basically carving out a strict subset of Haskell. That’s why I’d like to see the classes that we have today generalized to support unlifted types. A bit further in the future, I’d like to see a way to combine both lifted and unlifted programs in an ergonomic way. That would require easy conversion functions between lifted and unlifted versions of data types like maybe, tuples, and lists.
For me it is […] important to have the guarantee that my values don’t contain any surprise computations which may make my program take a lot more space and time than I expect. Instead of wondering if I should use a bang pattern at every binding (and then you still sometimes have to use
$!
for some function applications), with unlifted data types you only have to make that decision once per data type.
@tomjaguarpaw replied:
If there are examples of this issue that are solved by unlifted types but not by Make invalid laziness unrepresentable then I would like to know about them!
My response was that the strict-wrapper
approach only works if the values are in some kind of data structure, but if you just use the value in local let bindings and as function arguments then it does not help. As examples I mentioned that it can be difficult to know whether to use a strict or lazy variant of many functions like foldr/foldr'
and iterate/iterate'
.
If I’m understanding @tomjaguarpaw’s arguments correctly, then he believes each such function should have one blessed variant and the other should never be used. For the right fold the blessed version is the lazy version and for iterate
the blessed version is the strict iterate'
function.
I don’t agree with this and tried to show a useful application of a strict right fold:
type List :: UnliftedType -> UnliftedType
data List a = Nil | Cons a (List a)
foldrList' :: forall (a :: UnliftedType) (b :: UnliftedType). (a -> b -> b) -> b -> List a -> b
foldrList' _ z Nil = z
foldrList' k z (Cons x xs) = k x (foldrList' k z xs)
map f xs = foldrList' (\x xs' -> Cons (f x) xs') Nil xs
And here I’ll add an example where the lazy iterate
function is more performant than the strict variant:
foo i = head (iterate ('a' :) [] !! i)
The strict version would be exponential space (in the number of bits in the input Edit: actually,that’s not true.i
; ignoring that Int
has finite precision), while this one is constant space.
Of course this is not a practical example at all, but I think there are practical examples that similarly use laziness.
In @tomjaguarpaw’s latest comment he also wrote this:
Hmm, we may be talking at cross purposes. I’m trying to explain why it never makes sense to write
foldr' _ z [] = z foldr' f z (x:xs) = f x $! foldr' f z xs
If f is strict in its second argument then the $! is redundant and if it’s lazy then you’ve leaked stack space. Am I missing something here?
The compiler must assume that f
is lazy to preserve semantics. If you leave out the $!
then it will be forced to allocate a thunk even if f
turns out to be strict. So if you only plan to use this function with strict f
then you should use $!
to avoid allocating those thunks.