A collection of Either examples in PureScript compared with JavaScript
Note: This is Tutorial 5 in the series Make the leap from JavaScript to PureScript . Be sure to read the series introduction where I cover the goals & outline, and the installation, compilation, & running of PureScript. I will be publishing a new tutorial approximately once-per-month. So come back often, there is a lot more to come!
Index | << Introduction < Tutorial 4 Part 2 | Tutorial 6 > Tutorial 27 >>
Welcome to Tutorial 5 in the series Make the leap from Javascript to PureScript. I hope you’ve enjoyed the learnings thus far. Be sure to read the series Introduction to learn how to install and run PureScript. I borrowed (with permission) this series outline and javascript code samples from the egghead.io course Professor Frisby Introduces Composable Functional JavaScript by Brian Lonsdorf — thank you, Brian! A fundamental assumption is that you have watched his video before tackling the equivalent PureScript abstraction featured in this tutorial. And for this particular tutorial, you should also review his transcript. It has the imperative and FP code examples in Javascript that I used to port to PureScript. Brian covers the featured concepts extremely well, and it’s better that you understand its implementation in the comfort of JavaScript.
If you read something that you feel could be explained better, or a code example that needs refactoring, then please let me know via a comment or send me a pull request on Github. Finally, If you are enjoying this series then please help me to tell others by recommending this article and/or favoring it on social media
PureScript code organization
Each of the examples below shows the FP code snippet in JavaScript, followed by its port to PureScript. In my Github repository, you will find each code snippet in a separate PureScript file; importing ExampleX.purs
and calling it from Main.purs
. All my utility functions, including chain
and fromNullable
, and their corresponding FFI are in the Data
folder. Example 5 requires reading a JSON file, so I have created example.json
and put it in the folder ./src/resources
.
A few of the examples simulate receiving JSON objects and values over the wire. If I were doing this in production, I would probably use purescript-argonaut to parse the JSON and transform the objects and values into PureScript Records and basic types respectively. But I want to keep it simple, so I decided to access the JavaScript objects and their name/value pairs using the FFI. Furthermore, I treat them as Foreign
types until I print them to the console.
fromNullable and fromEmptyString
When handling Foreign
types returned from a Javascript function or, from JSON coming over the wire, there is always the possibility that they may be null
or undefined
. But in PureScript there are no null
or undefined
values, so we typically represent this concept by the value Nothing
from the Maybe
type. Think of Nothing
as something like a type-safe null
and, for now, we'll save further details on the Maybe
type for a later tutorial.
To match Brian’s code examples, I created fromNullable
to check whether a Foreign
type is null
or undefined
, returning Either Error Foreign
. Similarly, fromString
returns Either Error String
, depending on whether a string is empty. I am always looking for opportunities to DRY (Don't Repeat Yourself) out my code and found fromNullable
and fromString
to be very similar. So I abstracted the repetition into a separate function, toEither
. This method is another example of the benefits of PureScript's support for polymorphism. Notice how I was able to make Right x
a polymorphic type so that it works for both Foreign
, String
, and other types.
toEither :: forall a. Boolean -> String -> a -> Either Error a toEither predicate errorMsg value =
if predicate
then Left $ error errorMsg
else Right valuefromEmptyString :: String -> Either Error String fromEmptyString value = toEither (value == "") "empty string" value fromNullable :: Foreign -> Either Error Foreign
fromNullable value =
toEither (isNull value || isUndefined value)
"null or undefined" value
Example 1 — Point-Free style (tacit programming)
In PureScript and other FP languages, you’ll frequently find that, while a type declaration of a function states that it accepts arguments (or points), the actual arguments are missing in the implementation. We call this paradigm, point-free or tacit style programming, and it helps ‘sometimes’ to give a precise definition of the function. I said ‘sometimes’, because point-free can also obscure the meaning of a function; particularly when an argument name helps in understanding a function’s implementation. The PureScript example below has been written in a point-free style.
/* Example 1 - Javascript */const openSite = () =>
fromNullable(current_user)
.fold(showLogin, renderPage)
{- Example 1 - Purescript -}openSite :: Foreign -> String
openSite =
fromNullable >>>
either (\_ -> "showLogin") \_ -> "renderPage"
I chose point-free to illustrate this paradigm and to take advantage of function composition. That is, openSite = fromNullable >>> . . .
instead of openSite currentUser = (fromNullable currentUser) # . . .
But it is debatable whether it remains clear that it takes a current user. So, given that your mileage will vary, always proceed with caution when deciding whether to use point-free style.
Example 2
/* Example 2 - JavaScript */const getPrefs = user =>
(user.premium ? Right(user) : Left('not premium'))
.map(u => u.preferences)
.fold(() => defaultPrefs, prefs => loadPrefs(prefs))
{- Example 2 - PureScript -}getPrefs :: Foreign -> String
getPrefs user =
toEither (getPremium user) "not premium" user #
map getPreferences >>>
either (\_ -> defaultPrefs) \prefs -> "loadPrefs " <> prefs
Example 3 — Say goodbye to chain
/* Example 3 - JavaScript */const streetName = user =>
fromNullable(user.address)
.chain(a => fromNullable(a.street))
.map(s => s.name)
.fold(e => 'no street', n => n)
{- Example 3 - PureScript -}streetName :: Foreign -> String
streetName user =
(fromNullable $ getAddress user) >>=
(\address -> fromNullable $ getStreet address) >>=
(\street -> fromNullable $ getStreetName street) >>>
map (\name -> unsafeFromForeign name :: String) #
either (\_ -> "no street") id
Wait! What happened to chain
? Well, I have a secret that I've been keeping since the last tutorial - you can replace chain
with bind
(>>=
)! I know, shocking isn't it. Perhaps I should have come clean earlier, but I felt it was better to stick with chain
from Brian’s tutorials. So why and when can we replace chain
with bind
you ask? Let's look at their type declarations to see if that helps to illuminate things:
chain :: forall a b e. (a -> Either e b) -> Either e a -> Either e b bind :: forall a b. m a -> (a -> m b) -> m b
Nope, not really. Hold on a minute — let’s try looking at their implementations. First, chain
:
chain :: forall a b e. (a -> Either e b) -> Either e a -> Either e b
chain f = either (\e -> Left e) (\a -> f a)
Then, let’s look at the bind
class instance forEither
from Either.purs:
-- | The `Bind` instance allows sequencing of `Either` values and functions that
-- | return an `Either` by using the `>>=` operator:
-- |
-- | ``` purescript
-- | Left x >>= f = Left x
-- | Right x >>= f = f x
-- | ```
instance bindEither :: Bind (Either e) where
bind = either (\e _ -> Left e) (\a f -> f a)
Interesting, so bind
is quite similar to my implementation of chain
, but with the first two arguments flipped! Why does this work? Well, if bind
sees that the first argument is Left e
then, like chain
, it ignores the the second function argument (\a f -> f a)
, and returns the first argument, Left e
. But if the first argument is Right a
then the second function argument is applied. Finally, becausef a -> Either e b
, then Either e b
will be returned.
The next question is ‘when can I substitute chain
with bind
?’. Well, for all intents and purposes, they’re interchangeable! Consequently, you won't find a class instance of chain
for Either
in Either.purs.
Take a look at Example 6, where I've provided two versions of parseDbUrl
- one using chain
and the other using bind
. That example should help you to go back and refactor any functions using chain
to bind
. So . . . say goodbye to chain
and long live bind
!
Example 4 — Anonymous Function Arguments
/* Example 4 - JavaScript */
const concatUniq = (x, ys) =>
fromNullable(ys.filter(y => y === x)[0])
.fold(() => ys.concat(x), y => ys)
{- Example 4 - PureScript -}concatUniq :: String -> String -> String
concatUniq x ys =
filter (_ == x) ys #
fromEmptyString #
either (\_ -> ys <> x) \_ -> ys
This tip is straightforward and very useful in practice. In the PureScript example above, notice the expression filter (_ == x) ys
. You may be wondering why I didn't write it as filter (\y -> y == x) ys
. Well, because I'm using an anonymous function argument _
, represented in the predicate portion of the filter function. Think of _
as a little syntax sugar that helps to shorten your code. You'll be pleased to know that anonymous function arguments work for Records and other types of expressions, as well. You can learn everything you need to know about them in this blog post, which is part of the series '24-days-of-purescript-2016'.
Example 5 — let
vs. where
keywords
Before discussing the let
and where
keywords, let me mention that this code snippet makes good use of native side effects. That topic was well covered in Part 2 of Tutorial 4, so if you're still shaky on File IO and exception handling then go back and have a look. Now onto the use of where
vs. let
keywords in the duel wrapExample
snippets.
/* Example 5 - JavaScript */const readFile = x => tryCatch(() => fs.readFileSync(x)) const wrapExample = example =>
fromNullable(example.previewPath)
.chain(readFile)
.fold(() => example,
preview => Object.assign({}, example, preview))
{- Example 5 - PureScript -}wrapExample :: forall eff
. Foreign
-> Eff (fs :: FS, exception :: EXCEPTION | eff) Foreign wrapExample example =
fromNullable (getPreviewPath example) #
map (\path -> unsafeFromForeign path :: String) >>>
either (\_ -> pure example) wrapExample'
where
wrapExample' pathToFile =
(try $ readTextFile UTF8 pathToFile) >>=
either Left parseValue >>>
either (\_ -> example) (assignObject2 example) >>>
purewrapExample_ :: forall eff
. Foreign
-> Eff (fs :: FS, exception :: EXCEPTION | eff) Foreign wrapExample_ example =
fromNullable (getPreviewPath example) #
map (\path -> unsafeFromForeign path :: String) >>>
let wrapExample' pathToFile =
(try $ readTextFile UTF8 pathToFile) >>=
either Left parseValue >>>
either (\_ -> example) (assignObject2 example) >>>
pure
in either (\_ -> pure example) wrapExample'
We have seen the where
keyword many times before. It is usually at the top of a module, for the purpose of delineating the block of code represented by the module name. But, so far, I haven't used it inside a function. The purpose is the same - introduce a new block of code, indenting that code so that the compiler understands that where
is bound to the syntactic construct (i.e., the new block of code). In the example, you can see that where
is forever linked to the code that defines wrapExample'
.
Now, take a look at let
. At first blush, the purpose of where
and let
appear to be identical, and this is roughly correct. But there is a subtle difference! let . . . in . . .
is also an expression, and therefore can be written wherever expressions are allowed. The code snippet best explains this difference. I used let . . . in . . .
as an expression by inserting it between the map
and either
functions. I could have gone even further, with the following:
map (\path -> unsafeFromForeign path :: String) >>>
either (\_ -> pure example)
let wrapExample' pathFile = . . .
in wrapExample'
But, then it becomes a matter of readability.
For more information on let
vs. where
, check out Let vs. Where from wiki.haskell.org. Oh, and you are going to find that the syntax of Haskell is surprisingly similar to PureScript! I've heard a few PureScripters commenting that they were able to pick up Haskell much more quickly, thanks to having learned PureScript first. Likewise, I began learning Haskell first and found I was able to pick up PureScript more rapidly.
Example 6 — Regular expression validators and partial functions
First, before discussing regular expressions in PureScript, my reason for creating the dual code snippets below, parseDbUrl
and parseDbUrl_
, is to demonstrate how to port functions that use chain,
to use bind
instead. This code snippet will be the last time you will see chain
in my tutorials. And to learn why, then please review the discussion in Example 2.
/* Example 6 - JavaScript */const parseDbUrl = cfg =>
tryCatch(() => JSON.parse(cfg))
.chain(c => fromNullable(c.url))
.fold(e => null,
u => u.match(/postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/))
{- Example 6 - PureScript -}dBUrlRegex :: Partial => Regex
dBUrlRegex =
unsafePartial
case regex
"^postgres:\\/\\/([a-z]+):([a-z]+)@([a-z]+)\\/([a-z]+)$" noFlags of
Right r -> r matchUrl :: Regex -> String -> Either Error (Array (Maybe String)) matchUrl r url =
case match r url of
Nothing -> Left $ error "unmatched url"
Just x -> Right xparseDbUrl_ :: Partial => String -> Array (Maybe String)
parseDbUrl_ =
parseValue >>>
chain (\config -> fromNullable $ getDbUrl config) >>>
map (\url -> unsafeFromForeign url :: String) >>>
chain (matchUrl dBUrlRegex) >>>
either (\_ -> singleton Nothing) id parseDbUrl :: Partial => String -> Array (Maybe String)
parseDbUrl s =
(parseValue s) >>=
(\config -> fromNullable $ getDbUrl config) >>>
map (\url -> unsafeFromForeign url :: String) >>=
(\r -> matchUrl dBUrlRegex r) #
either (\_ -> singleton Nothing) id
Like JavaScript, PureScript supports the use of regular expressions very well — by wrapping JavaScript’s very own RegExp
object! The types and functions are part of the purescript-strings
library, located in the module Data.String.Regex
. First up is the Regex
object.
There’s some new pieces of syntax in theDbUrlRegex
function, namely Partial
and unsafePartial
. In this instance, they allow us to treat a non-exhaustive case expression as a regular case expression (unsafely). So why did I decide on unsafePartial
? I tested the regular expression "^postgres:\\/\\/([a-z]+):([a-z]+)@([a-z]+)\\/([a-z]+)$"
and I know it works for the sole purpose of demonstration in this tutorial! So no need to bother returning and dealing with an Either Error Regex
. You can also take advantage of unsafePartial
to return partial functions; again unsafely. And, as a consequence of DbUrlRegex
, that is exactly what I am doing in parseDbUrl
.
You’ll often hear from functional programmers the phrase, ‘Let the types be your guide’. So, with that mantra in mind, DbUrlRegex
is a partial function, so I declare that it belongs to the Partial
class (i.e., dbUrlRegex :: Partial => Regex
). This fact propagates all the way back to main
. I declare that parseDbUrl
returns a partial function and, consequently, in the main
code snippet below, I use the unsafeFromPartial
function to log the result to the console. So, as Eff
indicates the presence of side effects, Partial
is our way of telling future maintainers of our code that if you change the regular expression, you better be 100% sure that it works.
One final item — parseDbUrl
returns Array (Maybe String)
, and so you're probably wondering about the Maybe
constructor. As I mentioned in the fromNullable
and fromEmptyString
section, we'll cover that abstraction in a future tutorial.
Main program
defaultConfig :: String
defaultConfig = "{ \"url\": \"postgres:\\/\\/username:password@localhost/mydb\"}\n"main :: Effect Unit
main = do
log "A collection of Either examples"
log "Example 1"
log $ openSite getCurrentUser
log "Example 2"
log $ getPrefs getCurrentUser log "Example 3"
log $ streetName getCurrentUser
log "Example 4"
log $ concatUniq "x" "ys"
log $ concatUniq "y" "y" log "Example 5"
log "using where keyword in wrapExample"
example <- wrapExample getCurrentExample
log (unsafeFromForeign example :: String)log "Example 6"
log "Using bind to help parse the database URL"
logShow $ unsafePartial $ parseDbUrl defaultConfig log "Game Over"
In the next Tutorial we’re going to learn about Semigroups. Again, If you are enjoying this series then please help me to tell others by recommending this article and/or favoring it on social media. Thank you and till next time!