I tried to keep things really simple - the goals is to make this working:
λ> let myBook = beginBook "Haskell"
λ> let myBook' = addChapter (Chapter "Intro" ["Hello","World"]) myBook
λ> let myBook'' = addChapter (Chapter "Two" ["Haskell","is","fun"]) myBook'
λ> let myBook''' = modifyChapter
(\ (Chapter _ pages) -> Chapter "Chapter 2" pages)
"Two" myBook''
λ> myBook'''
Book "Haskell"
[ Chapter "Intro" ["Hello","World"]
, Chapter "Chapter 2" ["Haskell","is","fun"]]
rethinking the definitions
First I changed your definition a bit (why use MyData
when you are fine with the type = typeConstructor
in Chapter
? - and why has a book a list of titles? - also I want to show
books so add deriving Show
):
type BookTitle = String
type ChapterTitle = String
type Page = String
data Chapter = Chapter ChapterTitle [Page] deriving Show
data Book = Book BookTitle [Chapter] deriving Show
start a book
next I added a simple function to start some book with it's title:
beginBook :: BookTitle -> Book
beginBook title = Book title []
adding a chapter
addChapter
is not much more difficult (note that I use ++
- in bigger scenarios you maybe want to represent the chapters in a back-to-front fashion and rewrite the Show
to reverse on demand - but for now this is fine):
addChapter :: Chapter -> Book -> Book
addChapter nextChapter (Book title chapters) =
Book title (chapters ++ [nextChapter])
it just uses pattern-matching to deconstruct the book into it's parts, add the new chapter at the end and reconstructs a new Book
with the old title and the new chapters.
modifying a chapter
you did not specify how you want to modify a chapter so I choose a very general form: basically you have to give a function that will modify a single chapter and next the name of the chapter to modify (ending with the book you want to modify):
modifyChapter :: (Chapter -> Chapter) -> ChapterTitle -> Book -> Book
modifyChapter modify chapterTitle (Book titles chapters) =
Book titles chapters'
where chapters' = map modifyAll chapters
modifyAll chapter@(Chapter chapterTitle' _)
| chapterTitle == chapterTitle' = modify chapter
| otherwise = chapter
it again deconstructs the book into it's parts put then changes the chapters
by mapping the modifyAll
function over them.
This function just looks for a chapter with the right title, modifies it and leaves all other unchanged.
At the end the book again is reconstructed from it's parts - the title is unchanged and the modified chapters are put in.
using this it's easy to write more specific modify-functions.
Let's say you want to change the chapter-title of a chapter - you just have to give the write modify
function to modifyChapter
:
changeChapterTitle :: ChapterTitle -> ChapterTitle -> Book -> Book
changeChapterTitle oldTitle newTitle book =
modifyChapter (\ (Chapter _ pages) -> Chapter newTitle pages) oldTitle book
exercises
Using the functions above:
- write a function that starts a new chapter in a book
- write a function that adds a page to a chapter
- This one is harder: What if two chapters have the same title? Maybe the representation is not so good and it would be better to access the chapters not by their title but by their index - try to rewrite the functions accordingly
- Probably you did the last one using
(!!)
- a better way might be to use data Book = Book BookTitle (Map ChapterIndex Chapter)
with type ChapterIndex = Int
and Map
is Data.Map
- can you rewrite the functions to use this too? Note that Data.Map
uses functions quite similar to those here to modify maps (like adjust
) ;)
remarks about lenses
Lenses (which where mentioned) basically gives an abstract mechanism to save you from writing functions like these - but IMO it's not a bad idea to get a feeling for this lover level functional-programming stuff anyway