Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get to inner Maybe monad to extract class name from html button in purescript?

I am attempting to learn purescript.

I have a button in some HTML that I am trying to print the class name of. I am building and browserifying using pulp.

The function I am using is querySelector:

import Web.DOM.ParentNode (querySelector)

This returns the item I want, Element, within two "boxes": an outer Effect monad and an embedded Maybe monad:

> :type querySelector
QuerySelector -> ParentNode -> Effect (Maybe Element)

My Effect monad looks like:

getBtn ::  Effect Unit
getBtn = do
  doc <- map toParentNode (window >>= document)
  button <- querySelector (wrap "#btn") doc
  ... Need to extract class name from 'button' here

I know I can get to the inner Maybe by calling bind (>>=) on the outer Effect monad. My first plan of attack was to unbox the Maybe manually using the maybe funtion. Here's how you do this for a simple Int:

> maybe 0 (\x -> x) $  Just 7
7

for a Maybe Element I think it would look something like:

unboxMaybeElement :: Maybe Element -> Element
unboxMaybeElement Nothing = Nothing
unboxMaybeElement (Just a) = maybe (Element ..note: trying to create a default element here) (\x -> x) a 

One problem is I cannot find a constructor for type Element, so I am unable to supply a default value (the first arg of maybe), so I can't use this method.

In addition, I can't find a definition for the Element type.

I also read that it's not a good idea to reach inside a Maybe anyway, but instead to lift the functions going against it by using a functor (like map or fmap).

In this vein I tried to call an external function like:

  button <- querySelector (wrap "#btn") doc
  let cn = getClassName button
  log $ "classname=" <> show cn

-- The basic question is how to implement this function?
getClassName :: Maybe HTMLElement -> String
getClassName e = map className  (map fromElement e)

Note: className and fromElement are supplied in Web.HTML.HTMLElement.

As you can see, I'm trying to promote up the functions by calling them with a map, because the functions do not deal with Maybes, but I'm getting stuck in a morass of type conflicts between "Maybe Element" and "Element" and "HTMLElement" vs "Element" etc.

Yes, I admit I have a couple of issues going on here, and this isn't really a beginners problem, but it just irks me that something so simple as obtaining the class name of an HTML object is so hard. Surely, I must be missing something?

BTW, Here is my html:

<!doctype html>
<html>
  <body>
    <script src="index.js"></script>
    <p>
      <input type="button" class="btn-class" value="click me" id="btn"/>
    </p>
  </body>
</html>

The more general question is: how do you get to values nested two monad levels deep?

like image 704
vt5491 Avatar asked Aug 10 '18 07:08

vt5491


2 Answers

Actually you did a good job by investigating this problem on your own, I can just suggest refactoring based on the example, that you've provided.

Let's say we don't care about logging failed messages on a particular case. That means, that we can replace every such a logging by pure unit:

getBtn ::  Effect Unit
getBtn = do
  log "now in getBtn"
  doc <- map toParentNode (window >>= document)
  mbtn <- querySelector (wrap "#btn") doc
  case mbtn of
    Nothing -> pure unit
    Just btn -> do
      let mhtmlEl = fromElement btn
      case mhtmlEl of
        Nothing -> pure unit
        Just htmlEl -> do
          let cn = className htmlEl
          log $ "classname below:"
          cn >>= log

Then we can notice an interesting pattern here:

case mbtn of
  Nothing -> pure unit
  Just btn -> do
    let mhtmlEl = fromElement btn

i.e we want to apply fromElement function on a btn value if it Just btn or do nothing if it's Nothing. The resulting type must be Maybe value.

The first thought is using map function, but the type of fromElement is fromElement :: Element -> Maybe HTMLElement, so the resulting type will be Maybe (Maybe a)).

Anyway, we can even search such a function by type and the first result is bind (the same as (>>=)). So let's refactor (with pointing the types as comments just for clarity, but sure it's not necessary):

getBtn ::  Effect Unit
getBtn = do
  log "now in getBtn"
  doc <- map toParentNode (window >>= document)
  mbtn <- querySelector (wrap "#btn") doc

  let mhtmlEl = fromElement =<< mbtn -- Maybe HTMLElement

  case mhtmlEl of
    Nothing -> pure unit
    Just htmlEl -> do
      let cn = className htmlEl
      log $ "classname below:"
      cn >>= log

The next step is to reduce another case expression. Here's using map (the same as <$>) will be enough:

getBtn ::  Effect Unit
getBtn = do
  log "now in getBtn"
  doc <- map toParentNode (window >>= document)
  mbtn <- querySelector (wrap "#btn") doc

  let mhtmlEl = (fromElement =<< mbtn) -- Maybe HTMLElement
  let mCn = className <$> mhtmlEl -- Maybe (Effect String)

  case mCn of
      Nothing -> log "no such element"
      Just cn -> do
          log $ "classname below:"
          cn >>= log

The resulting type of getBtn function, must be Effect Unit, so every case of Maybe value must be processed here. I would leave it like this, since it quite clear, what's going on here. But if you seeking a concise representation, then maybe function can be applied here:

getBtn ::  Effect Unit
getBtn = do
  log "now in getBtn"
  doc <- map toParentNode (window >>= document)
  mbtn <- querySelector (wrap "#btn") doc

  let mhtmlEl = (fromElement =<< mbtn) -- Maybe HTMLElement
  let mCn = className <$> mhtmlEl -- Maybe (Effect String)

  maybe (log "no such element") (\cn -> log "classname below:" *> cn >>= log) mCn
like image 104
Igor Drozdov Avatar answered Oct 18 '22 02:10

Igor Drozdov


Based on some similar code I saw in this video, I was able to get something that compiled. I'm not saying this is the best way, or even a good way, just that it works:

getBtn ::  Effect Unit
getBtn = do
  log "now in getBtn"
  doc <- map toParentNode (window >>= document)
  mbtn <- querySelector (wrap "#btn") doc
  case mbtn of
    Nothing -> log "mbtn failed"
    Just btn -> do
      let mhtmlEl = fromElement btn
      case mhtmlEl of
        Nothing -> log "mhtml failed"
        Just htmlEl -> do
          let cn = className htmlEl
          log $ "classname below:"
          cn >>= log
          pure unit
      pure unit
  pure unit
pure unit

The "m" on some variable names ("mbtn", "mhtmlEl") is presumably to denote it's a wrapped object i.e. monadic. It's important that in your case Just statements you specify a different variable than in the case of statement, one without an "m" in front ("btn", "htmlEl"), to denote it's a raw or unwrapped value.

the pure unit lines are essentially dummy lines to appease the requirement that a monad always returns something other than a "binder" (a let or '<-' assignment), so you may or may not need them depending on how your do workflow is constructed.

There are effectively two levels of Maybe in this example: one on the querySelector result and one on the fromElement result. Add in another do to process the raw HTMLElement and you're up to three levels of do.

Unfortunately, the querySelector appears to be returning Nothing for me, but this is an orthogonal issue. That was the whole motivation for printing the className -- to determine if it was finding the element or not.

like image 1
vt5491 Avatar answered Oct 18 '22 01:10

vt5491