arliss_obsidian/fp/Language/Functions.md

205 lines
5.5 KiB
Markdown
Raw Normal View History

2024-09-22 19:24:51 +00:00
## Applying
Functions are applied by placing expressions after the function name, separated by whitespace, e.g.
```javascript
Math.pow(2, 10)
```
equivalent would be
```haskell
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`.
```haskell
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`.
```haskell
add :: Int -> Int -> Int
add a b = -- ...
```
### Body
The expression returned by the function, e.g.
```haskell
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
```haskell
div num 0.0 = infinity
div num den = num / den
```
this is equivalent to
```haskell
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`:
```haskell
ensureLeadingSlash :: String -> String
ensureLeadingSlash str =
if String.Util.startsWith "/" then
str
else
"/" <> str
```
Alternatively, we could implement this with guard patterns:
```haskell
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.
```haskell
add :: Int -> Int -> Int
-- ...
add2 :: Int -> Int
add2 n = add 2 n
```
is equivalent to
```haskell
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.
> ```haskell
> f a = g b c a
>
> \a -> g b c a
>
> f a = g $ h a
> ```
> can be written as
> ```haskell
> 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.
```haskell
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:
```haskell
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))