Home > Back-end >  How to avoid infinite loop in zipWith a self reference?
How to avoid infinite loop in zipWith a self reference?

Time:10-11

I'd like to create a list data structure that can zipWith that has a better behavior with self reference. This is for an esoteric language that will rely on self reference and laziness to be Turing complete using only values (no user functions). I've already created it, called Atlas but it has many built ins, I'd like to reduce that and be able to compile/interpret in Haskell.

The issue is that zipWith checks if either list is empty and returns empty. But in the case that this answer depends on the result of zipWith then it will loop infinitely. Essentially I'd like it to detect this case and have faith that the list won't be empty. Here is an example using DList

import Data.DList
import Data.List (uncons)

zipDL :: (a->b->c) -> DList a -> DList b -> DList c
zipDL f a b = fromList $ zipL f (toList a) (toList b)

zipL :: (a->b->c) -> [a] -> [b] -> [c]
zipL _ [] _ = []
zipL _ _ [] = []
zipL f ~(a:as) ~(b:bs) = f a b : zipL f as bs

a = fromList [5,6,7]

main=print $ dh where
   d = zipDL ( ) a $ snoc (fromList dt) 0
   ~(Just (dh,dt)) = uncons $ toList d

This code would sum the list 5,6,7 except for the issue. It can be fixed by removing zipL _ _ [] = [] because then it assumes that the result won't be empty and then it in fact turns out not to be empty. But this is a bad solution because we can't always assume that it is the second list that could have the self reference.

Another way of explaining it is if we talk about the sizes of these list.

The size of zip a b = min (size a) (size b)

So in this example: size d = min (size a) (size d-1 1)

But there in lies the problem, if the size of d is 0, then the size of d = 0, but if size of d is 1 the size is 1, however once the size of d is said to be greater than size of a, then the size would be a, which is a contradiction. But any size 0-a works which means it is undefined.

Essentially I want to detect this case and make the size of d = a.

So far the only thing I have figured out is to make all lists lists of Maybe, and terminate lists with a Nothing value. Then in the application of the zipWith binary function return Nothing if either value is Nothing. You can then take out both of the [] checks in zip, because you can think of all lists as being infinite. Finally to make the summation example work, instead of doing a snoc, do a map, and replace any Nothing value with the snoc value. This works because when checking the second list for Nothing, it can lazily return true, since no value of the second list can be nothing.

Here is that code:

import Data.Maybe

data L a = L (Maybe a) (L a)

nil :: L a
nil = L Nothing nil

fromL :: [a] -> L a
fromL [] = nil
fromL (x:xs) = L (Just x) (fromL xs)

binOpMaybe :: (a->b->c) -> Maybe a -> Maybe b -> Maybe c
binOpMaybe f Nothing _ = Nothing
binOpMaybe f _ Nothing = Nothing
binOpMaybe f (Just a) (Just b) = Just (f a b)

zip2W :: (a->b->c) -> L a -> L b -> L c
zip2W f ~(L a as) ~(L b bs) = L (binOpMaybe f a b) (zip2W f as bs)

unconsL :: L a -> (Maybe a, Maybe (L a))
unconsL ~(L a as) = (a, Just as)

mapOr :: a -> L a -> L a
mapOr v ~(L a as) = L (Just $ fromMaybe v a) $ mapOr v as

main=print $ h
   where
   a = fromL [4,5,6]
   b = zip2W ( ) a (mapOr 0 (fromJust t))
   (h,t) = unconsL $ b

The downside to this approach is it needs this other operator to map with Just . fromMaybe initialvalue. This is a less intuitive operator than . And without it the language could be built entirely on uncons and (:[]) which would be pretty neat.

The other thing I've figured out is in the current ruby implementation to throw an error when a value depends on itself, and catch it in the empty list detection. But this is vary hacky and not entirely sound, although it does work for cases like this. I don't think this can work in Haskell since I don't think you can detect self dependence?

Sorry for the long description and the very odd use case. I've spent tons of time thinking about this, but haven't solved it yet and can't explain it any more succinctly! Not expecting an answer but figured it is worth a shot, thanks for considering.

EDIT: After seeing it framed as a greatest fixed point question, it seems like a poor question because there is no efficient general solution to such a problem. For example, suppose the code was b = zipWith ( ) a (if length b < 1 then [1] else []).

For my purposes it could still be nice to handle some cases correctly - the example provided does have a solution. So I could reframe the question as: when can we find the greatest fixed point efficiently and what is that fixed point? But I believe there is no simple answer to such a question, and so it would be a poor basis for a programming language to rely on ad hoc rules.

CodePudding user response:

Sounds like you want a greatest fixed point. I'm not sure I've seen this done before, but maybe it's possible to make a sensible type class for types that support those.

class GF a where gfix :: (a -> a) -> a

instance GF a => GF [a] where
    gfix f = case (f (repeat undefined), f []) of
        (_:_, _) -> b:bs where
            b = gfix (\a' -> head (f (a':bs)))
            bs = gfix (\as' -> tail (f (b:as')))
        ([], []) -> []
        _ -> error "no fixed point greater than bottom exists"

-- use the usual least fixed point. this ain't quite right, but
-- it works for this example, and maybe it's Good Enough
instance GF Int where gfix f = let x = f x in x

Try it out in ghci:

> gfix (\xs -> zipWith ( ) [5,6,7] (tail xs    [0])) :: [Int]
[18,13,7]

This implementation isn't particularly efficient; e.g. replacing [5,6,7] with [1..n] results in a runtime that's quadratic in n. Perhaps with some cleverness that can be improved, but it's not immediately obvious to me how that would go.

CodePudding user response:

I have an answer for this specific case, not general.

appendRepeat :: a -> [a] -> [a]
appendRepeat v a = h : appendRepeat v t
   where
      ~(h,t) =
         if null a
         then (v,[])
         else (head a,tail a)

a = [4,5,6]

main=print $ head b
   where
   b = zipWith ( ) a $ appendRepeat 0 (tail b)

appendRepeat adds a an infinite list of a repeated value to the end of a list. But the key thing about it is it doesn't check if list is empty or not when deciding that it is returning a non empty list where the tail is a recursive call. This way laziness never ends up in an infinite loop checking the zipWith _ [] case.

So this code works, and for the purposes of the original question, it can be used to convert the language to just using 2 simple functions ( and :[]). But the interpreter would need to do some static analysis for appending a repeated value and replace it to using this special appendRepeat function (which can easily be done in Atlas). It seems hacky to only make this one implementation switcharoo, but that is all that is needed.

  • Related