arliss_obsidian/fp/Language/Functions/Defining/Guard Clause.md
Orion Kindel 3701b1e2f8
update
2024-09-23 18:56:55 -05:00

2.0 KiB

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

[!info] 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 Expressions/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, so 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 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.