Preface
Goal: Continue Part One
4: Extracting Records
We are going to flatten the list
.
Brief Overview
We can visualize step by step, what we are going to discuss here:
This is a messy process, the simpler the result, the more complex the code is. But do not worry, we are going to make it simpler later.
Flatten Tags Field
Extracting tags
field from songs
record is an easy thing to do.
In haskell
you just can rewrite this with (tags song)
.
While in purescript
accessing field is written as (song.tags)
.
import MySongs
tagsList :: [Song] -> [Maybe Tags]
tagsList songs = [ tags song | song <- songs ]
main = print $ tagsList songs
With the result similar as below list:
$ runghc 09-songs-tags.hs
[Just (Tags ["60s","jazz"]),Just (Tags ["60s","rock"]),Just (Tags ["70s","rock"]),Just (Tags ["70s","pop"]),Nothing]
Remove The Nothing Signature
We are going to flatten the list again,
but we have to do some process first to achieve this.
Consider to start with taking out tags signatured as Nothing
.
import MySongs
tagsList :: [Song] -> [Maybe Tags]
tagsList songs = [ tags song | song <- songs ]
flattenTags :: [Maybe Tags] -> [Maybe Tags]
flattenTags aTagsList = [
maybeTags | maybeTags <- aTagsList, maybeTags /= Nothing
]
main = print (flattenTags $ tagsList $ songs)
With the result similar as below list:
$ runghc 10-songs-tags-field.hs
[Just (Tags ["60s","jazz"]),Just (Tags ["60s","rock"]),Just (Tags ["70s","rock"]),Just (Tags ["70s","pop"])]
As usual, I mock up with list comprehension.
This maybeTags
can be written as x
.
[ x | x < xs, x /= Nothing]
I use maybeTags
to highlight that,
this still wrapped in Maybe
Constructor.
Unwrap The Maybe Constructor
There is already a fromJust
in prelude
.
We are going to reinvent the wheel,
because we are going to use it differently,
in respect with empty []
list later.
import MySongs
tagsList :: [Song] -> [Maybe Tags]
tagsList songs = [ tags song | song <- songs ]
tagsFromJust :: Maybe Tags -> Tags
tagsFromJust Nothing = error "Maybe.tagFromJust: Nothing"
tagsFromJust (Just tags) = tags
flattenTags :: [Maybe Tags] -> [Tags]
flattenTags aTagsList = [
tagsFromJust maybeTags | maybeTags <- aTagsList,
maybeTags /= Nothing
]
main = print (flattenTags $ tagsList songs)
With the result similar as below list:
$ runghc 11-songs-tags-unwrap.hs
[Tags ["60s","jazz"],Tags ["60s","rock"],Tags ["70s","rock"],Tags ["70s","pop"]]
We can alter the tagsFromJust
behaviour,
to have a default value, instead of throwing exception.
tagsFromJust Nothing = Tags []
tagsFromJust (Just tags) = tags
This way, we can avoid side effects.
catMaybes
Thank you Jihad D. Waspada,
for the catMaybes
advice.
With catMaybes
, this can even be simplified as below code:
import MySongs
import Data.Maybe
main = print $ catMaybes (map tags songs)
There are still two more steps to go. Please be patient.
Unwrap The Custom Tags Constructor
import MySongs
import Data.Maybe
unwrap :: Tags -> [String]
unwrap (Tags tags) = tags
flattenTags :: [Maybe Tags] -> [[String]]
flattenTags aTagsList = (map unwrap)
$ (catMaybes aTagsList)
main = print $ flattenTags $ (map tags songs)
With the result similar as below list:
$ runghc 12-songs-tags-extract.hs
[["60s","jazz"],["60s","rock"],["70s","rock"],["70s","pop"]]
It is simply using the previously discussed unwrap
:
unwrap (Tags tags) = tags
Finally Flattened
There is already standard concat
to flatten List
.
import MySongs
import Data.Maybe
unwrap :: Tags -> [String]
unwrap (Tags tags) = tags
flattenTags :: [Maybe Tags] -> [String]
flattenTags aTagsList = concat
$ (map unwrap (catMaybes aTagsList))
main = print $ flattenTags $ (map tags songs)
With the result similar as below list:
$ runghc 13-songs-tags-flatten.hs
["60s","jazz","60s","rock","70s","rock","70s","pop"]
The difference with previous code is here:
flattenTags :: [Maybe Tags] -> [[String]]
flattenTags aTagsList = (map unwrap) $ (catMaybes aTagsList)
We are done flattening, those fields from prepopulated songs record.
5: Put The String Together
Those Tags are, actually a
String
data type.
Towards final code. We need to assemble all the effort, and simplified the codes.
Applying Unique
Combining with previous custom made unique, we have this code below:
import MySongs
import Data.Maybe
unwrapTags :: Tags -> [String]
unwrapTags (Tags tags) = tags
flattenTags :: [Maybe Tags] -> [String]
flattenTags aTagsList = concat
$ ((map unwrapTags) (catMaybes aTagsList))
exclude :: String -> [String] -> [String]
exclude tag xs = [ x | x <- xs, (/=) tag x ]
unique :: [String] -> [String]
unique [] = []
unique (tag:tags) = tag:unique(exclude tag tags)
main = print $ unique $ flattenTags $ (map tags songs)
With the result similar as below list:
$ runghc 14-songs-tags-unique.hs
["60s","jazz","rock","70s","pop"]
Whoaaaa.. this is long and messy 😳! Does this functional programming always that bad 🤔?
Extra Miles
We need to clean-up.
import MySongs
import Data.Maybe
unwrapTags :: Tags -> [String]
unwrapTags (Tags tags) = tags
flattenTags :: [Maybe Tags] -> [String]
flattenTags aTagsList = concat
$ ((map unwrapTags) (catMaybes aTagsList))
unique :: [String] -> [String]
unique [] = []
unique (tag:tags) = tag:unique(filter((/=) tag) tags)
main = print (unique $ flattenTags $ (map tags songs))
with about the same result.
Finalizing
This is simpler right 🙂?
Do not stop clean up. We can optimize more. And get the final code as below:
import MySongs
import Data.List
import Data.Maybe
unwrapTags :: Tags -> [String]
unwrapTags (Tags tags) = tags
main = print $ nub $ concat
$ (map unwrapTags)
$ catMaybes
$ (map tags songs)
Instead of being complex from the start, you can just jump straight away, to example conclusion as above code.
Alternative Approach
If you care to change the data structure,
omiting Maybe Tags
wrapper,
you can even write this as below:
main = print $ nub $ concat
$ (map (\(Tags tags) -> tags))
$ (map tags songs)
I am pretty sure there are other ways to accomplish this task.
Further Data Structure Optimization
Jihad D. Waspada, help me to solve the situation even more, by changing typeclass into type synonym.
import Data.List
type Tags = [String]
data Song = Song { title :: String, tags :: Tags }
deriving (Show)
songs :: [Song]
songs = [
Song { title = "Cantaloupe Island",
tags = ["60s", "jazz"] },
Song { title = "Let It Be",
tags = ["60s", "rock"] },
Song { title = "Knockin' on Heaven's Door",
tags = ["70s", "rock"] },
Song { title = "Emotion",
tags = ["70s", "pop"] },
Song { title = "The River",
tags = [] }
]
main = print $ nub $ concat $ (map tags songs)
Thank you for pointing out into the further improvement. I guess, this is the beauty of functional approach. There are so many possibility. Thus, another optimization.
What is Next 🤔?
Porting Haskell to Purescript.
Consider continue reading [ Purescript - Playing with Records ].