Preface
Goal: A practical case to collect unique record fields using Haskell.
Having fun with Haskell
I was once want to leverage my ecmascript knowledge. So I learn typescript and then purescript. I was too scared to jump directly too purescript. then I remember a few years a go I learn Haskell, so I make a mockup in Haskell. I rewrite my ecmascript in Haskell, and then I port the Haskell code to purescript.
Writing to Haskell is easy for me because, I have already had experience before. But still, I feels like the journey enriched my knowledge, so I decide to share the adventure.
From Haskell To Purescript
Consider this article as Purescript Mockup.
Writing to Purescript is also easy, since of purescript is very similar to Haskell. But there is a catch, there are differences, and there is very limited guidance. In fact I spent hours find out, onhow to solve these little differences. I think I have an obligation to share, so can port Haskell to Purescript easier.
Prelude Library
Find The Vorpal Sword!
We can make our own example case to leverage Haskell experience.
Haskell has so many library, not all library is good.
But there is one basic libraries called prelude
,
that we can use most of it.
There are already so many goodies in prelude, that we can exploit to work out common programming a situation. We can combine different techniques, create more possibility, and finally enjoy prelude as much as possible. In short, prelude is enough someone who start step into Haskell.
It is time to stop this hocus pocus, and get straight to coding.
Reference Reading
Source Examples
You can obtain source examples here:
Common Use Case
Task: Get the unique tag string
Please read overview for more detail.
Songs and Poetry
const songs = [
{ title: "Cantaloupe Island", tags: ["60s", "jazz"] },
{ title: "Let It Be", tags: ["60s", "rock"] },
{ title: "Knockin' on Heaven's Door", tags: ["70s", "rock"] },
{ title: "Emotion", tags: ["70s", "pop"] },
{ title: "The River" }
];
For simplicity reason, I start with a loosely typed language. The famous ecmascript, as shown in above code.
Specification
-
This simple records has two fields. The optional tags field is not mandatory.
-
The tags, can be implemented differently.
- Ecmascript/Typescript: an array of string.
- Haskell: a list of string.
- Purescript: an array of string.
Ecmascript Solution
The Answer
There are many ways to do things in ecmascript
.
One of them is this cryptic oneliner as below:
console.log([... new Set(songs.flatMap(song => song.tags || []))]);
Prepopulated Data
Haskell
is more verbose, with this solution below:
import Data.List
data Tags = Tags [String] deriving (Eq, Show)
data Song = Song { title :: String, tags :: Tags }
deriving (Show)
songs :: [Song]
songs = [
Song { title = "Cantaloupe Island",
tags = Tags ["60s", "jazz"] },
Song { title = "Let It Be",
tags = Tags ["60s", "rock"] },
Song { title = "Knockin' on Heaven's Door",
tags = Tags ["70s", "rock"] },
Song { title = "Emotion",
tags = Tags ["70s", "pop"] },
Song { title = "The River",
tags = Tags [] }
]
main = print $ nub $ concat
$ (map (\(Tags tags) -> tags))
$ (map tags songs)
Actually the solution above is also oneliner.
main = print $ nub $ concat $ (map (\(Tags tags) -> tags)) $ (map tags songs)
Now you can see the power of functional approach. But we need to adapt so we can read, what is really happening with this oneliner above. We need to know how exactly each function do. And further more what alternative applied for each situation.
The Unreadable
After a while, I began to understand how to read functional code. Functional code was so hieroglyph for me, until I wrote it myself. Slowly but sure, I can read others people codes. Even spot some mistakes, and improved with my custom code.
Functional code is easy to be read. If you dare to leave imperative thinking in your mind.
1: Data Type
Sing One Song at A Time
I enjoy playing song, one at a time, memorized the lyrics, play it over and over again, and after that moving it into my playlist.
Before building a complex records, I began with simple datatype.
List of String
Haskell use List
instead of Array
.
The List
is using recursive, to construct itself.
Thus a linked list it is, in contrast of array.
Although there is also Array
in haskell
,
it is usually more comfortable to work with List
.
tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = do
print tags
With the result of
$ runghc 01-type-tags.hs
["rock","jazz","rock","pop","pop"]
Oh yes, you can runghc
instead of compile and run as below:
$ ghc 01-type-tags.hs
[1 of 1] Compiling Main ( 01-type-tags.hs, 01-type-tags.o )
Linking 01-type-tags ...
$ ./01-type-tags
["rock","jazz","rock","pop","pop"]
Typeclass
Type class in Haskell does look like interface, although actually it serve a slight different purpose.
data Tags = Tags [String] deriving (Show)
data Song = Song { title :: String, tags :: Tags }
deriving (Show)
song :: Song
song = Song { title = "Cantaloupe Island",
tags = Tags ["60s", "jazz"] }
main = print (tags song)
With the result of
$ runghc 02-data-song.hs
Tags ["60s","jazz"]
You can observe the difference.
Now the list
wrapped in Tags
constructor.
Data Decision
Alternatively we can use type synonyms. So instead, of using data constructor as below:
data Tags = Tags [String]
I can use type synonym as below
type Tags = [String]
I found that using typeclass is, more suitable for this Haskell article. On the other hand, using simple type synonym is more suitable for Purescript article.
Deriving
We also need to add deriving (show)
in typeclass declaration,
so we can print it later.
There is also other addition later such as Eq
,
so we can compare with other stuff.
data Tags = Tags [String] deriving (Show)
This is also common stuff.
Unwrap Data
Since we want to the final result be precisely an list
,
we need to make custom unwrap
function:
data Tags = Tags [String] deriving (Show)
data Song = Song { title :: String, tags :: Tags }
deriving (Show)
song :: Song
song = Song { title = "Cantaloupe Island",
tags = Tags ["60s", "jazz"] }
unwrap :: Tags -> [String]
unwrap (Tags tags) = [ tag | tag <- tags ]
main = print $ unwrap $ tags song
With the result of
$ runghc 03-data-unwrap.hs
["60s","jazz"]
Map and List Comprehensive
In the example above we are using list comprehensive.
unwrap (Tags tags) = [ tag | tag <- tags ]
We can instead using map
to achieve this:
unwrap (Tags tags) = map (\tag -> tag) tags
The choice is yours, whatever suit the situation. Or whatever you are comfortable with.
🤔
With map
we can simplified using id
.
unwrap (Tags tags) = map id tags
Since map id tags
is the as tags
, then we can rewrite as:
unwrap (Tags tags) = tags
Thank you to Jihad D. Waspada,
for this map id tags
to tags
improvement.
2: Data Structure
Meet The Songs Record
From just karaoke, we can go pro with recording studio.
From simple data, we can build a structure to solve our task.
Record in Haskell
Consider to make a new module, named MySongs.hs
.
module MySongs (Tags(..), Song, songs, title, tags) where
data Tags = Tags [String]
deriving (Eq, Show)
data Song = Song { title :: String, tags :: Maybe Tags }
deriving (Show)
songs :: [Song]
songs = [
Song { title = "Cantaloupe Island",
tags = Just (Tags ["60s", "jazz"]) },
Song { title = "Let It Be",
tags = Just (Tags ["60s", "rock"]) },
Song { title = "Knockin' on Heaven's Door",
tags = Just (Tags ["70s", "rock"]) },
Song { title = "Emotion",
tags = Just (Tags ["70s", "pop"]) },
Song { title = "The River",
tags = Nothing }
]
Exporting Constructor
To be able to use Tags
constructor in other module,
we need to write as below:
module MySongs (Tags(..), Song) where
Instead of as below
module MySongs (Tags, Song) where
Now we can use the constructor on the other modules:
unwrap :: Tags -> [String]
unwrap (Tags tags) = [ tag | tag <- tags ]
Wrapping in Maybe
Why do we have to wrapped tags in Maybe
.
Song { title :: String, tags :: Maybe Tags }
It looks redundant, because we can define tags,
as an empty list
anyway.
Song { title = "The River", tags = [] }
The anwer is, yes of course, it is redundant. In fact it is a good decision to write as below. And reducing so many complexity affected but the design.
Song { title :: String, tags :: Tags }
While the answer is yes,
why this article still stuck with wrapping inside Maybe
🤔?
Am I stubborn?
This article is intended for tutorial purpose.
Mimic the original javascript that can have undefined
, empty []
.
Thus we need the record, not to be too simple.
Using Songs Module
At this point we can utilize the module, as shown in this very simple script below:
import MySongs
main = print songs
With the result similar as below nested list:
3: Approach in Solving Unique
Consider going back to simple data. We need to discuss about solving unique list.
nub
Haskell
has this nub
,
as a comfortable method to bring off unique
list.
While Purescript
has this nub
to pull of unique
array.
import Data.List
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (nub tags)
With the result similar as below list:
$ runghc 08-tags-nub.hs
["rock","jazz","pop"]
As simple as that.
Avoid nub
Why do I need different approach anyway 🤔?
Because there is a coding pattern that I need to show,
especially in Purescript
.
I need to know how things works,
so we can apply to other case.
And I simply cannot explain this pattern for tutorial purpose,
if I linger with magic provided by,
a ready to use method such as this nub
thing.
In real world, you can safely use nub
or ordnub
,
and leaving the complexity behind.
Exclude: List Comprehension
Before we go further, I need to show this cool exclude
trick.
exclude :: String -> [String] -> [String]
exclude tag xs = [ x | x <- xs, (/=) tag x ]
tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (exclude "rock" tags)
With the result similar as below list:
$ runghc 05-tags-filter.hs
["jazz","pop","pop"]
The list comprehesion can be shown as below notation:
exclude tag xs = [ x | x <- xs, (/=) tag x ]
I like to mockup
things with this list comprehensive.
It is simply math
notation, and easier to be understood.
Exclude: Filter
Then I, simplify list comprehension above with filter. With about the same result.
exclude :: String -> ([String] -> [String])
exclude tag = filter((/=) tag)
tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (exclude "rock" tags)
Now the question is. why would I want to make it hard for myself 🤔? Wasn’t list comprehension is good enough 😢?
The reason is, in Purescript,
it is more comfortable in purescript
,
to work with Array
than List
.
And the catch is, there is no list comprehension, in Array
😅.
Infix
We can also write above function using infix fashioned.
exclude tag = filter(tag /=)
It is more clear to just show the code as below:
main = print $ filter ("rock" /=) tags
Using parantheses would make inequality operator as prefix.
while pre could be interpreted as before.
Infix here, could mean in the middle.
It is easier to think infix
as this way.
Unique
Afterward, we can continue to construct, a recursive list of unique string.
exclude :: String -> ([String] -> [String])
exclude tag = filter((/=) tag)
unique :: [String] -> [String]
unique [] = []
unique (tag:tags) = tag:unique(exclude tag tags)
tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (unique tags)
With the result similar as below list:
$ runghc 07-tags-unique.hs
["rock","jazz","pop"]
This unique is using x:xs
as pattern matching.
The semicolon :
is a cons
operator to create a List
.
The basic coding pattern is a recursive function as below:
func (x:xs) = x : func(xs)
There used to be a constructor named Cons
before my age.
It is now replaced by :
operator.
Simplified Unique
We can go further by simplify the code as oneliner below:
unique [] = []
unique (x:xs) = x : unique (filter ((/=) x) xs)
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = print (unique tags)
This way you can flexbily apply this code pattern, to a more complex record structure.
For simple case, use nub
instead.
What is Next 🤔?
We have all the preparation, we need to conclude this article.
Consider continue reading [ Haskell - Playing with Records ].