How to unwrap a Maybe in Purescript?

I am using match in Data.String.Regex to extract value from a string. I got the value wrapped in Maybe, how can I extract it?

My code:

import Data.Maybe (Maybe, fromJust)
import Data.Array.NonEmpty.Internal (NonEmptyArray)
import Data.String.Regex (Regex, test, regex, match)
import Data.String.Regex.Flags (RegexFlags, noFlags, global)

valueRegex :: Regex
valueRegex = unsafeRegex "\\d[\\w%]*" global

getValues :: String -> Maybe (NonEmptyArray (Maybe String))
getValues str = match valueRegex str
(Just (NonEmptyArray [(Just "20px"),(Just "30px"),(Just "40px")]))

I have tried to use unsafePartial to extract the value with fromJust, but I still got the error:

"clamp(20px, 30px, 40px)" # getValues # unsafePartial # fromJust
  Could not match constrained type

    Partial => t1

  with type


    Maybe (NonEmptyArray (Maybe String))

How can I extract the value from Maybe properly?

Doc:
https://pursuit.purescript.org/packages/purescript-strings/4.0.1/docs/Data.String.Regex#v:match

You should not do that. What if the regex doesnā€™t match the string? Youā€™ll get an error. You should handle both the Just and Nothing case.

I recall that you came from JS/TS. In those languages, youā€™d typically throw an error when you want to do something like this here. But in pure FP languages like PureScript, you should not use unsafe functions unless you are confident that nothing wrong will happen. Instead, you should use data types like Maybe and Either to convey the possibility of errors. There are multiple reasons for this. I can explain them if you are interested.

@mhmdanas Thank you so much for your reply. Yes I come from a JS background and I have used Ramda and Sanctuary.js before. I understand the meaning for Maybe was to avoid exception, but I canā€™t find a way to extract value from Maybe in Purescript.

I have asked the same question in slack, and now I know I can use Maybe effectively with pattern-matching and functions like Map, but I still have no clue how to extract value from it. It would be great if you can write a short code example for demonstration.

Iā€™m not sure this is exactly what you want, but I think itā€™ll help you use Maybe more effectively:

addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe a b = do
  a' <- a
  b' <- b
  pure (a' + b')

foo = addMaybe (Just 1) (Just 2) -- Just 3

bar = addMaybe (Just 2) Nothing -- Nothing

baz = addMaybe Nothing Nothing -- Nothing

Basically, you could say that we lifted the add function to a Maybe context. You may have heard this before, but Iā€™ll say it just in case: the do notation that I used above is syntactic sugar that desugars to bind invocations. You may not know what bind does, but youā€™ll encounter it soon on your FP journey if you didnā€™t do so already.

Also, note that the return value of addMaybe is still a Maybe: we canā€™t move Maybe into a lower context because it may not contain a value. Of course, we can still work around this by using unsafe functions, but they should be used sparingly.

If you want to move Maybe into a lower context safely, you can give a default value in case you get Nothing, but otherwise get the value inside Just. Use the fromMaybe function for that:

foo = fromMaybe 0 (addMaybe (Just 1) (Just 2)) -- 3

bar = fromMaybe 0 (addMaybe (Just 1) Nothing) -- 0

Note: the syntax highlighting seems to not be working properly in the second code block. Does anybody know why thatā€™s happening?

2 Likes

Hereā€™s a few options with your code

  let match = "clamp(20px, 30px, 40px)" # getValues
  -- pattern match it out and explicitly handle both cases
  case match of
    Just ns -> Console.logShow ns
    Nothing -> Console.log "No match"
    
  -- if you have 2 functions, one to handle each case, you can
  -- unwrap with the Maybe function
  match # maybe (Console.log "No Match") Console.logShow
  
  -- fromMaybe allows you to provide a default value
  -- 
  -- This isn't ideal in the scenario because there's no
  -- good default value for a NonEmptyArray by design
  match # fromMaybe (singleton Nothing) # Console.logShow

  -- This is generally discouraged because it throws an exception at runtime
  Console.logShow (unsafePartial (fromJust match))

I put them all in a gist so you can see it on the trypurescript site

https://try.purescript.org/?gist=7e526c38daac53016ef4f268c78e6a8a

3 Likes

Also, I think this will work if you replace unsafePartial # fromJust with unsafePartial fromJust. But again, I donā€™t recommend using fromJust here.

@sharkbrainguy This is really helpful, thank you. For the do in your code, is its semantic meaning identical to the one shown in this doc? I am confused by it.

Doc:
https://book.purescript.org/chapter4.html?highlight=do,notation#do-notation

You may find this helpful to understand what do notation is exactly: https://jordanmartinez.github.io/purescript-jordans-reference-site/content/11-Syntax/05-Prelude-Syntax/src/02-Do-Notation-ps.html.

However, you need to know what bind is to understand the stuff in the link thoroughly. Do you know what it is?

Itā€™s semantics are indeed the same but possibly in a very surprising way, ie more abstract, than you perhaps would think. I second @mhmdanas recommendation to persevere and really really get to a full understanding of bind, do, pure etc as it will unlock so much for you.

But if you find it too high a wall to climb right now, just think of the do in combination with the function signature as ā€œdetermining which type of ā€˜construct Xā€™ iā€™m working in in the following indented blockā€ and the <- as meaning something like ā€œtake something out of one of these Xā€™sā€ and pure as ā€œokay, wrap this one up to go nowā€

Itā€™s also important to note that the do block can ā€œshort circuitā€ to the defined empty value for these X thingsā€¦in the case of a Maybe that would be a Nothing

So overall, @mhmdanas add example is exactly equivalent to nested if-then-else or case statements.

2 Likes

It might be helpful to note that in many ways, a Maybe (NonEmptyArray (Maybe String)) is equivalent to an Array (Maybe String) (where the Nothing case of the former is equivalent to [] in the latter). You could always just convert to Array (Maybe String) if you wanted, e.g.,

f :: forall a. Maybe (NonEmptyArray a) -> Array a
f (Just xs) = toArray xs
f Nothing = []

and that might be easier to consume from your code in the way that you expect.

I suspect that the choice to use Maybe (NonEmptyArray (Maybe String)) in this case is because your code almost always would want to take a different branch altogether in the case that your Regex finds no matches. So typically, itā€™s expected that you would need to pattern match and provide a separate branch of code when it matches Nothing.

2 Likes

After reading your helpful answers, I am able to come up with this myself. How can I further improve this code? How would an expert in Purescript rewrite this?

getValues :: String -> Array (String)
getValues str = str # matchResult # unwrapArray <#> unwrapString
  where
    matchResult :: String -> Maybe (NonEmptyArray (Maybe String))
    matchResult str = match valueRegex str

    valueRegex :: Regex
    valueRegex = unsafeRegex "\\d[\\w%]*" global

    unwrapArray :: forall a. Maybe (NonEmptyArray a) -> Array a
    unwrapArray (Just n) = n # toArray
    unwrapArray Nothing = []

    unwrapString :: Maybe String -> String
    unwrapString (Just n) = n
    unwrapString Nothing = ""

"clamp(20px, 30px, 40px)" # getValues --["20px","30px","40px"]--
1 Like

I still found bind very difficult to understand, but now I know it takes the value out of Maybe, like taking the value out of a container. Thank you so much for your code example.

Yes it is painfully difficult for me to understand right now, but I will try it again today :grinning:.

So from your explanation and @mhmdanas, I guess <- is used for taking value from Maybe, and pure is for putting the value back to Maybe. Am I correct in this one?

I wouldnā€™t consider myself an expert, but I came up with this. I hope itā€™s clear!

getValues :: String -> Array String
getValues = fromMaybe [] <<< go
  where
    go :: String -> Maybe (Array String)
    go str = do
      regExp <- hush $ regex "\\d[\\w%]*" global
      result <- match regExp str
      nonEArr <- sequence result
      pure $ toArray nonEArr
  1. getValues is in ā€œpoint-freeā€ style, meaning the argument isnā€™t written explicitly. This passes the argument directly into go.

  2. <<< is function composition ā€“ it means after go runs, pass the result to the function on the left and run that.

  3. fromMaybe (from Data.Maybe) takes a default value and a maybe value and returns the maybe if itā€™s Just, or the default if itā€™s Nothing.

  4. The do block in go is in a Maybe context, so as long as the functions on the right of the arrows return Just values, then the variables on the left will have the values from inside the Justs. If anything returns Nothing, then the Nothing will be returned cascade through the rest of the block and eventually be returned (edit: @afc is right, it short-circuits instead of going through the rest of the block.)

  5. regex returns an Either String Regex, and hush (from Data.Either) converts an Either into a Maybe. If regex is successful then regExp is a Regex.

  6. match returns a Maybe (NonEmptyArray (Maybe String)), so result has the type NonEmptyArray (Maybe String).

  7. sequence (from Data.Traversable) can swap containers - for example a box of bags turns into a bag of boxes. So sequence result gives a Maybe (NonEmptyArray String) and nonEArr gets the NonEmptyArray String part.

  8. toArray converts it into a regular array, and pure puts it back into a Maybe.

  9. Now that go is complete, its result is passed into fromMaybe [].


do is syntax sugar on top of bind (>>=), so the function looks more like this to the compiler:

 hush (regex "\\d[\\w%]*" global) >>= \regExp ->
   match regExp str >>= \result ->
     sequence result >>= \nonEArr ->
       pure $ toArray nonEArr

And because of how bind is defined for Maybe, this is (essentially) what it ends up doing:

  case hush (regex "\\d[\\w%]*" global) of
    Nothing -> Nothing
    Just regExp -> 
      case match regExp str of
        Nothing -> Nothing
        Just result ->
          case sequence result of
            Nothing -> Nothing
            Just nonEArr ->
              Just (toArray nonEArr)
1 Like

Correct as long as you have understood two crucial details:

  • each (<-) only yields the contents of the Just value (any Nothing short-circuits at any point to a final Nothing)
  • the whole do, <-, pure syntax is polymorphic, this is what it does for a Maybe. I think youā€™ve understood this but it bears repeating
1 Like

Iā€™m not sure you should be forcing yourself to understand bind and pure like this. Personally for me, when I started learning FP, I read a tutorial or two about them but understood nothing and gave up on tutorials.

Instead, I was able to build an intuition around them by using them a lot with Effect, Maybe, Array, etc. Thatā€™s how I was able to grasp many operations, not just bind and pure.

Iā€™m not sure thisā€™ll work with you, but you could try using these operations on multiple data types and build an intuition for the shared semantics between them just as I did. You donā€™t need to rush yourself.

I think I have a hard time explaining this to beginners because I understand it intuitively, not by someone comparing monads to burritos.

1 Like

Also not an expert, but in my journey with PureScript, one of the most valuable things I get out of it is that it helps me to think about all the edge cases. I have a hard time giving any advice about your implementation, because I donā€™t have enough context to know what your application should do for edge cases like not finding any matches (or an ā€œoptional unmatched capturing groupā€, though in your case, valueRegex is hard-coded and doesnā€™t have any optional capturing groups, so thatā€™s not really a concern). If your application can always treat no matches the same as at treats matches, Iā€™d say your implementation looks good (though there are shortcuts you can use to shorten your definition, like the ones that @smilack helpfully gave examples of). But, if you application (at least sometimes) needs to take a different approach when there are no matches, then Iā€™d suggest leaving the output as Maybe (Array String) or Maybe (NonEmptyArray String), so that the caller of this function remembers that they have to think about that edge case. Iā€™d probably write those like

getValues1 :: String -> Maybe (Array String)
getValues1 str = match valueRegex str <#> NonEmptyArray.toArray <#> Array.catMaybes
  where
    valueRegex = unsafeRegex """\d[\w%]*"""

getValues2 :: String -> Maybe (NonEmptyArray String)
getValues2 str = match valueRegex str <#> NonEmptyArray.catMaybes
  where
    valueRegex = unsafeRegex """\d[\w%]*"""

(catMaybes converts Array (Maybe a) -> Array a by stripping out any Nothing values. <#> you used in your implementation to map an Array. You can also use it to map Maybe!)

I have seen the word polymorphic so many times but still I am not sure what does it mean, so I want to take this opportunity to ask for it as well. In this context, does it mean it will accept any data regardless of their type?

I found a very good article on polymorphism, but I am not sure if I understand the meaning of polymorphic here.

1 Like