[Haskell-cafe] Reifying case expressions [was: Re: On stream processing, and a new release of timeplot coming]
Heinrich Apfelmus
apfelmus at quantentunnel.de
Tue Dec 27 13:09:51 CET 2011
Sebastian Fischer wrote:
> Heinrich Apfelmus wrote:
>> Likewise, each function from lists can be represented in terms of our new
>> data type [...]
>>
>> length' :: ListTo a Int
>> length' = CaseOf
>> (0)
>> (\x -> fmap (1+) length')
>>
>> length = interpret length'
>
> This version of `length` is tail recursive while the previous version is
> not. In general, all functions defined in terms of `ListTo` and `interpret`
> are spine strict - they return a result only after consuming all input list
> constructors.
>
> This is what Eugene observed when defining the identity function as
>
> idC = CaseOf [] (\x -> (x:) <$> idC)
>
> This version does not work for infinite lists. Similarly, `head` and `take`
> cannot be defined as lazily as in the standard libraries.
Indeed, the trouble is that my original formulation cannot return a
result before it has evaluated all the case expressions. To include
laziness, we need a way to return results early.
Sebastian's ListTransformer type does precisely that for the special
case of lists as results, but it turns out that there is also a
completely generic way of returning results early. In particular, we can
leverage lazy evaluation for the result type.
The idea is, of course, to reify another function. This time, it's going
to be fmap
data ListTo a b where
Fmap :: (b -> c) -> ListTo a b -> ListTo a c
CaseOf :: b -> (a -> ListTo a b) -> ListTo a b
(GADT syntax due to the existential quantification implied by Fmap ). To
see why this works, have a look at the interpreter
interpret :: ListTo a b -> ([a] -> b)
interpret (Fmap f g) = fmap f (interpret g)
interpret (CaseOf nil cons) = \ys -> case ys of
[] -> nil
(x:xs) -> interpret (cons x) xs
In the case of functions, fmap is simply function concatenation
fmap f (interpret g) = f . interpret g
Now, the point is that our interpretation returns part of the result
early whenever the function f is lazy and returns part of the result
early. For instance, we can write the identity function as
idL :: ListTo a [a]
idL = CaseOf [] $ \x -> Fmap (x:) idL
When interpreted, this function will perform a pattern match on the
input list first, but then the Fmap will ensure that we return the
first element of the result. This seems incredible, so I encourage the
reader to check this by looking at the reduction steps for the expression
interpret idL (1:⊥)
To summarize, we do indeed have id = interpret idL .
Of course, the result type is not restricted to lists, any other type
will do. For instance, here the definition of a short-circuiting and
andL :: ListTo Bool Bool
andL = CaseOf True $ \b -> Fmap (\c -> if b then c else False) andL
testAnd = interpret andL (True:False:undefined)
-- *ListTo> testAnd
-- False
With the right applicative instance, it also possible to implement take
and friends, see also the example code at
https://gist.github.com/1523428
Essentially, the Fmap constructor also allows us to define a properly
lazy function const .
> To avoid confusion, I chose new names for my new types.
>
> data ListConsumer a b
> = Done !b
> | Continue !b (a -> ListConsumer a b)
I know that you chose these names to avoid confusion, but I would like
to advertise again the idea of choosing the *same* names for the
constructors as the combinators they represent
data ListConsumer a b
= Const b
| CaseOf b (a -> ListConsumer a b)
interpret :: ListConsumer a b -> ([a] -> b)
interpret (Const b) = const b
interpret (CaseOf nil cons) = \ys -> case ys of
[] -> nil
(x:xs) -> interpret (const x) xs
This technique for designing data structures has the huge advantage that
it's immediately clear how to interpret it and which laws are supposed
to hold. Especially in the case of lists, I think that this approach
clears up a lot of confusion about seemingly new concepts like Iteratees
and so on: Iteratees are just ordinary functions [a] -> b, albeit with a
slightly different representation in terms of familiar combinators like
case of, const, or fmap.
The "turn combinators into constructors" technique is the staple of
designing combinator libraries and goes back to at least Hughes' famous
paper
John Hughes. The Design of a Pretty-printing Library. (1995)
http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.38.8777
Best regards,
Heinrich Apfelmus
--
http://apfelmus.nfshost.com
More information about the Haskell-Cafe
mailing list