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 fortrue
, 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))