diff --git a/01-Getting-Started/06-Other-Important-Info.md b/01-Getting-Started/06-Other-Important-Info.md index 7c7fb0580..513f3a592 100644 --- a/01-Getting-Started/06-Other-Important-Info.md +++ b/01-Getting-Started/06-Other-Important-Info.md @@ -1,5 +1,6 @@ # Other Important Info +- [Functional Programming Made Easier](https://leanpub.com/fp-made-easier) is a more recent work that literally walks you through every possible thought process, mistake, compiler error, and issue you would need to make to learn PureScript and build a web application in one book. I would recommend reading this book over the **PureScript by Example** book below. - [Purescript By Example](https://book.purescript.org/) is the official up-to-date book that teaches Purescript. - [PureScript Cookbook](https://github.com/JordanMartinez/purescript-cookbook) is an unofficial cookbook that shows "How to do X" in PureScript. - ["Not Yet Awesome" PureScript](https://github.com/milesfrain/not-yet-awesome-purescript) is a list of things that are not _yet_ awesome in PureScript diff --git a/02-FP-Philosophical-Foundations/05-Looping-via-Recursion.md b/02-FP-Philosophical-Foundations/05-Looping-via-Recursion.md index 61daa1b56..e34c03070 100644 --- a/02-FP-Philosophical-Foundations/05-Looping-via-Recursion.md +++ b/02-FP-Philosophical-Foundations/05-Looping-via-Recursion.md @@ -60,6 +60,8 @@ factorial' 1 24 24 ``` +In some cases, one will need to write more complex code to get the desired performance using a combination of defunctionalization and continuation-passing style (CPS). This is covered in more detail in the `Design Patterns/Defunctionalization.md` file. + ## For ... Break If ```javascript diff --git a/11-Syntax/01-Basic-Syntax/spago.dhall b/11-Syntax/01-Basic-Syntax/spago.dhall index 436bc8ec7..9b77d7c50 100644 --- a/11-Syntax/01-Basic-Syntax/spago.dhall +++ b/11-Syntax/01-Basic-Syntax/spago.dhall @@ -5,13 +5,12 @@ You can edit this file as you like. { sources = [ "src/**/*.purs" ] , name = "untitled" , dependencies = - [ "console" - , "effect" - , "newtype" + [ "newtype" , "partial" , "prelude" , "psci-support" , "unsafe-coerce" + , "safe-coerce" ] , packages = ../../packages.dhall } diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/01-Single-Paramter.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/01-Single-Paramter.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/01-Single-Paramter.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/01-Single-Paramter.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/02-Constraining-Types-Using-Typeclasses.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/02-Constraining-Types-Using-Typeclasses.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/02-Constraining-Types-Using-Typeclasses.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/02-Constraining-Types-Using-Typeclasses.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/03-Dictionaries--How-Type-Classes-Work.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Dictionaries--How-Type-Classes-Work.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/03-Dictionaries--How-Type-Classes-Work.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Dictionaries--How-Type-Classes-Work.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/01-Partial.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/01-Partial.purs new file mode 100644 index 000000000..9cb36363c --- /dev/null +++ b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/01-Partial.purs @@ -0,0 +1,45 @@ +module Syntax.Basic.Typeclass.Special.Partial where + +-- This function is imported from the `purescript-partial` library. +import Partial.Unsafe (unsafePartial) + +-- Normally, the compiler will require a function to always exhaustively +-- pattern match on a given type. In other words, the function is "total." + +data TwoValues = Value1 | Value2 + +renderTwoValues :: TwoValues -> String +renderTwoValues = case _ of + Value1 -> "Value1" + Value2 -> "Value2" + +-- In the above example, removing the line with `Value2 -> "Value2"` +-- from the source code would result in a compiler error as the function +-- would no longer be "total" but "partial." +-- However, there may be times when we wish to remove that compiler restriction. +-- This can occur when we know that a non-exhaustive pattern match will +-- not fail or when we wish to write more performant code that only works +-- when the function has a valid argument. + +-- In such situations, we can add the `Partial` type class constraint +-- to indicate that a function is no longer a "total" function but is now +-- a "partial" function. In othe rwords, the pattern match is no longer +-- exhaustive. If someone calls the function with an invalid invalid argument, +-- it will produce a runtime error. + +renderFirstValue :: Partial => TwoValues -> String +renderFirstValue Value1 = "Value1" + -- There is no `Value2` line here! + +-- When we wish to call partial functions, we must remove that `Partial` +-- type class constraint by using the function `unsafePartial`. + +-- unsafePartial :: forall a. (Partial a => a) -> a + +callWithNoErrors_renderFirstValue :: String +callWithNoErrors_renderFirstValue = unsafePartial (renderFirstValue Value1) + +-- Uncomment this code and run it in the REPL. It will produce a runtime error. +callWithRuntimeErrors_renderFirstValue :: String +callWithRuntimeErrors_renderFirstValue = + unsafePartial (renderFirstValue Value2) diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/02-Coercible.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/02-Coercible.purs new file mode 100644 index 000000000..5be188f3b --- /dev/null +++ b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/03-Special-Type-Classes/02-Coercible.purs @@ -0,0 +1,218 @@ +module Syntax.Basic.Typeclass.Special.Coercible where + +import Prelude +import Prim.Coerce (class Coercible) +import Safe.Coerce (coerce) + +-- ## Linking to the paper for an (optional) detailed explanation + +-- In this file, we'll provide a beginner-friendly summary of the paper +-- that is linked below. For our purposes, we will only explain the bare +-- minimum necessary to make the rest of this file make sense. + +-- If you wish to know more, read the paper below. However, be warned that +-- those who are new to functional programming will likely not understand +-- as much until they understand the `Functor` and/or `Foldable` type classes. +-- These are covered in the `Hello World/Prelude-ish` folder in this project. + +-- Here's the paper: "Safe zero-cost coercions for Haskell" +-- https://repository.brynmawr.edu/cgi/viewcontent.cgi?referer=&httpsredir=1&article=1010&context=compsci_pubs + +--------------------------------------------------------------------------- + +-- ## Summary of the Problem + +-- While we have stated earlier that newtypes are "zero-cost abstractions" +-- in that one does not incur a performance penalty for wrapping and unwrapping +-- a newtyped value, there are some situations where this is not true. + +-- For example, let's say you had the following types: + +-- | A comment that has multiple lines of text. +newtype MultiLineComment = MultiLineComment String + +-- | A comment that has only 1 line of text. +newtype SingleLineComment = SingleLineComment String + +-- Let's say we wish to convert an `Array MultiLineComment` into +-- `Array SingleLineComment` via the function, +-- `exposeLines :: String -> Array String` + +-- While newtypes are "zero-cost abstractions," this particular algorithm +-- would incur a heavy performance cost. Here's what we would have to do: +-- 1. Convert the `MultiLineComment` type into the `String` type +-- by iterating through the entire `Array MultiLineComment` and unwrapping +-- the `MultiLineComment` newtype wrapper. +-- 2. Use `exposeLines` to convert each multi-line `String` into an `Array` +-- of Strings by iterating through the resulting array. +-- Each `String` in the resulting array would have only 1 line of content. +-- 3. Combine all `Arrays` of single-line `String`s into one Array. +-- In other words, `combine :: Array (Array String) -> Array String` +-- 4. Convert the `String` type into the `SingleLineComment` type +-- by iterating through the final `Array` and wrapping each `String` in a +-- `SingleLineComment` newtype. + +-- Steps 1 and 4 are necessary to satisfy type safety. At the type-level, +-- a `String` is not a `MultiLineComment`, nor a `SingleLineComment`. +-- However, those three types do have the same runtime representation. Thus, +-- Steps 1 and 4 are an unnecessary performance cost. Due to using newtypes +-- in this situation, we iterate through the array two times more than needed. + +-- A `MultiLineComment` can be converted into a `String` safely and +-- a `String` into a `SingleLineComment` safely. This type conversion +-- process is safe and therefore unnecessary. The problem is that the developer +-- does not have a way to provide the compiler with a proof of this safety. +-- If the compiler had this proof, it could verify it and no longer complain +-- when the developer converts the `Array MultiLineComment` into an +-- `Array String` through a O(1) functio. + +-- The solution lays in two parts: the `Coercible` type class +-- and "role annotations." + +-- ## Coercible + +-- This is the exact definition of the `Coercible` type class. However, +-- we add the "_" suffix to distinguish this fake one from the real one. +class Coercible_ a b where + coerce_ :: a -> b + +-- The `Coercible` type class says, "I can safely convert a value of type `a` +-- into a value of type `b`." This solves our immediate problem, but it +-- introduces a new problem. Since the main usage of `Coercible` is to +-- remove the performance cost of newtypes in specific situations, how do +-- make it impossible to write `Coercible` instances for invalid types? + +-- For example, a `DataBox` is a literal box at runtime because it uses the +-- `data` keyword. It actually has to wrap and unwrap the underying value: +data DataBox a = DataBox a + +-- The `NewtypedBox` below is NOT a literal box at runtime because +-- it doesn't actually wrap/unwrap the underlying value. +newtype NewtypedBox theValue = NewtypedBox theValue + +-- Thus, while we could have a type class instance for `MultiLineComment`, +-- `String`, and `SingleLineComment`, should we have an instance +-- between `DataBox` and `NewtypedBox`? The answer is no. +-- +-- However, how would we tell that to the compiler, so it could verify that +-- for us? The answer is "role annotations." + +-- ## Role Annotations + +-- For another short explanation, see the answer to the post, +-- "What is a role?" https://discourse.purescript.org/t/what-is-a-role/2109/2 + +-- Role annotations tell the compiler what rules to follow when determining +-- whether a Coercible instance between two types is valid. There are +-- three possible values: representational, phantom, and nominal. + +-- Role annotation syntax follows this syntax pattern: +-- `type role TheAnnotatedType oneRoleAnnotationForEachTypeParameter` + +-- ### Representational + +-- Representational says, +-- "If `A` can be safely coerced to `B` and the runtime representation of +-- `Box a` does NOT depend on `a`, then `Box a` can be safely +-- coerced to `Box b`." (in contrast to `nominal`) + +-- Given a type like Box, which only has one type parameter, `a`... +data Box a = Box a + +-- ... we would write the following: +type role Box representational + +-- Here's another example that shows what to do when we have +-- multiple type parameters +data BoxOfThreeValues a b c = BoxOfThreeValues a b c +type role BoxOfThreeValues representational representational representational + +-- ### Phantom + +-- Phantom says, +-- "Two phantom types never have a runtime representation. Therefore, +-- two phantom types can always be coerced to one another." + +-- Given a box-like type that has a phantom type parameter, `phantomType`... +data PhantomBox :: Type -> Type +data PhantomBox phantomType = PhantomBox + +-- ... we would write the following: +type role PhantomBox phantom + +-- Here's another example that mixes role annotations: +data BoxOfTwoWithPhantom :: Type -> Type -> Type -> Type +data BoxOfTwoWithPhantom a phantom b = BoxOfTwoWithPhantom + +type role BoxOfTwoWithPhantom representational phantom representational + +-- ### Nominal + +-- Nominal says, +-- "If `A` can be safely coerced to `B` and the runtime representation of +-- `Box a` DOES depend on `a`, then `Box a` can NOT be safely +-- coerced to `Box b`." (in contrast to `representational`) + +-- When we don't have enough information (e.g. writing FFI), we default +-- to the nominal role annotation. Below, we'll see why. + +-- For example, let's consider `HashMap key value`. Let's say we use a type class +-- called `Hashable` to calculate the hash of a given key. Since newtypes +-- can implement a different type class instance for the same runtime +-- representation, wrapping that value in a newtype and then hashing it +-- might not produce the same hash as the original. Thus, we would return +-- a different value. + +class Hashable key where + hash :: key -> Int + +instance hashableInt :: Hashable Int where + hash key = key + +newtype SpecialInt = SpecialInt Int +derive instance eqSpecialInt :: Eq SpecialInt +instance hashableSpecialInt :: Hashable SpecialInt where + hash (SpecialInt key) = key * 31 + +data Map key value = Map key value + +type role Map representational representational + +data Maybe a = Nothing | Just a +derive instance eqMaybe :: (Eq a) => Eq (Maybe a) + +lookup :: forall key1 key2 value + . Coercible key2 key1 => Hashable key1 + => Map key1 value -> key2 -> Maybe value +lookup (Map k value) key = + let + coercedKey :: key1 + coercedKey = coerce key + in if hash k == hash coercedKey + then Just value + else Nothing + +normalMap :: Map Int Int +normalMap = Map 4 28 + +-- This will output `true` +testLookupNormal :: Boolean +testLookupNormal = (lookup normalMap 4) == (Just 4) + +-- This will output `false` +testLookupSpecial :: Boolean +testLookupSpecial = (lookup specialMap 4) == (Just 4) + where + -- changes `Map 4 28` to `Map (SpecialInt 4) 28` + specialMap :: Map SpecialInt Int + specialMap = coerce normalMap + +-- To prevent this possibility from ever occurring, we indicate that +-- a type parameter's role is 'nominal'. Rewriting our `Map` implementation +-- so that `key` is nominal would prevent this from occurring. Since +-- the `value` type parameter does not affect the runtime representation, +-- it can be representational. + +data SafeMap key value = SafeMap key value + +type role SafeMap nominal representational diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/05-Typeclass-Relationships.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/04-Typeclass-Relationships.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/05-Typeclass-Relationships.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/04-Typeclass-Relationships.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/06-Typeclasses-with-No-Definitions.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/05-Typeclasses-with-No-Definitions.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/06-Typeclasses-with-No-Definitions.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/05-Typeclasses-with-No-Definitions.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/06-Type-Class-Kind-Signatures.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/06-Type-Class-Kind-Signatures.purs new file mode 100644 index 000000000..e34c4344a --- /dev/null +++ b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/06-Type-Class-Kind-Signatures.purs @@ -0,0 +1,39 @@ +module Syntax.Basic.Typeclass.KindSignatures where + +import Prelude + +{- +We saw previously that a data type can have a kind signature: +-} + +-- Kind Signature: Type -> Type -> Type +data ImplicitKindSignature1 a b = ImplicitKindSignature2 a b String + +data ExplicitKindSignature1 :: Type -> Type -> Type +data ExplicitKindSignature1 a b = ExplicitKindSignature1 a b String + +-- Kind Signature: Type -> Type +type ImplicitKindSignature2 a = ImplicitKindSignature1 a Int + +type ExplicitKindSignature2 :: Type -> Type +type ExplicitKindSignature2 a = ExplicitKindSignature1 a Int + +-- We also saw that we can use type classes to constrain data types +showStuff :: forall a. Show a => a -> String +showStuff a = "Showing 'a' produces " <> show a + +{- +It turns out that type classes can also have kind signatures. +However, rather than the right-most value representing a "concrete" type, +these represent a "concrete" constraint. -} + +-- Kind Signature: Type -> Consraint +class ImplicitKindSignature a where + someValue1 :: a -> String + +class ExplicitKindSignature :: Type -> Constraint +class ExplicitKindSignature a where + someValue2 :: a -> String + +-- Remember, `data` and `type`'s right-most entity/kind is `Type` whereas +-- type classes' right-most entity/kind is `Constraint`. diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/07-Multi-Paramter.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/07-Multi-Paramter.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/07-Multi-Paramter.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/07-Multi-Paramter.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/08-Functional-Dependencies.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/08-Functional-Dependencies.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/08-Functional-Dependencies.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/08-Functional-Dependencies.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/09-Instance-Chains.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/09-Instance-Chains.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/09-Instance-Chains.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/09-Instance-Chains.purs diff --git a/11-Syntax/01-Basic-Syntax/src/12-Newtypes/10-Keyword--Newtype.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/11-Keyword--Newtype.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/12-Newtypes/10-Keyword--Newtype.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/11-Keyword--Newtype.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/04-Deriving-Common-Typeclass-Instances-for-Custom-Types.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/21-Deriving-Common-Typeclass-Instances-for-Custom-Types.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/04-Deriving-Common-Typeclass-Instances-for-Custom-Types.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/21-Deriving-Common-Typeclass-Instances-for-Custom-Types.purs diff --git a/11-Syntax/01-Basic-Syntax/src/12-Newtypes/22-Deriving-Typeclass-Instances-for-Newtyped-Types.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/22-Deriving-Typeclass-Instances-for-Newtyped-Types.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/12-Newtypes/22-Deriving-Typeclass-Instances-for-Newtyped-Types.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/22-Deriving-Typeclass-Instances-for-Newtyped-Types.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/11-Type-Equality-Not-Propagate.purs b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/41-Type-Equality-Not-Propagate.purs similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/11-Type-Equality-Not-Propagate.purs rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/41-Type-Equality-Not-Propagate.purs diff --git a/11-Syntax/01-Basic-Syntax/src/11-TypeClasses/91-Designing-Typeclasses.md b/11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/91-Designing-Typeclasses.md similarity index 100% rename from 11-Syntax/01-Basic-Syntax/src/11-TypeClasses/91-Designing-Typeclasses.md rename to 11-Syntax/01-Basic-Syntax/src/11-TypeClasses-and-Newtypes/91-Designing-Typeclasses.md diff --git a/31-Design-Patterns/02-Partial-Functions/06-Using-Variant-Based-Errors.md b/31-Design-Patterns/02-Partial-Functions/06-Using-Variant-Based-Errors.md index 89fc969e0..fc624483b 100644 --- a/31-Design-Patterns/02-Partial-Functions/06-Using-Variant-Based-Errors.md +++ b/31-Design-Patterns/02-Partial-Functions/06-Using-Variant-Based-Errors.md @@ -1,3 +1,5 @@ # Using Variant-Based Errors -See [The Problem with Typed Errors](https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html) +See [The Problem with Typed Errors](https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html). + +You might consider using [`purescript-veither`](https://pursuit.purescript.org/packages/purescript-veither). diff --git a/31-Design-Patterns/02-Partial-Functions/Readme.md b/31-Design-Patterns/02-Partial-Functions/Readme.md index 3546d7cfd..53c45c329 100644 --- a/31-Design-Patterns/02-Partial-Functions/Readme.md +++ b/31-Design-Patterns/02-Partial-Functions/Readme.md @@ -13,8 +13,10 @@ There are three different ways one can handle partial functions in Purescript: - via `Maybe a` - via `Either String a` - via `Either CustomErrorType a` + - via `Veither errorRows a` 3. Use refined types - via `NonZeroInt` (or some other refined type) +4. Return the output on valid inputs and a default value on invalid inputs ## Compilation Instruction diff --git a/31-Design-Patterns/23-Stack-Safety.md b/31-Design-Patterns/23-Stack-Safety/01-Explicit-TCO.md similarity index 99% rename from 31-Design-Patterns/23-Stack-Safety.md rename to 31-Design-Patterns/23-Stack-Safety/01-Explicit-TCO.md index f3b96b05d..4e1e109be 100644 --- a/31-Design-Patterns/23-Stack-Safety.md +++ b/31-Design-Patterns/23-Stack-Safety/01-Explicit-TCO.md @@ -1,4 +1,4 @@ -# Stack Safety +# Explicit Tail-Call Optimization ## An Example of Stack-Unsafe Code diff --git a/31-Design-Patterns/23-Stack-Safety/02-Defunctionalization.md b/31-Design-Patterns/23-Stack-Safety/02-Defunctionalization.md new file mode 100644 index 000000000..ffafccc44 --- /dev/null +++ b/31-Design-Patterns/23-Stack-Safety/02-Defunctionalization.md @@ -0,0 +1,200 @@ +# Defunctionalization + +Or converting recursive stack-unsafe code into stack-safe code + +See these resources: +- [Don't Think, Just Defunctionalize](https://www.joachim-breitner.de/blog/778-Don%E2%80%99t_think%2C_just_defunctionalize) +- [The reasonable effectivness of the ConT monad](https://blog.poisson.chat/posts/2019-10-26-reasonable-continuations.html) +- [The best refactoring you've never heard of](https://web.archive.org/web/20201107223338/http://www.pathsensitive.com/2019/07/the-best-refactoring-youve-never-heard.html) + +The rest of this page will do two things: +1. Provide a few examples showing the simple but stack-unsafe code and its corresponding stack-safe but complex code. +2. Provide general principles to follow to help you figure out how to write stack-safe code for any given situation. + +## Stack-Safety: Opening Example + +The below examples move from simple to more complex. Start with the first one to get a general idea, make sure you understand it well, and only then move on to the next example. + +### Mapping a list from the last element to the first + +Let's say one had to change every element in a `List a` from type `a` to `b`. You're thinking, "That's easy. We'll just use `map`." Correct. + +But, let's add a new constraint: you must change the elements in a specific order. The first element you need to change is the last one in the list and the last element you should change is the first element in the list. In other words, a list like `4 : 3 : 2 : 1 : Nil` should have `1` changed first and `4` changed last. + +"Phwah!" you say. "I'll just use `reverse <<< map f <<< reverse` and call it a day!" As easy as that is to write, that will iterate through the list 3 times. Not the most performant thing in the world. + +So, you might write something like the following stack-unsafe code which iterates through a list twice, once as it descends down the list, and once as it ascends back up the list while constructing the returned value: +```purescript +mapLastElemFirst_unsafe :: forall a b. (a -> b) -> List a -> List b +mapLastElemFirst_unsafe f = case _ of + Nil -> Nil + Cons h tail -> do + let newTail = mapLastElemFirst_unsafe f tail -- stack unsafe! + Cons (f h) newTail +``` + +While the above code is short and easy to read, it'll also cause a stack-overflow error if you give it a large enough list. + +A stack-safe version of the above code might look like this. Unfortunately, it's not as easy to read if you're not familiar with this style of writing. However, it only iterates through the list twice, similar to the stack-unsafe version, and it will never throw a stack-overflow error: +```purescript +mapLastElemFirst_safe :: forall a b. (a -> b) -> List a -> List b +mapLastElemFirst_safe f ls = go Nil (Left ls) + where + -- To keep track of "where" we are in the data structure, we'll + -- maintain our own stack of what else still needs to be done. + -- + -- `Left` values represent items in the tree we haven't yet examined + -- and/or changed. + -- `Right` values represent either the final list or the current state + -- of the final list as we are constructing it. + go :: List a + -> Either (List a) (List b) + -> List b + go stack = case _ of + Left (Cons h tail) -> + -- we've hit the next element in the list + -- remember, we can't modify the value of type `a` + -- represented by `h` by calling `f h` because + -- this might not be the last element. + go (h : stack) (Left tail) + + Left Nil -> + -- we've hit the end of the list + -- we can now start consuming the stack we've created + go stack $ Right Nil + + Right val -> + -- `val` is either the final list or a portion of that final list + -- because we're still constructing it. + case stack of + -- `a` is the element next closest to the end of the original list. + -- We've already changed all elements after it + -- so we can now map it's type from `a` to `b`. + Cons a rest -> do + let b = f a + go rest $ Right (b : val) + + -- the stack is now empty; there's no more elements to map. + -- So, we return the final value + Nil -> val +``` + +To see the unsafe version fail and the safe version succeed with the same large input, see the [Writing Stack-Safe Code - Part 1](https://try.purescript.org/?gist=ae3abb190145838cffbe4a256ac0d123). + +### Zipping two lists together with one in reverse + +While the previous example is not the most realistic thing ever, it does set us up for the next twist. Let's say you need to zip two lists together where one has values in the opposite order (e.g. `4 : 3 : 2 : 1 : Nil`) and the other has values in the normal order (e.g. `1 : 2 : 3 : 4 : Nil`) . Your goal is to get the following `(Tuple 1 1) : (Tuple 2 2) : (Tuple 3 3) : (Tuple 4 4) : Nil`. + +You're right. Calling something like `zipOpposingOrder revList normList = zip (reverse revList) normList` might be the easier and better thing to do. Still, you might have some circumstances where a variation of this idea forces you to do things differently. Fortunately, our example above only needs to change slightly. + +The stack-unsafe code: +```purescript +zipOpposingOrder_unsafe :: forall a b. List a -> List b -> Maybe (List (Tuple a b)) +zipOpposingOrder_unsafe revList normalList = map snd $ go revList normalList + where + go :: List a -> List b -> Maybe (Tuple (List a) (List (Tuple a b))) + go revRemaining normalRemaining = case normalRemaining of + Nil -> + Just $ Tuple revRemaining Nil + + Cons headB tail -> do + -- We're using the Maybe monad here to make this easier to read. + -- A `Nothing` will be produced if the normal list had more elements + -- than reversed one did at this particular level or + -- if either one of the lists did at a deeper level + + -- finish zipping the tail first and return the remaining + -- elements from the `revRemaining` list + Tuple newRevRemaining newTail <- go revRemaining tail + + -- if the `newRevRemaining` has a value, zip it with this + -- level's value, and return the tail for the parent's + -- computation (if any) + { head: headA, tail: revTail } <- uncons newRevRemaining + pure $ Tuple revTail $ Cons (Tuple headA headB) newTail +``` + +Again, the above code is straight forward, but it'll cause a stack-overflow error if you give it a large enough input. + +A stack-safe version of the above code might look like this: +```purescript +zipOpposingOrder_safe :: forall a b. List a -> List b -> Maybe (List (Tuple a b)) +zipOpposingOrder_safe reveredList normalList = go reveredList Nil (Left normalList) + where + -- To keep track of "where" we are in the data structure, we'll + -- maintain our own stack of what else still needs to be done. + -- + -- `Left` values represent items in the normal list we haven't yet changed. + -- They will appear in a reversed order when we start consuming them. + -- + -- `Right` values will either be the final list or the current state + -- of the final list as it is being built. + go :: List a + -> List b + -> Either (List b) (List (Tuple a b)) + -> Maybe (List (Tuple a b)) + go revList stack = case _ of + Left (Cons head tail) -> + -- we've hit the next element in the list + -- remember, we can't merge the head value yet + -- because it might not be the last element. + go revList (head : stack) (Left tail) + + Left Nil -> + -- we've hit the end of the normal list + -- we can now start consuming the stack we've created + go revList stack $ Right Nil + + Right val -> + -- `val` is either the end of the final list (i.e. `Nil`) + -- or the current state of the final list since we're + -- still in the process of creating it. + -- We need to look at the stack to see what to do + case stack of + -- `b` is the element next closest to the end of the normal list + -- so we can now zip it together with the revList's next element + -- (if it exists) + Cons b restB -> case revList of + Cons a restA -> + go restA restB $ Right $ (Tuple a b) : val + + Nil -> + Nothing -- more elems in normalList than in reversedList + + -- the stack is now empty, so we return the final list + -- (assuming there wasn't any other values left in the reverse list). + Nil -> case revList of + Nil -> Just val + Cons _ _ -> Nothing -- more elems in reversedList than in normalList +``` + +To see the unsafe version fail and the safe version succeed with the same large input, see the [Writing Stack-Safe Code - Part 2](https://try.purescript.org/?gist=bb07a6e6b39ea5acd547e6d8ccf1ca80). + +### Topologically sorting a graph + +See [`topologicalSort` from `purescript-graphs`](https://github.com/purescript/purescript-graphs/blob/v5.0.0/src/Data/Graph.purs#L66-L114). We'll cover the types first and then explain each step in the computation. + +First, `SortState` stores 1) the final `result` that will be returned, which is either the final list or the current state of that still-being-constructed final list, and 2) the map of not yet visited keys. For this version of an FP graph, the map keys are directed edges that point towards a vertex and all other edges to which the vertex is connected and points. These keys will be removed from the map after they have been visited. + +Second, `SortStep` indicates two actions via defunctionalization: +- `Visit` means add the current key (i.e. edge) to the returned list and then add all of the edges coming out of the vertex to the stack, so that they can be visited, too. +- `Emit` means the key has already been visited and doesn't need to be checked again. Rather, add it to the final list that will be returned + +Below is a general explanation of the control flow: +1. Visit the first smallest key. Indicate that the key should be added to the final list via `Emit` and indicate that all of its relationships should be examined via `Visit`, remove the key from the keys map, then loop. +2. Visit the first key's first relationship. If it hasn't already been visited, do the same as step 1. If it has been visited, just loop. +3. At some point, the first key and all of its relationships will have been visited, and the stack will be a topologically-sorted list in reverse. All of its values will be `Emit key`. For example, it may store, `Emit 5 : Emit 4 : Emit 3 : Emit 2 : Emit 1 : Nil`, when the final list should be `1 : 2 : 3 : 4 : 5 : Nil`. +4. Now the stack is reversed, producing the final topologically-sorted list and storing it in `result`. +5. Since the stack is empty, the `visit` loop stops. We return to the `go` loop and find the next smallest key, repeating steps 1 - 5. +6. At some point, there are no more smallest keys because all keys have been visited. Thus, we get a `Nothing` in the `go` loop and we return the `result`, which is now a topologically-sorted list. + +## Principles + +1. Write a simpler stack-safe function that isn't performant (e.g. `reverse <<< map f <<< reverse`) +1. Once you know you need the performance, write a stack-unsafe function first that solves the problem +1. Make the function tail-recursive by calling the same function on every possible path except one +1. Use defunctionalization to indicate what should happen in each loop. + - Is it easier to read if you use explicit data constructors (e.g. `Visit` and `Emit` in the above graph example) or do you just need to distinguish between two states (e.g. `Either` in the above `mapLastElemFirs` List examples)? +1. Write the code "in order" of how it would execute. First X occurs, then Y, then Z. +1. Figure out what the stack should look like. + - Is it a simple `List a` or something more complicated like `List (Either a b)`? diff --git a/31-Design-Patterns/34-Defunctionalization.md b/31-Design-Patterns/34-Defunctionalization.md deleted file mode 100644 index e4fbdb3cc..000000000 --- a/31-Design-Patterns/34-Defunctionalization.md +++ /dev/null @@ -1,8 +0,0 @@ -# Defunctionalization - -Or converting recursive stack-unsafe code into stack-safe code - -See these resources: -- [Don't Think, Just Defunctionalize](https://www.joachim-breitner.de/blog/778-Don%E2%80%99t_think%2C_just_defunctionalize) -- [The reasonable effectivness of the ConT monad](https://blog.poisson.chat/posts/2019-10-26-reasonable-continuations.html) -- [The best refactoring you've never heard of](https://web.archive.org/web/20201107223338/http://www.pathsensitive.com/2019/07/the-best-refactoring-youve-never-heard.html) diff --git a/packages.dhall b/packages.dhall index 96852b7c5..00ce6fc4b 100644 --- a/packages.dhall +++ b/packages.dhall @@ -1,5 +1,5 @@ let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.14.1-20210516/packages.dhall sha256:f5e978371d4cdc4b916add9011021509c8d869f4c3f6d0d2694c0e03a85046c8 + https://github.com/purescript/package-sets/releases/download/psc-0.14.2-20210629/packages.dhall sha256:534c490bb73cae75adb5a39871142fd8db5c2d74c90509797a80b8bb0d5c3f7b let additions = { benchotron =