arliss_obsidian/fp/Language/Functions.md
2024-09-22 14:24:51 -05:00

5.5 KiB

Applying

Functions are applied by placing expressions after the function name, separated by whitespace, e.g.

Math.pow(2, 10)

equivalent would be

Number.pow 2.0 10.0

Defining

Parts of a function definition are:

  • type signature (optional)
  • 1 or more "implementations" of the type signature
    • name
    • 0 or more arguments
    • function body

Type Signature

Function type signatures follow the form <name> :: <type>, and are always the line immediately above the function implementations, e.g.

this add has 2 Int arguments and returns an Int.

add2 :: Int -> Int -> Int
-- ...

Name

function names can contain any alphanumeric character, _, ' but the first character must be a lowercase alpha or _.

Arguments

Arguments are space-separated, e.g.

the first Int argument is bound to a, the second to b.

add :: Int -> Int -> Int
add a b = -- ...

Body

The expression returned by the function, e.g.

add a b = a + b

Multiple Implementations

Functions may have multiple implementations that change behavior based on the shape of the arguments.

This is similar to method overloading in OOP languages, but differs in that the number of arguments and type of the arguments must be the same for all implementations.

Functions can have any number of implementations, as long as all possible inputs are covered exhaustively.

e.g.

this implementation of Number division has 2 paths:

  • when the denominator is zero, yields Infinity
  • otherwise, performs the division
div num 0.0 = infinity
div num den = num / den

this is equivalent to

div num den =
  if den == 0.0 then
    infinity 
  else
    num / den

Guards

Single function implementations may have guards, which allows for conditional checks on inputs (as opposed to structural pattern matching)

This pattern often takes 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.

e.g.

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.

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

Currying

Functions are curried in PureScript, meaning that "a function of 2 arguments" is actually "a function of 1 argument, returning a function of 1 argument."

This allows you to call many functions point-free, and think in terms of "building up to a conclusion" rather than "i need everything at once."

e.g.

add :: Int -> Int -> Int
-- ... 

add2 :: Int -> Int
add2 n = add 2 n

is equivalent to

add :: Int -> Int -> Int
-- ... 

add2 :: Int -> Int
add2 = add 2

Walking through this:

add has type Int -> Int -> Int

if we give add a single Int argument, it will return a function of type Int -> Int. This function is the "second half" of add, waiting for it's second argument. Since Int -> Int is the type of add2, we can simply say add2 = add 2.

[!info] as a rule, any time a function's last argument is passed as the last argument to another function, you can remove both.

f a = g b c a

\a -> g b c a

f a = g $ h a

can be written as

f = g b c

g b c

f = g <<< h

Working with Functions

Functions being first-class citizens means that it's important that applying, extending and combining functions must be easy.

Composition

The most common pattern by far; piping the output of a function into the input of the next.

compose :: forall a b c. (b -> c) -> (a -> b) -> (a -> c)
compose g f a = g (f a)

compose (infix as <<<) accepts 2 functions; f which is a -> b and g; b -> c. compose returns a new function a -> c that "glues" the 2 functions together; piping the output of f into g.

e.g.

consider a function normalizedPathSegments :: String -> Array String

This function would normalize a file path, removing trailing / leading slashes and resolving relative paths, then split the path by its segments.

A very good approach would be to split this function into separate single-purpose components, e.g.

  • stripLeadingSlash :: String -> String
  • stripTrailingSlash :: String -> String
  • splitPath :: String -> Array String
  • normalizePathSegments :: Array String -> Array String

then define normalizedPathSegments like so:

normalizedPathSegments :: String -> Array String
normalizedPathSegments =
  normalizePathSegments
  <<< splitPath
  <<< stripTrailingSlash
  <<< stripLeadingSlash

map map map (a -> b) -> (f a) -> (f b) (a -> b) -> (c -> a) -> (c -> b)

((a -> b) -> (c -> a) -> (c -> b)) -> ((c -> b) -> (a -> b)) -> ((c -> b) -> (c -> a))