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:
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
@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.
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, @mhmdanasadd example is exactly equivalent to nested if-then-else or case statements.
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.
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?
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 .
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
getValues is in āpoint-freeā style, meaning the argument isnāt written explicitly. This passes the argument directly into go.
<<< is function composition ā it means after go runs, pass the result to the function on the left and run that.
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.
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 returnedcascade 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.)
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.
match returns a Maybe (NonEmptyArray (Maybe String)), so result has the type NonEmptyArray (Maybe String).
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.
toArray converts it into a regular array, and pure puts it back into a Maybe.
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
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)
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.
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
(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.