I teach a data structures and algorithms course. Students can choose C++ or Java. As an exercise for myself, I am doing the assignments in Haskell. My primary goal is to understand what aspects of my class are easier in Haskell, and which aspects are more challenging. I am a fairly noob Haskell coder. Hence, I tend to be unaware of changes within the Haskell community, best practices, and "the right things". This is my secondary goal.
The class includes a List, Stack, Queue, Heap, and hash table. I'm up to the Queue structure so far.
Questions
For the List, I implemented
Foldable
,Functor
,Monoid
,Semigroup
to allow my List to be "list like" with the rest of Haskell's ecosystem, e.g.,fmap
, and<>
. Should I do that for the Queue too?In the C++/Java version of the class we implement a doubly-linked list then apply a Stack Interface on top of it. This is to show the flexibility of the doubly-linked list structure. However for the Haskell version it seemed more prudent to express the Queue structure directly, i.e, a thing with a beginning and ending, with some stuff in the middle. Is this a good choice? Is there a better expression of the Queue structure?
- Is
push
still constant time, i.e., O(1)? The recursive call for the More case will match either One or Two and be done.
- Is
Errors.
head Nil
is undefined. This is lame. Whilehead :: Queue a -> Maybe a
is an option, is there a better option?Is there a question I should have asked?
Related Work
- A simple queue implementation in Haskell
- Uses Maybe which is a hint my implementation should as well.
- Derives Eq. That makes sense to compare two Queue's. I'll add that to mine.
Queue Data Structure
module Queue
( Queue (Nil, Unit, More),
head,
pop,
push,
fromList,
empty,
)
where
import Data.List (foldl')
import Prelude hiding (head)
data Queue a
= Nil
| Unit a
| Two a a
| More a (Queue a) a
deriving (Show)
head :: Queue a -> a
head Nil = undefined
head (Unit x) = x
head (Two _ x) = x
head (More _ _ x) = x
pop :: Queue a -> (Queue a, a)
pop Nil = (Nil, undefined)
pop (Unit x) = (Nil, x)
pop (Two x y) = (Unit x, y)
pop (More x middle y) = (More x middle' newEnd, y)
where
(middle', newEnd) = pop middle
empty :: Queue a -> Bool
empty Nil = True
empty _ = False
push :: a -> Queue a -> Queue a
push x Nil = Unit x
push x (Unit y) = Two x y
push x (Two y z) = More x (Unit y) z
push w (More x middle y) = More w (push x middle) y
fromList :: [a] -> Queue a
fromList = foldl' push' Nil
where
push' = flip push
Tests
module QueueSpec (spec) where
import Data.Foldable (toList)
import Data.List (sort)
import Queue as Q
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Test.QuickCheck.Modifiers
spec :: Spec
spec = do
describe "head" $ do
it "put gets back" $ do
Q.head (Unit 5) `shouldBe` 5
it "pop unit" $ do
let x = Nil
let x2 = push 5 x
let (q, y) = pop x2
y `shouldBe` 5
it "pop more" $ do
let x = Unit 1
let x2 = push 2 x
let (q, y) = pop x2
y `shouldBe` 1
it "pop more with priority" $ do
let x = Unit 2
let x2 = push 1 x
let (q, y) = pop x2
y `shouldBe` 2
it "push a list" $ do
let x = fromList [1, 2, 3, 4, 5]
let (q, y) = pop x
y `shouldBe` 1
it "first element of list is first of queue" $
property prop_firstListIsFirstQueue
prop_firstListIsFirstQueue :: NonEmptyList Int -> Bool
prop_firstListIsFirstQueue xs = Prelude.head l == Q.head q
where
l = getNonEmpty xs
q = fromList l