Functional Programming for the Web: Monads and Basic DOM Manipulation in PureScr...
source link: https://medium.com/@KevinBGreene/functional-programming-for-the-web-monads-and-basic-dom-manipulation-in-purescript-fa662fe57a4c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Source: PureScript Tutorial
Previously in this Series
PureScript in the Browser
The DOM in inherently impure. Declarative UIs like those made popular by React mitigate this problem. When we move to a purely functional environment we need abstractions that allow us to maintain purity while getting done what we need to get done. The secret here are monads.
There are declarative UI packages, including bindings for React, written for PureScript. I’ll look at declarative UI later. This article is going to look at lower-level DOM manipulations with purescript-web-dom
and purescript-web-html
. When I say lower-level I mean these packages provide functions and types that are based on the W3C specs for the DOM and HTML5. We will look at running querySelector
and reading textContent
from within PureScript.
There are several reasons for focusing on these lower-level packages. One is that it’s kind of like starting with the basics before building up to more powerful abstractions. The bigger reason for me though is that it provides a nice context in which to introduce and discuss monads. Monads used to be a very cryptic and misunderstood construct. They were made to sound much more complex than they really are. If you understand how to use a Promise
you are 90% of the way to understanding monads.
Back to the Code
Moving on to DOM manipulation also means that we are going to need an HTML page to manipulate. I’m going to start back in the project directory I left off with in the lastarticle and install Parcel . Parcel is a web application bundler similar to Webpack.
$ npm i -D parcel-bundler
The great thing about Parcel is that it needs no configuration for development. Just point it at your HTML file and it will figure out what it needs to bundle. Which means we need an HTML file. I make mine at the top level of my project directory index.html
.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello, PureScript!</title> </head> <body> <script src="./index.js"></script> </body> </html>
The only thing of interest here so far is pointing the script src to ./index.js
. What is this file? When we left off I had created a file called runner.js
that imported and called the main
function of our generated PureScript.
const { main } = require('./output/Main'); main();
I’m just renaming that file to index.js
.
To generate our bundle we need to tell Parcel about our new HTML file. Over in package.json
I add a new script.
"scripts": { "dev": "parcel index.html", "test": "echo \"Error: no test specified\" && exit 1" },
After the first article in this series I was left with a Main.purs
that looks like this:
module Main whereimport Prelude import Effect (Effect) import Effect.Console (log) import Shapes (Shape (Circle), area)main :: Effect Unit main = log (show (area (Circle { x: 0.0, y: 0.0} 5.3)))
Now if I run Parcel we should see the area of our Circle
log out in an empty page.
$ npm run dev
> parcel index.html
Server running at http://localhost:1234
Built in 274ms.
Okay, so Parcel started a dev server on port 1234
. If I head over there I see by log.
Cool, so we’re running PureScript in a browser. This will work. There is a lot you could do with PureScript this way, pure computations you could write and then import into your JavaScript.
Hello, Monad
So what are monads? I said this would be key to us working with the DOM. Monads are data with additional context. There are rules that differentiate monads from other types like functors, but in essence they are container types.
In JavaScript a container type may be as simple as:
class Container { consturctor(val) { this.value = val; } }
What is the point of putting a value inside of a container? It allows us to provide a consistent API for working with the value based on the context of the value. Going back to Promise
, a Promise
represents a potentially future value. We have a convenient method then
or sugar await
to access the value when it is available. The future state of the value is its context and then
is our consistent API for working with the value.
You’ve seen something like this with arrays.
class Container { consturctor(val) { this.value = val; } map(fn) { return new Container(fn(this.value)); } }
The map
function allows you to change the value inside of the container. I mentioned functor earlier. You can think of a functor as a type that can be mapped. So what is a monad?
So, map
means we have a functor. Monads have two associated functions pure
and bind
. You will see different names for these, but once you know what they do they’ll be easy to recognize regardless of name.
The simpler of the two is pure
. The pure
function takes a value and puts it inside the monad.
class Container { consturctor(val) { this.value = val; } static pure(val) { return new Container(val); } }
Like I said, that should be easy enough to understand. You’ll see this a lot in JavaScript as a create
static method.
The bind
function is a little more interesting, but not that bad. It is very similar to map
. What did map
do? It took a function that operated on a value of the type found in the container. It applied that function to the contained value and put the result into a new container and returned that.
The bind
function is similar here in that it takes a function that operates on a value of the type in the container. It differs in that it doesn’t return a value of the same type. It returns a new instance of the container.
class Container { consturctor(val) { this.value = val; } bind(fn) { return fn(this.value); } }
So, in JavaScript, the bind
method wouldn’t be implemented as static
it needs to be an instance method that has access to the value of the instance. It still returns a new instance of Container
. It’s just that new instance is created by the function.
const c = new Container(5); const f = c.bind((val) => new Container(val + 6));
Why then are monads important for working with the DOM in a functional way? The DOM is unreliable. Any random code that gets loaded on the page can manipulate it. It’s not part of our app. It’s something we work with. One of the most common monads you’ll work with is the Maybe
monad. It represents values that can possibly be empty, null, void… whatever you want to call it… values that are unreliably present.
First Steps with the DOM
We have a little more set up to do before we really get to work. I said we were going to use a couple packages. We need to install those.
$ npx spago install web-dom web-html
The Github descriptions for these libraries are “Type definitions and low level interface implementations for the W3C HTML5 spec” and “Type definitions and low level interface implementations for the W3C DOM spec”. Pretty similar. As they say, these are low level interfaces and you would probably using higher level abstractions when building real apps. However, this is a good place to start to get our feet wet.
First, let’s add a little more color to our index.html
file.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello, PureScript</title> </head> <body> <div id="root">Hello, PureScript!</div> <script src="./index.js"></script> </body> </html>
Yeah, so this is very little color.
What we want to do is just read the text from the root div and log it. It’s like we’re starting all over with HTML and JavaScript.
Let’s go back to our Main.purs
and start over.
module Main whereimport Preludeimport Effect (Effect) import Effect.Console (log)main :: Effect Unit main :: log "What do we do now?"
Okay, so where to now? If we were just writing JavaScript what would we do. We would select the div
and read the textContent
. How do we do that here? These two packages we installed are implementations of the W3C spec. The functions we want are probably there.
In Web.DOM.ParentNode
there is a function called querySelector
with this signature:
querySelector :: QuerySelector -> ParentNode -> Effect (Maybe Element)
This function takes a QuerySelector
and a ParentNode
and returns an Effect
of Maybe Element
. So, what is a QuerySelector
?
Newtype
If we look in the same package we find this:
newtype QuerySelector = QuerySelector String
In part one of this series we briefly introduced the data
keyword for defining algebraic data types. The newtype
keyword is similar. It creates a very specific kind of algebraic data type. It creates a type with a single data constructor that takes exactly one parameter. Here QuerySelector
has one data constructor, also called QuerySelector
, that takes one argument, a String
.
You can think of this as a type alias. In TypeScript an alias would look like this:
type QuerySelector = string;
There is a key difference here. A type created with newtype
is unique to the type system. You can’t say directly assign a String
to a QuerySelector
. You must use the data constructor to create a QuerySelector
.
This is useful to ensure that you don’t use types incorrectly. If you had an application where you were representing currencies of different countries as numbers the type system could give you no guarantees that you were using them correctly, but if you had different types for dollars, euros, yen, pesos… the type system could guarantee you didn’t mix them up.
As an example let’s go over to Main.purs
and create a new QuerySelector
. We start by importing QuerySelector
.
import Web.DOM.ParentNode (QuerySelector(..))
So, this is a good bit of review. There are two things we are importing here. The first is the type QuerySelector
and the second is the data constructor QuerySelector
. In the import, the name we are importing is the type and in the parens we import the data constructors. For QuerySelector
there is only one data constructor. The two dots inside the parens means to import all data constructors. So for our case QuerySelector(..)
is the same as QuerySelector(QuerySelector)
. In the second example there we are importing data constructors by name.
We are then free to use our imported type and data constructor. We want to select a div
with the id root
. That give us this:
rootSelector :: QuerySelector rootSelector = QuerySelector ("#root")
The name rootSelector
is bound to a QuerySelector
that will match elements with the CSS selector #root
.
All together:
module Main whereimport Preludeimport Effect (Effect) import Effect.Console (log)import Web.DOM.ParentNode (QuerySelector(..))rootSelector :: QuerySelector rootSelector = QuerySelector ("#root")main :: Effect Unit main :: log "What do we do now?"
Type Hell
Going from JavaScript to PureScript can be pretty shocking. JavaScript is about as dynamic as you can get and PureScript is very strict. It is very strict with its types. That’s a big part of the reason why if your program compiles you can have a good deal of confidence it will be stable. That doesn’t mean your logic is correct, but it does mean that the flow of data in your application makes sense.
Now that we have a QuerySelector
how do we use it? The querySelector
function took two arguments, the QuerySelector
and a ParentNode
. What is a ParentNode
and how do we get one? In JavaScript we would write document.querySelector
, or window.document.querySelector
. How do we get a reference to the document?
Over in Web.HTML.Window
we find a function called document
. This function has the following signature:
foreign import document :: Window -> Effect HTMLDocument
The foreign import
part of this isn’t important now, but what it means is this function is defined in JavaScript. We’ll trust the package author implemented correctly and the result is a function that takes Window
and returns Effect HTMLDocument
. So, a new question, how do we get the Window
? One level up, in Web.HTML
we find the definition of window
.
foreign import window :: Effect Window
So, window is just an Effect Window
. You may be noticing that Effect
is used to wrap anything we go ask JavaScript for. JavaScript is unsafe. The Effect
monad protects us.
Composing Monads
It seems like we have most of the pieces we need. How do we line all of this stuff up?
Let’s go back to both our previous discussion of monads and what we already know about Promise
. We discussed that monads define a bind
function and this bind
is what allows us to use the value inside the monad. If you think about a Promise
, that is what the then
method is. Chaining monads is a lot like chaining Promises
.
As we saw with (+)
, PureScript allows us to define infix functions that look like operators. These functions can use characters we don’t usually associate with function names. The bind
function here is defined as >>=
.
Back in Main.purs
we can start laying this out like this:
main :: Effect Unit main = window >>= document
What does this do? It takes Effect Window
and gives the Window
to the function on the right of >>=
. In this case document
, a function that takes Window
and returns Effect HTMLDocument
. This won’t compile because the output of our main
function is now Effect HTMLDocument
and not Effect Unit
. We could change the return type of main
and this would compile. If you want to test this you would need to import HTMLDocument
from Web.HTML.HTMLDocument
.
import Web.HTML.HTMLDocument (HTMLDocument)
Now we need to take our HTMLDocuemnt
and run the querySelector
function with it. If you remember the querySelector
function didn’t take an HTMLDocument
it took a ParentNode
. An HTMLDocument
should always be a ParentDocument
. From the Web.HTML.HTMLDocument
package there is a function called toParentNode
which does exactly what it says. Let’s import that.
import Web.HTML.HTMLDocument (toParentNode, HTMLDocument)
Now with that piece and our previously defined rootSelector
we should be able to satisfy the querySelector
function with something like this:
querySelector rootSelector (toParentNode doc)
Let’s clean this up as a helper function.
selectFromDocument :: HTMLDocument -> Effect (Maybe Element) selectFromDocument doc = querySelector rootSelector (toParentNode doc)
The interesting thing here is we return a Maybe Element
. There is no guarantee that querySelector
actually finds an Element
with the given QuerySelector
so it must represent that possible failure. The Maybe
type has two data constructors Just
and Nothing
. Just
represent a value and Nothing
represents the lack of a value.
This will work in our main
function where we have been chaining functions that take some type and return an Effect
of some other type.
main :: Effect Unit main = window >>= document >>= selectFromDocument
Next we need to get the textContent
of a Maybe Element
. There’s probably a function ready for us named textContent
. Over in Web.DOM.Node
we find this:
foreign import textContent :: Node -> Effect String
That’s not exactly what we need. We need to take a Maybe Element
and get a Node
. Let’s write a function to help us with this. Starting with the type signature of what we want to do.
maybeText :: Maybe Element -> Effect String
We have a Maybe Element
and we want to end up with a String
and because we’re dealing with the unreliable DOM API we’ll end up with an Effect String
.
Maybes
are very common in PureScript. Whenever you are writing a function that takes a Maybe
it is very likely you will be using pattern matching to have different branches for the Just
case and the Nothing
case.
The other thing we need to to is figure out how to get a Node
from the Element
. As you may know Element
is a more specific type than Node
. So any Element
should be easily convertible to a Node
. Over in the Web.DOM.Element
package we find a function toNode
that casts an Element
to a Node
.
toNode :: Element -> Node
In the Just
case of our maybeText
function we’ll use this function to coerce to Node
and then use the result as an argument to textContent
to get our Effect String
.
In the Nothing
case we’ll return an empty string. We could change maybeText
to return a Maybe Effect String
and return a Nothing
in the Nothing
case, but for this function an empty string makes sense to me. It also makes our job easier down the road.
That ends up looking like this:
maybeText :: Maybe Element -> Effect String maybeText (Just el) = textContent (toNode el) maybeText _ = pure ""
Most interestingly here is we are using the pure
function to take an empty string and wrap it in an Effect
. PureScript knows what monad we are wrapping into because of the return type of our function.
Now we can go back to our main
function and include this in our composition.
main :: Effect Unit main = window >>= document >>= selectFromDocument >>= maybeText
The last thing we need to do is log
the result of maybeText
. If we remember log
takes a String
and returns Effect Unit
. That’s the final return type we want. Currently we have an Effect String
. We can use the bind (>>=)
function again to compose log
with what we already have.
main :: Effect Unit main = window >>= document >>= selectFromDocument >>= maybeText >>= log
We can now compile and run our code.
First, compile with spago build
.
$ npx spago build
Then bundle and run via Parcel.
$ npm run dev
> [email protected] dev /Users/kevingreene/Documents/purescript-tutorial
> parcel index.htmlServer running at http://localhost:1234
Open the given URL and we should see our log when we open up dev tools.
For completeness, this is what our program looks like now with all imports:
module Main whereimport Preludeimport Data.Maybe (Maybe(..))import Effect (Effect) import Effect.Console (log)import Web.HTML (window) import Web.HTML.Window (document) import Web.HTML.HTMLDocument (toParentNode, HTMLDocument)import Web.DOM.Element (Element, toNode) import Web.DOM.Node (textContent) import Web.DOM.ParentNode (querySelector, QuerySelector(QuerySelector))rootSelector :: QuerySelector rootSelector = QuerySelector ("#root")maybeText :: Maybe Element -> Effect String maybeText (Just el) = textContent (toNode el) maybeText _ = pure ""selectFromDocument :: HTMLDocument -> Effect (Maybe Element selectFromDocument doc = querySelector rootSelector (toParentNode doc)main :: Effect Unit main = window >>= document >>= selectFromDocument >>= maybeText >>= log
This works and once you get used to looking at the bind (>>=)
function the result is much like calling then
on a sequence of Promises
. The similarity doesn’t stop there. With Promises
we now have async
functions that allow us to write async
code in a way that looks synchronous.
Do Notation
The do
notation is the way we can write a series of composed monadic operations in a way that looks more imperative. Before we get into specifics I’m going to show what the end result give us. We can rewrite our main
function as:
main :: Effect Unit main = do w <- window d <- document w el <- selectFromDocument d str <- maybeText el log str
We start with the do
keyword. What follows is a series of operations that each return a monad, in this case the Effect
monad. The first line w <- window
can be read as assign the result of window
to w
. The name w
can then be used as the value inside the Effect
, here Window
. The arrow <-
then isn’t read exactly as assignment. It’s read as take the inner value of the monad on the right and assign that inner value to the name on the left.
This allows us to pass w
directly to the document
function. This continues each step. This format probably looks more familiar to you and make long monadic compositions much easier to read even for experienced functional programmers.
In do
notation the last line must be an expression (not an assignment via <-
). For us the expression is to log
our result log str
. The entire do
notation expression is then equal to the type of the final expression Effect Unit
here.
We can rebuild. If Parcel is still running in one terminal window, you can run spago build
in another window and Parcel will automatically pick up the change.
$ npx spago build
And check the browser again.
Exact same result.
That’s it for this article. We covered quite a lot of interesting ground when it comes to working in PureScript. Composing monadic operations is going to be key to our future growth. In the next article we will look at doing more complex work with the DOM and also introduce PureScript’s foreign interface API for writing code in JavaScript and exposing it in PureScript.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK