Haskell Reccursive Implementation for Maybe

Viewed 116

I am new to haskell, and I tried to implement a stack.

data Stack a = Stack [a] deriving Show

emptyStack :: Stack a
emptyStack = Stack []

push :: a -> Stack a -> Stack a
push element (Stack s) = Stack (element:s)

isEmpty :: Stack a -> Bool
isEmpty (Stack s) = null s

pop :: Stack a -> Maybe [a]
pop (Stack []) = Nothing
pop (Stack (x:xs)) = Just xs

main = do
    print (pop (Stack [1..10])) 
    -- Just [2,3,4,5,6,7,8,9,10]
    
    print (push 1 $ push 2 $ push 3 (Stack [1..10]))
    -- Stack [1,2,3,1,2,3,4,5,6,7,8,9,10]
         
    print (pop (Stack [1]))
    -- Just []
    
    print (pop $ pop (Stack [1..2]))
    -- Compile Error

In the above code, I wanted to pop multiple times like in the line print (pop $ pop (Stack [1..2])) but it's resulting in error because of the data type.

What would be a right way to implement this pop function such that I can call it continuously and have a proper behavior for empty stack.

Also, I'm still wondering how Haskell can be used in a real world application coming from imperative language background. I like it and want to use it in a web application.

1 Answers

Your push takes a stack and returns a stack. The first step to making your pop work like that is to take a stack and return a stack -- rather than returning a list, which is a different data type!

pop :: Stack a -> Maybe (Stack a)
pop (Stack []) = Nothing
pop (Stack (x:xs)) = Just (Stack xs)

When combining multiple calls to push, you use $:

push 1 $ push 2 $ push 3 (Stack [])

The ($) function takes an output of push and feeds it to another call to push. So it has a type that accepts a push output and passes it to a push-like function:

($) ::
    -- the type of push foo
    (Stack a -> Stack a) ->
    -- the type of a previous push's output
    Stack a ->
    -- the output type we want from pushing twice
    Stack a

Its implementation is very simple:

f $ stack = f stack

There's one more minor wrinkle you probably wouldn't think of at first: should f $ g $ stack mean (f $ g) $ stack or f $ (g $ stack)? For what we're doing, we want the latter. We have to tell the compiler this, and it can be done with a fixity declaration:

infixr 0 $

This says that repeated applications of $ should group to the right. There's also infixl when things should group to the left, and infix for when the compiler should not automatically group things. You can ignore the number for now.

The second step to making your pop work like push does is to invent a function that takes an output of pop and feeds it to another call to pop. Since pop returns a Maybe (Stack a), we'll need to have it take one of those. For historical reasons, I'm going to pick the name (=<<), but you could pick whatever name you like better -- maybe $+ or something to signify that it's a "souped-up" $. What type should it have? Well, it should take a pop output and feed it to pop. So:

(=<<) ::
    -- the type of pop
    (Stack a -> Maybe (Stack a)) ->
    -- the type of previous pop's output
    Maybe (Stack a) ->
    -- the output we want from doing two pops
    Maybe (Stack a)

Its implementation is a bit more work than ($)'s was, but not much:

f =<< x = case x of
    Just stack -> f stack
    Nothing -> Nothing -- what else can you do? there's no stack to feed to f

As before, we need a fixity declaration, and as before we want right-associativity.

infixr 1 =<<

Now we can chain calls to pop:

pop =<< pop =<< pop (Stack [1,2,3])

Enjoy!

P.S. Actually $ has a more general type than the one I showed here:

($) :: (a -> b) -> a -> b

It turns out our =<< has a more general type than the one shown above, too:

(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b

P.P.S. Next you will want to mix push and pop. You will soon discover that there could be very many variants of $ and =<< that you need to write... in each spot below, you could either choose to have a Maybe or not:

(Maybe? a -> Maybe? b) -> Maybe? a -> Maybe? b

Many of these are actually handled by the polymorphism in $'s existing type -- the ones where you make the same choice of Maybe/not Maybe in the first and third location and in the second and fourth location. You might have fun thinking about the remaining possibilities. Can they all be written? (No. Why not?)

In practice most people get most of their work done with just four operations:

($)   :: (a ->       b) ->       a ->       b
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
(<$>) :: (a ->       b) -> Maybe a -> Maybe b
pure  :: a -> Maybe a

(The last one, which doesn't exactly fit the pattern, is used when the conversion you want to do is to remove a Maybe from before an input to a function, without changing its output type.)

Related