Note: This is Tutorial 24 in the series Make the leap from JavaScript to PureScript. Be sure to read the series introduction where we cover the goals & outline, and the installation, compilation, & running of PureScript. I’ll be publishing a new tutorial approximately once-per-month. So come back often, there’s a lot more to come!
In the last tutorial, we wrapped up our look at the Traversable
type class with an example of how to process a sequence of HTTP GET
requests using the member function traverse
. Now, we'll move on to natural transformations in functional programming - what they are and the laws they must obey. Then, in the next tutorial, we'll show that natural transformations are beneficial in everyday programming.
I borrowed this series outline, and the JavaScript code samples with permission from the egghead.io course Professor Frisby Introduces Composable Functional JavaScript by Brian Lonsdorf — thank you, Brian! A fundamental assumption is that you've watched his video on the topic before tackling the equivalent PureScript abstraction featured in this tutorial. Brian covers the featured concepts exceptionally well, and I feel it's better that you understand its implementation in the comfort of JavaScript.
You'll find the text and code examples for this tutorial on Github. 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. Also, before leaving, please give it a star to help me publicize these tutorials.
As Brian mentions in his video, a simple explanation of a natural transformation is that its a type conversion. It takes one functor holding an a
to another functor holding that same a
. You can think of it as a change in data constructors F a -> G a
.
Let's implement a natural transformation by turning an Either
data constructor into a Task
data constructor.
Recall from Tutorial 3 that Either
is used to express a computation that may or may not succeed. Either a b
always contains a value of a
or b
; defined by the constructors Left a
or Right b
, but never both at the same time. Right b
is the idiomatic designation of a successful computation, while Left a
means a failed computation. We naturally transform Either
to a Task
by mapping Left a
to taskRejected
and Right b
to taskOf
. Let's create this natural transformation eitherToTask
with the following JavaScript code:
const eitherToTask = e =>
e.fold(Task.rejected, Task.of)
In PureScript, the equivalent to e.fold
is the either
function from the Data.Either module. Using point free style, the equivalent function in PureScript is:
type Either = String
eitherToTask :: forall a. Either Error a -> TaskE Error a
eitherToTask = either (\e -> taskRejected e) (\a -> taskOf a)
main :: Effect Unit
main = do
void $ launchAff $
eitherToTask (Right "Nightingale") #
fork (\e -> Console.error $ "Error: " <> e) (\s -> Console.log $ "Result: " <> s)
With the help of a type alias, we designate our Error
type to be a String
. Logging this to the console we get Result: Nightengale
. Calling eitherToTask
with a (Left "errrrr")
argument will console.error
the string Error: errrrr
.
Now, let's try another example using our old friend Box
from Tutorial 2.
First, Brian's example in JavaScript:
const boxToEither = b =>
b.fold(Right)
const res = boxToEither(Box(100))
The equivalent in PureScript is:
type Error = String
boxToEither :: forall b. Box b -> Either Error b
boxToEither (Box b) = Right b
Compared to the JavaScript example, notice that we don't provide a fold operation in our PureScript code. Instead, we leverage Purescript's pattern matching capabilities to take our b
from Box
and put it into Either
's Right
directly. If you try compiling boxToEither (Box b) = Left b
, you'll get an error. Why? Look at boxToEither
's type signature: forall b. Box b -> Either Error b
. We see that when Box b
is transformed, our b
is defined by the Right b
constructor. If the compiler permitted us to chose Left b
, then it would be invalid because it violates the laws of natural transformations, described below.
We can express the laws of natural transformations with the following:
- For every element
a
, and the functorsF
andG
, the natural transformationnt
betweenF
andG
is a morphism (i.e.,mapping
) fromF a
toG a
. - For any natural transformation
nt
, when you transform any elementa
and map over it, then it is equivalent to mapping overa
, then applying the natural transformation to the result. This is a commutative operation that can be expressed as:map (a -> b) -> nt a -> nt b == nt $ map (a -> b) -> f a -> f b
.
The commutative diagram below helps to visualize the natural transformation laws expressed above:
If we have an element a
contained within the functor F
and we map over it with a function map (a -> b)
, then we obtain F b
. Furthermore, if we apply the natural transformation nt
to F b
, we get G b
. Going the other way, if instead we first apply the natural transformation nt
to F a
to obtain G a
, then we map over it with the same function (a -> b)
we reach the same result G b
.
Let's apply these laws to confirm that boxToEither
is a natural transformation:
-- The left side of the commutative expression
-- (a -> b) -> nt a -> nt b
res1 :: Either Error Int
res1 = map (\x -> x * 2) (boxToEither $ Box 100)
-- The right side of the commutative expression
-- nt $ (a -> b) -> f a -> f b
res2 :: Either Error Int
res2 = boxToEither $ map (\x -> x * 2) (Box 100)
When we log the results of both functions, the console output is Right 200
, confirming that boxToEither
is a natural transformation. Let's apply these laws to one more example, head
, which takes the first element of Array Int
and naturally transforms it to Maybe Int
:
-- (a -> b) -> nt a -> nt b
res3 :: Maybe Int
res3 = (\x -> x + 1) <$> head [1, 2, 3]
-- nt $ (a -> b) -> f a -> f b
res4 :: Maybe Int
res4 = head $ (\x -> x + 1) <$> [1, 2, 3]
The head
function gets the first a
element in an array, returning Just a
or Nothing
when the array is empty. Here again, logging the result to the console produces: Just 2
for both functions. Whenever the array is empty, then both functions return Nothing
.
In this tutorial, we looked at a natural transformation - what it is, and what laws it obeys. A simple explanation of a natural transformation is that it is a type conversion. It takes one functor holding an a
to another functor holding that same a
. You can think of it as a data constructor change F a -> G a
. Digging further, we find that natural transformations obey the law of commutativity. Such that map (a -> b) -> nt a -> nt b == nt $ map (a -> b) -> F a -> F b
, where nt
is a natural transformation from the functor F
to a functor G
.
In the next tutorial, we'll continue with natural transformations by looking at examples of where they come in handy. That's all for now. If you are enjoying these tutorials, then please help me to tell others by recommending this article and favoring it on social media. Till next time!