From 3ee2a9bbbdba1c6b03bc9b4f1992b5627cfa5ea3 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 24 Sep 2024 17:52:08 -0500 Subject: [PATCH] update --- fp/.obsidian/workspace.json | 50 +++-- fp/Language/Expressions/Lambda Functions.md | 23 ++ fp/Language/Expressions/case .. of.md | 21 +- fp/Language/Expressions/do notation.md | 209 ++++++++++++++++++ .../Expressions/if .. then .. else ...md | 5 + fp/Language/Expressions/let .. in ...md | 33 +++ fp/Language/Functions.md | 4 +- fp/Language/Functions/Currying.md | 4 +- .../Functions/Defining/Guard Clause.md | 8 +- .../Functions/Defining/Pattern Matching.md | 14 +- fp/Terminology/Point-free.md | 18 ++ 11 files changed, 356 insertions(+), 33 deletions(-) create mode 100644 fp/Language/Expressions/Lambda Functions.md create mode 100644 fp/Terminology/Point-free.md diff --git a/fp/.obsidian/workspace.json b/fp/.obsidian/workspace.json index bbae538..ec74fd0 100644 --- a/fp/.obsidian/workspace.json +++ b/fp/.obsidian/workspace.json @@ -11,8 +11,12 @@ "id": "6465d16034124b8f", "type": "leaf", "state": { - "type": "graph", - "state": {} + "type": "markdown", + "state": { + "file": "Language/Functions/Defining/Guard Clause.md", + "mode": "source", + "source": true + } } } ] @@ -81,6 +85,7 @@ "state": { "type": "backlink", "state": { + "file": "Language/Functions/Defining/Guard Clause.md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -97,6 +102,7 @@ "state": { "type": "outgoing-link", "state": { + "file": "Language/Functions/Defining/Guard Clause.md", "linksCollapsed": false, "unlinkedCollapsed": true } @@ -118,7 +124,9 @@ "type": "leaf", "state": { "type": "outline", - "state": {} + "state": { + "file": "Language/Functions/Defining/Guard Clause.md" + } } } ] @@ -142,6 +150,24 @@ }, "active": "6465d16034124b8f", "lastOpenFiles": [ + "Language/Functions/Defining/Pattern Matching.md", + "Language/Functions/Applying.md", + "Language/Functions/Composition.md", + "Language/Functions/Currying.md", + "Language/Functions.md", + "Terminology/Point-free.md", + "Language/Infix Operators/Common Operators/Applying & Composing Functions.md", + "Language/Functions/Defining.md", + "Language/Expressions/case .. of.md", + "Language/Expressions/Lambda Functions.md", + "Language/Functions/Defining/Guard Clause.md", + "Language/Expressions/let .. in ...md", + "Language/Expressions/if .. then .. else ...md", + "Language/Expressions/do notation.md", + "Classes/Functor/Apply.md", + "Classes/Functor/Applicative.md", + "Classes/Bind/Monad.md", + "Classes/Bind/Bind.md", "Classes/Alternative/Alt.md", "Classes/Alternative/Plus.md", "Monads/Transformers/MaybeT.md", @@ -151,31 +177,13 @@ "Monads/Transformers/StateT.md", "Monads/Transformers", "Classes/Alternative/Alternative.md", - "Classes/Functor/Applicative.md", - "Classes/Functor/Apply.md", - "Classes/Functor/Functor.md", "Classes/Collapsing", "Classes/Bind", "Classes/Functor", "Classes/Alternative", - "Language/Infix Operators/Common Operators/Data.md", - "Classes/Traversable.md", - "Data/Collections/NonEmptyArray.md", - "Data/Collections/NonEmptyList.md", - "Data/String.md", - "Data/Collections.md", - "Data/Collections/HashMap.md", "Data/Product", "Data/Sum", - "Data/Collections/HashSet.md", - "Data/Collections/Set.md", - "Data/Collections/List.md", - "Data/Collections/Map.md", "Data/Collections", - "Language/Row Types.md", - "Language/Row Types/Variant.md", - "Language/Row Types/Record.md", - "Data/Product/Tuple.md", "Language/Row Types", "Classes/Math", "Untitled 1.canvas", diff --git a/fp/Language/Expressions/Lambda Functions.md b/fp/Language/Expressions/Lambda Functions.md new file mode 100644 index 0000000..d53314a --- /dev/null +++ b/fp/Language/Expressions/Lambda Functions.md @@ -0,0 +1,23 @@ +lambda functions are anonymous functions (as opposed to [[Functions|top-level functions]]) that can be written anywhere: + +```haskell +\ -> +``` + +```haskell +prependHello a = "hello, " <> a + +appendName = \a -> a <> "henry!" + +str = appendName $ prependHello "" +-- hello, henry! +``` + +their parameters can destructure their arguments: +```haskell +let + getFoo = \{foo} -> foo +in + getFoo {foo: "hello"} +-- "hello" +``` \ No newline at end of file diff --git a/fp/Language/Expressions/case .. of.md b/fp/Language/Expressions/case .. of.md index 3f33ea8..d1654ce 100644 --- a/fp/Language/Expressions/case .. of.md +++ b/fp/Language/Expressions/case .. of.md @@ -1,4 +1,4 @@ -Pattern matching +Structural pattern matching expression ```haskell case of @@ -18,7 +18,7 @@ where `arm` is: - array literals, containing patterns - wildcard `_` -### Guard Clauses +## Guard Clauses Case arms can have [[Functions/Defining/Guard Clause|guard clauses]] like [[Functions|function]] definitions: ```haskell @@ -28,7 +28,22 @@ case _ of | otherwise -> Nothing ``` -### Examples +## Anonymous expression +Underscore can be placed in the expression position to write an [[Lambda Functions|anonymous function]] that immediately case matches on its argument: + +```haskell +case _ of + Foo -> true + Bar -> false + +-- equivalent to + +\a -> case a of + Foo -> true + Bar -> false +``` + +## Examples Match on `Int` when zero ```haskell diff --git a/fp/Language/Expressions/do notation.md b/fp/Language/Expressions/do notation.md index e69de29..6d442a8 100644 --- a/fp/Language/Expressions/do notation.md +++ b/fp/Language/Expressions/do notation.md @@ -0,0 +1,209 @@ +Do notation is sugar for [[Bind|bind]] `>>=`, allowing imperative blocks that use bind as the driver of the "and-then" behavior for the specific type being returned. + +```haskell +main :: Effect Unit +main = do + log "hello, world!" + log "foo bar baz" + +-- logs: +-- hello, world! +-- foo bar baz +``` + +Do blocks are opened with the `do` keyword and support only a few operations: + +- [[#bind|"await" with <-]] +- [[#let-bindings|define local variables]] +- [[#eval and ignore|evaluate an expression but discard its output]] +- [[#return|return an expression]] + +statements in do blocks are evaluated line-by-line top-to-bottom, just like any other imperative language. + +## bind +The left-arrow `<-` in `do` blocks is analogous to the `await` keyword in JS, C# & Java, and the question mark operator in Rust. + +```haskell +intPositive :: Int -> Maybe Int +intPositive n + | n > 0 = Just n + | otherwise = Nothing + +f :: String -> Maybe Int +f s = do + -- if `s` can't be parsed, returns Nothing early + num <- Int.fromString s + -- if `num` is <= 0, returns Nothing early + pos <- intPositive num + Just pos +``` + +This desugars to: +```haskell +f s = + Int.fromString s + >>= ( \num -> + intPositive num + >>= (\pos -> Just pos) + ) +``` + +## let-bindings +Related to [[let .. in ..]], You can define new local variables in the middle of do blocks that can depend on previous bindings: + +```haskell +do + a <- Just 1 + let + b = a + 1 + c = b + 1 + Just c +-- evaluates to (Just 3) +``` + +> [!attention]- Unexpected or mismatched indentation +> A common gotcha with let-bindings in do blocks is only indenting once after trying to insert a line break after the equals: +> ```haskell +> do +> a <- Just 1 +> let b = +> a + 1 +> Just b +> ``` +> +> ```text +> [ERROR 1/1 ErrorParsingModule] ... +> +> 4 a + 1 +> +> +> Unable to parse module: +> Unexpected or mismatched indentation +> ``` +> +> This happens because oneline let bindings +> ```haskell +> do +> let a = b +> .. +> ``` +> are identical to +> ```haskell +> do +> let +> a = b +> .. +> ``` +> So when you break after the equals and only indent once: +> ```haskell +> do +> let a = +> b +> .. +> ``` +> what you're actually writing is +> ```haskell +> do +> let +> a = +> b +> .. +> ``` + +## eval and ignore +When an expression returns `m Unit`, you don't have to write an arrow: +```haskell +guard :: Boolean -> Maybe Unit +guard true = Just unit +guard false = Nothing + +do + n <- Int.fromString "123" + -- when `n > 0`, this resolves to `Just unit` and + -- the execution continues. + -- If this is Nothing, the block short-circuits + -- and returns Nothing. + guard (n > 0) + Just n +``` + +If you want to discard a value other than unit, you need to use the discard syntax `_ <- x` + +```haskell +main :: Effect Unit +main = do + fooTxt <- File.createWriteStream "foo.txt" + -- Stream.writeString returns a Boolean + -- indicating whether the stream needs us + -- to wait for the `drain` event before writing + -- more, but we don't care here. + _ <- Stream.writeString UTF8 "foo" fooTxt + pure unit +``` + +## return +the last statement in a do block is the expression the do block will return, and can't be a let-binding or bind statement: + +```haskell +-- block immediately resolves with `Just ""` +do + Just "" +``` + +```haskell +-- block resolves with `Just foo` +do + foo <- getFoo + Just foo +``` + +```haskell +-- block resolves with `Just "foo"` +do + do + do + do + Just "foo" +``` + +## Examples +`do` in [[Maybe]] allows us to return early with empty values. + +In this example, `getApiUrlPort` will return Nothing if: +- environment variable `API_URL` is unset +- `API_URL` is not a valid URL +- `API_URL` is a valid URL, but the port isn't set + +```haskell +lookupEnv :: String -> Maybe String +urlFromString :: String -> Maybe URL +urlPort :: URL -> Maybe Int + +getApiUrlPort = do + apiUrlString <- lookupEnv "API_URL" + apiUrl <- urlFromString apiUrlString + urlPort apiUrl +``` + +`do` in [[Either]] allows us to return early with errors. + +```haskell +lookupEnv :: String -> Either String String +urlFromString :: String -> Either String URL +urlPort :: URL -> Maybe Int + +getApiUrlPort = do + apiUrlString <- lookupEnv "API_URL" + apiUrl <- urlFromString apiUrlString + liftMaybe ("no port set in URL " <> apiUrlString) + $ urlPort apiUrl +``` + +> [!tip]- `liftMaybe` +> liftMaybe allows us to turn `Maybe a` into any `m a` as long as `m` supports [[MonadThrow|throwing]] errors. +> +> In the above example, if `urlPort` is `Nothing`, it will return `Left "no port set in ..."`. +> +> ```haskell +> liftMaybe :: forall m e a. MonadThrow e m => e -> Maybe a => m a +> ``` \ No newline at end of file diff --git a/fp/Language/Expressions/if .. then .. else ...md b/fp/Language/Expressions/if .. then .. else ...md index e69de29..3a3e0c9 100644 --- a/fp/Language/Expressions/if .. then .. else ...md +++ b/fp/Language/Expressions/if .. then .. else ...md @@ -0,0 +1,5 @@ +Conditional expression, similar to ternary expressions in imperative languages: + +```haskell +if a == b then "foo" else "bar" +``` \ No newline at end of file diff --git a/fp/Language/Expressions/let .. in ...md b/fp/Language/Expressions/let .. in ...md index e69de29..cbc55f9 100644 --- a/fp/Language/Expressions/let .. in ...md +++ b/fp/Language/Expressions/let .. in ...md @@ -0,0 +1,33 @@ +`let .. in ..` is an expression with some scoped variables, separated by newlines: + +```haskell +let + a = 1 + b = 2 +in + a + b +``` + +> [!tip] +> there is a second local let-binding syntax, `where`: +> ```haskell +> a + b +> where +> a = 1 +> b = 2 +> ``` + +### Examples + +Recursive Fibonacci generator, using a helper function `go` +```haskell +fib n = + let + go ns a b = + if Array.length ns == n then + ns + else + go (ns <> [a]) b (a + b) + in + go [] 1 1 +``` diff --git a/fp/Language/Functions.md b/fp/Language/Functions.md index e58ccda..cfc095c 100644 --- a/fp/Language/Functions.md +++ b/fp/Language/Functions.md @@ -1 +1,3 @@ -The backbone of functional programming. Functions can be constants, transform [[Data Structures|data]], perform [[Effect|effects]] and much more. \ No newline at end of file +The backbone of functional programming. Functions can be constants, transform [[Data Structures|data]], perform [[Effect|effects]] and much more. + +Functions can be [[Defining|defined]] as top-level [[Modules|module]] items, [[Lambda Functions|lambda functions]], [[Infix Operators|infix operators]] or created by [[Currying|partially applying]] functions. \ No newline at end of file diff --git a/fp/Language/Functions/Currying.md b/fp/Language/Functions/Currying.md index febd86c..5ccdd84 100644 --- a/fp/Language/Functions/Currying.md +++ b/fp/Language/Functions/Currying.md @@ -1,6 +1,6 @@ [[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." +This allows you to call many functions [[Point-free|point-free]], and think in terms of "building up to a conclusion" rather than "i need everything at once." e.g. @@ -27,7 +27,7 @@ Walking through this: 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] +> [!tip] > 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 diff --git a/fp/Language/Functions/Defining/Guard Clause.md b/fp/Language/Functions/Defining/Guard Clause.md index c7f88d6..bc6f9ef 100644 --- a/fp/Language/Functions/Defining/Guard Clause.md +++ b/fp/Language/Functions/Defining/Guard Clause.md @@ -1,14 +1,14 @@ Single [[Functions|Functions]] [[Defining/Pattern Matching|implementations]] may have 1 or more guard clauses. -> [!info] +> [!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 [[Expressions/if .. then .. else ..]] expressions. +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 `| = `. 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`. +[[String]]s 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`: @@ -31,7 +31,7 @@ ensureLeadingSlash str When the first pattern `String.Util.startsWith "/" str` returns `true`, the function will use `... = str`. Otherwise, it will prepend `/` to `str`. -> [!tip] +> [!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: diff --git a/fp/Language/Functions/Defining/Pattern Matching.md b/fp/Language/Functions/Defining/Pattern Matching.md index b7951a3..64025e1 100644 --- a/fp/Language/Functions/Defining/Pattern Matching.md +++ b/fp/Language/Functions/Defining/Pattern Matching.md @@ -1,7 +1,17 @@ [[Functions]] may have multiple implementations that change behavior based on the shape of the arguments. -> [!note] -> You can always replace this with a [[case .. of|case expression]]. +> [!tip]- [[case .. of]] +> Pattern matching in function definitions can always be rewritten as [[case .. of]]. +> ```haskell +> isA "a" = true +> isA _ = false +> ``` +> can be rewritten as +> ```haskell +> isA a = case a of +> "a" -> true +> _ -> false +> ``` 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. diff --git a/fp/Terminology/Point-free.md b/fp/Terminology/Point-free.md new file mode 100644 index 0000000..360a68a --- /dev/null +++ b/fp/Terminology/Point-free.md @@ -0,0 +1,18 @@ +Written without explicit arguments and application, e.g. +```haskell +foo a b = bar a b +``` +can be written point-free as +```haskell +foo = bar +``` + +Rewriting +```haskell +\a -> g (f a) +``` +as +```haskell +g << f +``` +is also referred to as point-free.