Purescript: Playing with Records

epsi

E.R. Nurwijayadi

Posted on December 22, 2020

Purescript: Playing with Records

This post was originally posted on my personal blog šŸ˜€. The source code example also provided in github, given in that blog.

šŸ•· Purescript - Playing with Records

šŸ•· Haskell - Playing with Records

Goal

Goal: A step by step practical case to collect unique record fields. Using Purescript from Haskell mockup.

The task is simply to collect unique tags from records. I have done it using Javascript, and Typescript. And I want to know how it looks like with Purescript.

The issue is, I never code in Purescript before. Luckily I have learnt Haskell a few years ago. So I decide to make a mockup from Haskell. Rewrite from javascript to haskell takes an adaptation, but after a while, it is not that hard to do.

It turned out, that it is easy to rewrite, from Haskell to Purescript. But beware of the differences.

While it is comfortable to work with list in Haskell, Purescript utilize array, that is more compatible with Javascript.

This might cause an issue, for custom algorithm. For example purpose, we are going to use algorithm, for collecting unique array/list. Of course we can do it with nub standard library. I just want to show the reader something more fundamental, just in case anyone need to make custom function in Purescript.

Let's get it on.


Data Structure: Original in Javascript

Consider a case example of some popular songs from the old decades.

With javascript, I can just write away the records in below form:

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

export default songs;
Enter fullscreen mode Exit fullscreen mode

Data Structure: Ported to Haskell

With Haskell, I can strictly append additional type signature.

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 }
  ]
Enter fullscreen mode Exit fullscreen mode

Data Structure: Ported to Haskell

I also attach Maybe as a nullable option, so we can adapt while no tags data is available.


Data Structure: Ported to Purescript

The same applied with purescript. Except we use array instead of list.

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

Enter fullscreen mode Exit fullscreen mode

Data Structure: Ported to Purescript


Functional: in Javascript

These days with ecmascript 2019 we can use flatMap, so the code can be shorter.

import songs from "./songs-data.js";

const unique = array => [... new Set(array)];

const allTags = unique(songs
  .filter(song => song.tags)
  .flatMap(song => song.tags)
);
console.log(allTags );
Enter fullscreen mode Exit fullscreen mode

Functional: in Javascript


Oneliner Solution: in Haskell

We can also make a oneliner statement in Haskell. Using catMaybes to extract values from list of Maybes. Using concat to flatten the list. And finally using nub standar library to filter just the unique values.

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)
Enter fullscreen mode Exit fullscreen mode

With the result similar to below:

$ runghc 14-songs-tags-unique.hs
["60s","jazz","rock","70s","pop"]
Enter fullscreen mode Exit fullscreen mode

Oneliner Solution: in Haskell

Short enough to makes me happy. If you ever write a Haskell codes, you should know how it works.

Oneliner Solution: in Purescript

It is a little bit longer in Purescript. Because we need more import statement, compared to haskell counterpart.

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)
Enter fullscreen mode Exit fullscreen mode

With the result similar to below:

$ spago run --main Step14
[info] Build succeeded.
["60s","jazz","rock","70s","pop"]
Enter fullscreen mode Exit fullscreen mode

You can spot, that in Purescript we are using (map _.tags songs) to access field of the each songs record, instead of (map tags songs) in Haskell.

Oneliner Solution: in Purescript

There are details to achieve this in provided link above. I just want this notes to be short. So I strip out the details here.


Custom Unique Function: in Haskell

We are done with introduction. Now let me write a custom unique function, based on some stackoverflow and coderosetta.

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)
Enter fullscreen mode Exit fullscreen mode

With the result similar to below:

$ runghc 07-tags-unique.hs
["rock","jazz","pop"]
Enter fullscreen mode Exit fullscreen mode

The thing is, the code is using x:xs pattern matching. This could be a troublesome while working with array in purescript, since x:xs pattern matching is made for list. And we are working with array in purescript.

For you who never heard x:xs pattern, it is easier to understand, if we write the pattern as, head:tail instead of x:xs. Where the head is the first element, and the tail are the rest elements in list or array.


Custom Unique Function: in Purescript

Now, how is it going to be in Purescript šŸ¤”?

Well, we can utilize uncons. Then recursively cons the tails.

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)
Enter fullscreen mode Exit fullscreen mode

With the result similar to below:

$ spago run --main Step07
[info] Build succeeded.
["rock","jazz","pop"]
Enter fullscreen mode Exit fullscreen mode

No need to take hours to find replacement for this x:xs pattern in array.


Conclusion

That is all. You can use the pattern for your own custom algorithm.

What do you think?

šŸ’– šŸ’Ŗ šŸ™… šŸš©
epsi
E.R. Nurwijayadi

Posted on December 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Purescript: Playing with Records
purescript Purescript: Playing with Records

December 22, 2020

Scripting in Haskell and PureScript
purescript Scripting in Haskell and PureScript

July 8, 2019