23

Functional Programming for the Web: Monads and Basic DOM Manipulation in PureScr...

 4 years ago
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.

vY7j22r.jpg!web

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.

3u2Yjii.png!web

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.html
Server 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK