Preface
Goal: A practical case to collect unique record fields using Purescript.
Reference Reading
Source Examples
You can obtain source examples here:
Credits
This source from article can be made, by the help of people in the community:
- Andika Rizary
- Jihad D. Waspada
- Ananda Umamil
Once I was having an issue in solving unique
in Array.
But now it has solved with the help of this three helpful people.
Common Use Case
Task: Get the unique tag string
Please read overview for more detail.
I have already make a mockup in Haskell, you can read the details here:
- [Haskell - Playing with Records][local-haskell-play]
Prepopulated Data
Songs and Poetry
type Tags = Array String
type Song = { title :: String, tags :: Tags }
songs :: Array Song
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",
tags : [] }
]
Purescript Solution
The Answer
There are many ways to do things in purescript
.
One of them is this oneliner as below:
main = log $ show $ nub $ concat $ map (\song -> song.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.
Functional code is easy to be read. If you dare to leave imperative thinking in your mind.
Enough with introduction, at this point we should go straight to coding.
Environment
I am using spago
.
And all script placed in /src
as modules.
Each module should have different name, so it does not collide with each other.
module Step01 where
I run the script directly from spago
using CLI,
with example as below:
$ spago run --main Step01
[info] Build succeeded.
["rock","jazz","rock","pop","pop"]
This way, we can test each script easily,
without making a new spago
project.
Spago
Here is my spago.dhall
configuration:
I add arrays
and "exceptions"
.
{-
Welcome to a Spago project!
You can edit this file as you like.
-}
{ name = "my-project"
, dependencies = [ "console", "effect", "lists", "psci-support",
"arrays", "exceptions" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}
1: Data Type
Before building a complex records, I begin with simple datatype.
Array or List?
You can use either List
or Array
in Purescript
.
For a beginner like me, it is more comfortable,
to use Array
instead List
in Purescript
.
Since we are using Array
as example in this article,
beware of the differences
with previously made Haskell code using List
.
Array of String
Our first example is very similar with the Haskell mockup.
Except there are many import
header clause in purescript
.
module Step01 where
import Prelude
import Effect.Console (log)
tags :: Array String
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = do
log (show tags)
With the result similar to below code:
$ spago run --main Step01
[info] Build succeeded.
["rock","jazz","rock","pop","pop"]
Type Synonims
Records in Purescript might lead to ambiguous, when it compares to Haskell typeclass. The term records in purescript here is simply type synonims. Type synonims does not have a constructor and so on.
module Step02 where
import Prelude
import Effect.Console (log)
type Tags = Array String
type Song = { title :: String, tags :: Tags }
song :: Song
song = { title : "Cantaloupe Island", tags : ["60s", "jazz"] }
main = log $ show (song.tags)
With the result of
$ spago run --main Step02
[info] Build succeeded.
["60s","jazz"]
You can observe the difference.
Purescript is using :
instead of =
in haskell.
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 Purescript
Consider to make a new module, named MySongs.purs
.
module MySongs (Tags(..), Song, songs) where
import Data.Maybe
type Tags = Array String
type Song = { title :: String, tags :: Maybe Tags }
songs :: Array Song
songs = [
{ title : "Cantaloupe Island",
tags : Just (["60s", "jazz"]) },
{ title : "Let It Be",
tags : Just (["60s", "rock"]) },
{ title : "Knockin' on Heaven's Door",
tags : Just (["70s", "rock"]) },
{ title : "Emotion",
tags : Just (["70s", "pop"]) },
{ title : "The River",
tags : Nothing }
]
Wrapping in Maybe
Why do we have to wrapped tags in Maybe
.
type Song = { title :: String, tags :: Maybe Tags }
It looks redundant, because we can define tags,
as an empty list
anyway.
{ 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.
type 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:
module Step04 where
import Prelude
import Effect.Console (log)
import MySongs
main = log $ show songs
With the result similar as below nested array:
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.
module Step08 where
import Prelude
import Data.Array (nub)
import Effect.Console (log)
tags :: Array String
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = log $ show $ nub $ tags
With the result similar as below list:
$ spago run --main Step08
[info] Build succeeded.
["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 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 utilize nub
,
and leaving the complexity behind.
Exclude: Filter
Before we go further, I need to show this cool exclude
trick.
module Step06 where
import Prelude
import Data.Array (filter)
import Effect.Console (log)
exclude :: String -> Array String -> Array String
exclude tag xs = filter((/=) tag) xs
tags :: Array String
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = log $ show (exclude "rock" tags)
There is no list comprehension, in Array
😅.
Thus, filter is the choice.
Unique
Afterward, we can continue to construct, an array of unique string.
module Step07 where
import Prelude
import Data.Array (filter, (:), cons, uncons)
import Data.Maybe
import Effect.Console (log)
exclude :: String -> Array String -> Array String
exclude tag xs = filter((/=) tag) xs
unique :: Array String -> Array String
unique tags = case uncons tags of
Nothing -> []
Just {head: tag, tail: tags} -> cons tag (unique (exclude tag tags))
tags :: Array String
tags = ["rock", "jazz", "rock", "pop", "pop"]
main = log $ show (unique tags)
With the result similar as below array:
$ spago run --main Step07
[info] Build succeeded.
["rock","jazz","pop"]
Difference with List in Haskell
We cannot use x:xs
as pattern matching technique,
that is applied for list only.
W have toe use uncons
pattern instead,
then also utilize cons
to append new array recursively.
unique tags = case uncons tags of
Nothing -> []
Just {head: tag, tail: tags} -> cons tag (unique (exclude tag tags))
This is just a matter of syntax. But since there is very few working example in the internet, I have to dig for almost six hours, just to find these three lines.
With example above, we can apply the pattern to other cases as well. Especially with a more complex record structure.
For simple case, use nub
instead.
4: Extracting Records
We are going to flatten the array
.
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 purescript
accessing field is written as (song.tags)
.
While haskell
the notation is (tags song)
.
module Step09 where
import Prelude
import Data.Array
import Data.Maybe
import Effect.Console (log)
import MySongs
tagsArray :: Array Song -> Array (Maybe Tags)
tagsArray songs = map (\song -> song.tags) songs
main = log $ show (tagsArray songs)
With the result similar as below list:
$ spago run --main Step09
[info] Build succeeded.
[(Just ["60s","jazz"]),(Just ["60s","rock"]),(Just ["70s","rock"]),(Just ["70s","pop"]),Nothing]
There are still three more steps to go. Please be patient.
Pro Tips
Lucky me 🙂!
Jihad D. Waspada give me a pro tips.
map (\song -> song.tags) songs
, can be rewritten asmap (_.tags) songs
.
This is a real improvement.
Remove The Nothing Signature
We are going to flatten the array again,
but we have to do some process first to achieve this.
Consider to start with taking out tags signatured as Nothing
.
module Step10 where
import Prelude
import Data.Maybe
import Data.Array (filter)
import Effect.Console (log)
import MySongs
tagsArray :: Array Song -> Array (Maybe Tags)
tagsArray songs = map (_.tags) songs
flattenTags :: Array (Maybe Tags) -> Array (Maybe Tags)
flattenTags aTagsArray = filter ((/=) Nothing) aTagsArray
main = log $ show $ flattenTags $ tagsArray $ songs
With the result similar as below list:
$ spago run --main Step10
[info] Build succeeded.
[(Just ["60s","jazz"]),(Just ["60s","rock"]),(Just ["70s","rock"]),(Just ["70s","pop"])]
I use maybeTags
to highlight that,
this still wrapped in Maybe
Constructor.
Unwrap The Maybe Constructor
There is already a fromJust
in Data.Maybe
.
We are going to reinvent the wheel,
because we are going to use it differently,
in respect with empty []
list later.
module Step11 where
import Prelude
import Data.Maybe
import Data.Array (filter)
import MySongs
import Effect.Console (log)
import Effect.Unsafe
import Effect.Exception
error :: forall a. String -> a
error = unsafePerformEffect <<< throw
tagsArray :: Array Song -> Array (Maybe Tags)
tagsArray songs = map (_.tags) songs
tagsFromJust :: Maybe Tags -> Tags
tagsFromJust Nothing = error "Maybe.fromJust: Nothing"
tagsFromJust (Just tags) = tags
flattenTags :: Array (Maybe Tags) -> Array Tags
flattenTags aTagsArray = (map tagsFromJust)
$ (filter ((/=) Nothing) aTagsArray)
main = log $ show $ flattenTags $ tagsArray $ songs
With the result similar as below list:
$ spago run --main Step11
[info] Build succeeded.
[["60s","jazz"],["60s","rock"],["70s","rock"],["70s","pop"]]
catMaybes
Thank you Jihad D. Waspada,
for the catMaybes
advice.
With catMaybes
, this can even be simplified as below code:
module Step11cm where
import Prelude
import Data.Maybe
import Data.Array (filter, catMaybes)
import MySongs
import Effect.Console (log)
main = log $ show $ catMaybes $ (map _.tags songs)
_ There are still two more steps to go. Please be patient.
Finally Flattened
There is already standard concat
to flatten Array
.
module Step13 where
import Prelude
import Data.Array (concat, filter, catMaybes)
import Data.Maybe
import MySongs
import Effect.Console (log)
main = log $ show $ concat $ catMaybes $ (map _.tags songs)
_ With the result similar as below list:
$ spago run --main Step13
[info] Build succeeded.
["60s","jazz","60s","rock","70s","rock","70s","pop"]
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.
Applying Unique
Combining with previous custom made unique, we have this code below:
module Step14 where
import Prelude
import Effect.Console (log)
import Data.Array (concat, filter, catMaybes, nub)
import Data.Maybe
import MySongs
main = log $ show $ nub $ concat $ catMaybes $ (map _.tags songs)
_ With the result similar as below list:
$ spago run --main Step14
[info] Build succeeded.
["60s","jazz","rock","70s","pop"]
Alternative Approach
If you care to change the data structure,
omiting Maybe Tags
wrapper,
you can even write this as below:
main = log $ show $ nub $ concat
$ map (_.tags) $ songs
I am sure there are other ways to accomplish this task.
What is Next 🤔?
Consider continue reading [ ReasonML - Playing with Records ].