Pick a specific picture from a list

130 Views Asked by At

I have the following function:

blockToPicture :: Int -> [Picture] -> Picture
blockToPicture n [pic1,pic2,pic3] | n==0 = ...
                                  | n==1 = ...
                                  | otherwise = ...

If n==0 I want to select pic1, if n==1 I want to select pic2. Otherwise I want to select pic3. The problem is when one of the pictures doesn't load, so it doesn't appear on the list. Instead of [pic1,pic2,pic3] I have something like [Pic1,Pic3]. When the function is supposed to select a picture that isn't on the list I want it to write "X" instead. For that I'll use the function text "X" instead. The problem is that I don't know how to make it write the "X" instead of selecting the wrong picture.

Edit: I've created the following function but for some reason I'm getting the error "Variable not in scope" to the pictures.

blocoParaPicture :: Int -> [Picture] -> Picture
blocoParaPicture b l | b==0 = if elem pic1 l then pic1 else text "X"
                     | b==1 = if elem pic2 l then pic2 else text "X"
                     | otherwise = if elem pic3 l then pic3 else text "X"
2

There are 2 best solutions below

0
On BEST ANSWER

You can't just discard a picture that doesn't load; if you tried to load 3 pictures and end up with [some_pic, some_other_pic], how do you know which one didn't load? You need a list of type [Maybe Picture], with Just pic representing a successfully loaded picture and Nothing a failure. Then your function would look like

blockToPicture :: Int -> [Maybe Picture] -> Maybe Picture
blockToPicture _ []          = Nothing                  -- No pictures to choose from
blockToPicture 0 (Nothing:_) = Nothing                  -- Desired picture failed to load
blockToPicutre 0 (x:_)       = x                        -- Found desired picture!
blockToPicture n (_:xs)      = blockToPicture (n-1) xs  -- This isn't it; try the next one

Adapting Jorge Adriano's suggestion to use lookup (which is a good one)

import Control.Monad

blockToPicture :: Int -> [Maybe Picture] -> Maybe Picture
blockToPicture n pics = join (lookup n (zip [0..] pics))

Since lookup :: a -> [(a,b)] -> Maybe b and b here is Maybe Picture, we have a scenario where lookup returns Nothing if n is too big; Just Nothing if the desired picture fails to load, and Just (Just pic) if the desired picture is found. The join function from Control.Monad reduces the Maybe (Maybe Picture) value that lookup returns to the "regular" Maybe Picture that we want.

4
On
blocoParaPicture :: Int -> [Picture] -> Picture
blocoParaPicture b l | b==0 = if elem pic1 l then pic1 else text "X"
                     | b==1 = if elem pic2 l then pic2 else text "X"
                     | otherwise = if elem pic3 l then pic3 else text "X"

I'm getting the error "Variable not in scope" to the pictures.

The expression elem x xs checks if a given x is in a list xs. In your code when you write pic1, there's no such variable in scope, it isn't defined anywhere. In any case you don't want to search for a specific value in the list, rather you want to know if a given position "exists", that is if the list is long enough.

Also you can't just "write" inside a function with this type. In Haskell input and output is reflected on the types. This is a pure function, that takes some arguments and calculates a result, no side effects.

So what you can do here is return a Maybe Picture, which has values Nothing or Just pic depending whether you can return a picture or not. Or you can use Either String Picture, where values are of the form Left string or Right pic. Lets go for this latter option.

blocoParaPicture :: Int -> [Picture] -> Either String Picture

In terms of implementation we could diverge from the subject to get into a discussion of error management (since the problem is that access to a position may fail). But at this point I think it's best to avoid that detour so lets keep it (relatively) simple.

direct recursion (simplest)

The simplest most direct method would be direct recursion (as suggested by @chepner in the comments below).

blocoParaPicture :: Int -> [Picture] -> Either String Picture
blocoParaPicture _ []     = Left "X"
blocoParaPicture 0 (x:_)  = Right x
blocoParaPicture n (x:xs) = safe (n-1) xs

making sure !! succeds

If you do want to use the standard access function !!, one way to go about it (but potentially inefficient in the general case) would be to construct a "safe" infinite list.

import Data.List 

blocoParaPicture :: Int -> [Picture] -> Either String Picture
blocoParaPicture n xs = zs !! n 
                        where zs = [Right x | x <- xs] ++ repeat (Left "X")

The list zs is an infinite list made up of two lists. First [Right x | x <- xs] which just like your original list, but each element x becomes Right x. Then from then onwards all elements are of the form Left "X" to indicate failure. In general the above approach can be inefficient. If you look for a big n in a list:

[Right 1, Right 2] ++ [Left "X", Left "X", ...

you are doing many unnecessary steps, since you could stop when the first list ends. But works just fine for small n.

using lookup

Yet another possibility, similar to your attempt to use the elem function, would be to use lookup on indices. This function is safe by design.

lookup :: Eq a => a -> [(a, b)] -> Maybe b

Following this approach you first construct the list,

[(0,x0), (1,x1), (2,x2) ...(k,xk)]

and then look for your given n to return the associated xn (or Nothing).

blocoParaPicture' :: Int -> [Picture] -> Maybe Picture
blocoParaPicture' n xs = lookup n (zip [1..] xs)

This returns Nothing when not found though. But if you wish to, you can convert to Either via maybe :: b -> (a -> b) -> Maybe a -> b.

blocoParaPicture :: Int -> [Picture] -> Either String Picture
blocoParaPicture n xs = maybe (Left "X") Right (lookup n (zip [1..] xs))

This is certainly a bit too complex when all you want is a simple access function. But can be handy in situations where things are not as simple.