arliss_obsidian/fp/Language/Functions/Defining/Guard Clause.md
Orion Kindel 3ee2a9bbbd
update
2024-09-24 17:52:08 -05:00

2.0 KiB

Single Functions Defining/Pattern Matching may have 1 or more guard clauses.

Tip

case .. of expressions can also use guard clauses.

These allow functions to switch on boolean expressions based on the inputs, and can often take up less space than explicit if .. then .. else .. expressions.

Guard patterns are placed after the function arguments and take the form | <Boolean expr> = <body>. There can be any number of guard patterns, as long as all cases are exhaustively covered.

Example

Strings can't be structurally pattern matched aside from the entire string. In order to ask if a string starts with a substring we need to call a function like Data.String.Utils.startsWith.

We could do this with if then else:

ensureLeadingSlash :: String -> String
ensureLeadingSlash str =
  if String.Util.startsWith "/" then
    str
  else
    "/" <> str

Alternatively, we could implement this with guard patterns:

ensureLeadingSlash :: String -> String
ensureLeadingSlash str
  | String.Util.startsWith "/" str = str
  | otherwise = "/" <> str

When the first pattern String.Util.startsWith "/" str returns true, the function will use ... = str. Otherwise, it will prepend / to str.

[!tip] otherwise otherwise is simply an alias for true, specifically for better-reading fallthrough guard patterns.

Much less often, but occasionally useful is the ability to structurally pattern match in a guard clause:

ensureLeadingSlashM :: Maybe String -> String
ensureLeadingSlashM mstr
  | Just s <- mstr, String.Util.startsWith "/" s = s
  | otherwise = ""

Here the bit Just s <- mstr is saying "when mstr is Just s..."

then String.Util.startsWith "/" s says "and s starts with "/"..."

| Just s <- mstr, startsWith "/" s = s "when mstr is Just s and s starts with "/", return s"

| otherwise = "" otherwise return ""

You can have any number of comma-separated guard bindings before the last boolean expression.