Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

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:

Haskell: Using The Module

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)

Haskell: Further Clean Up

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)

Haskell: Simpler Songs Record

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 ].