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: 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:

  1. Andika Rizary
  2. Jihad D. Waspada
  3. 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"]

Purescript: Array of String

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

Purescript: Song Data Structure

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:

Purescript: Using The Module


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:

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 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 as map (_.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"]

Purescript: Finalization

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

Purescript: Simpler Approach

I am sure there are other ways to accomplish this task.


What is Next 🤔?

Consider continue reading [ ReasonML - Playing with Records ].