├── .gitignore ├── 01-Getting-Started ├── 01-Overview-of-Haskell.md ├── 02-Installing-Haskell.md └── 03-Haskell-REPL.md ├── 02-Syntax ├── 01-Primitive-DataTypes.md ├── 02-Type-Classes.md ├── 03-Functions-etc.md ├── 05-Record-Syntax.md ├── 06-Control-Flow.md ├── 07-Lazy-by-Default.md ├── 08-Modules.md ├── 09-Documentation.md ├── 10-Special-Compiler-Features.md ├── 11-Haskell-Only.md └── ReadMe.md ├── 03-Gotchas.md ├── 04-Build-Tools └── package-template.yml ├── 05-Yesod-and-Servant.md ├── 20-yesod ├── README.md ├── app │ ├── DevelMain.hs │ ├── devel.hs │ └── main.hs ├── config │ ├── favicon.ico │ ├── keter.yml │ ├── models │ ├── robots.txt │ ├── routes │ ├── settings.yml │ └── test-settings.yml ├── package.yaml ├── src │ ├── Application.hs │ ├── Foundation.hs │ ├── Handler │ │ ├── Common.hs │ │ └── Routes.hs │ ├── Import.hs │ ├── Import │ │ └── NoFoundation.hs │ ├── Model.hs │ ├── Model │ │ └── DataType.hs │ ├── Settings.hs │ └── Settings │ │ └── StaticFiles.hs ├── stack.yaml ├── stack.yaml.lock ├── static │ └── placeholder.txt ├── templates │ └── placeholder.txt └── test │ ├── Handler │ └── CommonSpec.hs │ ├── Spec.hs │ └── TestImport.hs ├── 21-servant ├── README.md ├── Setup.hs ├── app │ └── Main.hs ├── package.yaml ├── src │ ├── MinimalExample.hs │ └── ServantSyntax.hs ├── stack.yaml ├── stack.yaml.lock └── test │ └── Spec.hs ├── 91-Language-Extensions.md ├── Readme.md ├── assets ├── Haskell-Numeric-Type-Class-Hierarchy.graphml └── Haskell-Numeric-Type-Class-Hierarchy.svg └── table-of-contents.md /.gitignore: -------------------------------------------------------------------------------- 1 | regen-toc.sh 2 | 3 | **/*yesod/dist* 4 | **/*yesod/static/tmp/ 5 | **/*yesod/static/combined/ 6 | **/*yesod/config/client_session_key.aes 7 | **/*yesod/*.hi 8 | **/*yesod/*.o 9 | **/*yesod/*.sqlite3 10 | **/*yesod/*.sqlite3-shm 11 | **/*yesod/*.sqlite3-wal 12 | **/*yesod/.hsenv* 13 | **/*yesod/cabal-dev/ 14 | **/*yesod/.stack-work/ 15 | **/*yesod/.stack-work-devel/ 16 | **/*yesod/yesod-devel/ 17 | **/*yesod/.cabal-sandbox 18 | **/*yesod/cabal.sandbox.config 19 | **/*yesod/.DS_Store 20 | **/*yesod/*.swp 21 | **/*yesod/*.keter 22 | **/*yesod/*~ 23 | **/*yesod/\#* 24 | **/*yesod/*.cabal 25 | 26 | **/*servant/.stack-work/ 27 | **/*servant/*.cabal 28 | **/*servant/*~ 29 | 30 | **/bower_components/ 31 | **/node_modules/ 32 | **/.pulp-cache/ 33 | **/output/ 34 | **/generated-docs/ 35 | **/.psc-package/ 36 | **/.psc* 37 | **/.purs* 38 | **/.psa* 39 | **/.spago 40 | -------------------------------------------------------------------------------- /01-Getting-Started/01-Overview-of-Haskell.md: -------------------------------------------------------------------------------- 1 | # Overview of Haskell 2 | 3 | ## Haskell the Language 4 | 5 | Haskell the language is defined by the Haskell standard. This standard is redefined every so often. The original Haskell specification was Haskell 1998. The latest one is [Haskell 2010](https://www.haskell.org/onlinereport/haskell2010/). 6 | 7 | After a standard has been defined, anyone can propose adding a language extension to change how the language works. Since these divert from the standard, language extensions are completely opt-in: one has to enable them to use them. 8 | Extensions typically change what the syntax does, enables new syntax, enables new type system features, or other things like that. Developers propose and use language extensions to experiment with ideas. If an experimentation is successsful and many people begin to use such an extension, it can become a part of the Haskell standard. 9 | 10 | Since it has been ~9 years since the last standard was defined, there are a large number of language extensions that are used by most Haskell programmers but which aren't currently a part of the standard. When a new standard is defined, many of theses will likely become a part of the standard and will no longer need to be turned on. Unfortunately, the upcoming standard, Haskell 2020, seems to have been delayed due to "diffusion of responsibility" ([original comment](https://github.com/haskell/rfcs/issues/15#issuecomment-302138648) and [comment providing more context](https://github.com/haskell/rfcs/issues/15#issuecomment-302146265)) 11 | 12 | ## Haskell's Developer Tools 13 | 14 | These tools will be covered more in depth in the `Build Tools` folder. 15 | 16 | ### Compiler Tools 17 | 18 | Since the standard defines what Haskell is, a compiler is what turns Haskell source code into a working program. The Glasgow Haskell Compiler (GHC) is the state-of-the-art Haskell compiler. While there are other Haskell compilers, most refer to GHC when they say they program in Haskell. 19 | 20 | GHC comes with a tool called GHCi (i.e. GHC interactive). This is the GHC's REPL. It is vastly more powerful than PureScript's REPL. 21 | 22 | ### Dependency Managers / Build Tools 23 | 24 | #### Haskell uses Package Version Policy (PVP), not Semantic Versioning (SemVer) 25 | 26 | Semantic Versioning: MAJOR.MINOR.PATCH 27 | Haskell's Package Version Policy: MAJOR.MAJOR.MINOR.PATCH 28 | 29 | See [Haskell PVP FAQ](https://pvp.haskell.org/faq/) for a good explanation. 30 | 31 | #### Use `hpack` to generate `.cabal` files via `package.yml` files 32 | 33 | `.cabal` files store metadata about a package (e.g. author, contact info, dependencies, language extensions enabled by default, etc.). This is the file format used in the upcoming build tools. Unfortunately, the file format used by `.cabal` is verbose. 34 | 35 | [`hpack`](https://github.com/sol/hpack#readme) is a tool that can read and use `package.yml` files to produce valid `.cabal` files. `package.yml` files come with three advantages over `.cabal` files. They... 36 | > Don't require the user to state the obvious; make sensible assumptions by default 37 | > Give the user 100% control when needed 38 | > Don't require the user to repeat things; facilitate DRY-ness 39 | 40 | As we'll see in the "Syntax" folder, there are a lot of language extensions you will likely want to enable. Moreover, there are GHC options you will likely want to turn on by default. Rather than enabling each extension in each file or including each GHC option in the CLI, it can be easier to add these across the entire project via your `package.yml` file. 41 | 42 | Both `cabal` (lowercased 'c') and `stack` utilize the `.cabal` file. So, it's often better to edit your `package.yml` file, use `hpack` to generate a valid `.cabal` file, and let the tools use that generated `.cabal` file. 43 | 44 | #### Cabal and Stack 45 | 46 | The community is strongly split between `Cabal`/`cabal` and `stack`. Originally, there was only `Cabal`/`cabal`. `Stack` came about to fix a "dependency hell" problem with Cabal that arose due to transitive dependencies. `Cabal`/`cabal` then fixed this problem and added a few other features. I think they have similar features now. 47 | 48 | `cabal` has "v1" commands and "v2" commands. You should always use the "v2" commands. 49 | 50 | PureScript developers tend to use one of two tools to install dependencies and build their code: 51 | 1. `bower` + `pulp` 52 | 2. `package-sets` + `spago` (the dependency manager parts) and `spago` (the build tool part). 53 | 54 | The `bower + pulp` workflow has similarities to Haskell's `Cabal` library (i.e. the library that reads `.cabal` files and builds projects) and `cabal` tool (i.e. the tool that people use to package Haskell projects). 55 | Similarly, the package sets used by PureScript's `spago` for dependency management and the build tool aspect of `spago` is similar to the Haskell's `Stack`. `Stack` uses a curated set of packages that is defined via resolvers (resolvers are similar to [`purescript/package-sets` releases](https://github.com/purescript/package-sets/releases)). Unlike `package-sets`, `stack` does not use Dhall to produce the curated sets. 56 | 57 | For a somewhat longer overview of `cabal` and `stack`, see [Why not both?](https://medium.com/@fommil/why-not-both-8adadb71a5ed) 58 | 59 | ## Documentation Tools 60 | 61 | | Name | PureScript Corresponding Tool | Clarifications | 62 | | - | - | - | 63 | | Hackage | Pursuit without a search engine | Only provides the documentation. Nothing else | 64 | | Hoogle | Pursuit's search engine | Via a search, one can find the corresponding Hackage documentation for an entity (i.e. package, module, function, value, type class, etc.) 65 | | Stackage | -- | Stackage is what we would get if Spago created an instance of Pursuit that only had documentation for packages in a given package set. 66 | 67 | ## Miscellaneous Tools 68 | 69 | - [`hlint`](https://github.com/ndmitchell/hlint) is a tool that provides suggestions for improving your code. 70 | -------------------------------------------------------------------------------- /01-Getting-Started/02-Installing-Haskell.md: -------------------------------------------------------------------------------- 1 | # Installing Haskell 2 | 3 | Note: this file is still a WIP. 4 | 5 | ## Installation via Stack 6 | 7 | We'll install Haskell via Stack. However, we'll do it "correctly" in that we won't install a specific version of Haskell across your entire computer. Rather, we'll install a specific version that can be used in projects that wish to use that specific version of Haskell later on. 8 | 9 | ```bash 10 | # 1. Install Stack 11 | curl -sSL https://get.haskellstack.org/ | sh 12 | # 2. Upgrade Stack 13 | stack upgrade 14 | # 3. Create a new Stack-based project 15 | # in the `~/Projects/haskell-project/` directory 16 | cd ~/Projects 17 | stack new haskell-project 18 | 19 | # 4. Change directory into the project's root folder 20 | cd haskell-project/ 21 | 22 | # 5. Download a version of the GHC without installing it globally on your computer 23 | # This will take a long time to do the first time, but will be quick every 24 | # time thereafter. 25 | stack setup 26 | 27 | # 6. Build the sample project 28 | stack build 29 | ``` 30 | 31 | ## Installing Developer Tools 32 | 33 | Once the above commands have been run, you should install some additional tools to help with development. **WARNING: If you were trying to install a tool called `someTool` (made-up name), most blog posts will tell you to run `stack install someTool`. Don't do that!** This will install that GHC-specific-version package globally. If you ever use a different GHC version, you will get conflicts. New learners think `stack install someTool` means there is a corresponding `stack uninstall someTool`. That is not the case. `stack install someTool` is an alias for `stack build --copy-bins someTool`. **The safer route is to run `stack build --copy-compiler-tool someTool` instead.** (~ [source: an opinionated guide to Haskell in 2018](https://lexi-lambda.github.io/blog/2018/02/10/an-opinionated-guide-to-haskell-in-2018/)) 34 | 35 | Tools we'll be installing: 36 | - hlint - provides suggestions for how to fix errors and otherwise improve the quality of your code 37 | - hoogle - search Haskell's API docs 38 | - weeder - removes dead code 39 | 40 | Tools we won't be installing 41 | - `ghc-mod` because it doesn't appear to work on more recent versions of Haskell (e.g. `8.4.x` and up) due to requiring a specific version range of `base`. 42 | 43 | ```bash 44 | # 6. Install useful tools in the project, but not globally on your computer* 45 | stack build --copy-compiler-tool hlint hoogle weeder 46 | ``` 47 | 48 | ## Generating Hoogle database 49 | 50 | ```bash 51 | stack hoogle --server 52 | # You can now open up a Hoogle instance for your project and its dependencies 53 | # by opening a browser to `http://127.0.0.1:` 54 | 55 | # Generate documentation for your project, so that it's accessible 56 | # for other tools (e.g. nvim below) 57 | stack exec -- hoogle generate 58 | ``` 59 | 60 | ## Editor Support 61 | 62 | GHC's REPL (i.e. GHCi, which can be started via `stack ghci`) can get you pretty far. However, if you want more IDE integration, follow these instructions 63 | 64 | I tried setting up editor support for Atom, but couldn't get the plugins to work. I'm not entirely sure why. 65 | 66 | Thus, this shows how to set up an IDE via the Language Server Protocol and Neovim. 67 | 68 | ### NeoVim 69 | 70 | 1. [Install neovim](https://github.com/neovim/neovim/wiki/Installing-Neovim) 71 | 2. [Install `vim-plugin`](https://github.com/junegunn/vim-plug) 72 | 3. Start NeoVim `nvim` 73 | 4. Type `:edit $MYVIMRC` to edit the `init.vim` file 74 | 5. Type `i` to go to Insert mode 75 | 6. Paste [my `init.vim` file](https://github.com/JordanMartinez/_dotfiles/blob/master/init.vim) via `CTRL+SHIFT+V` 76 | - [Top 50 `init.vim` settings](https://www.shortcutfoo.com/blog/top-50-vim-configuration-options/) 77 | - [List of all `init.vim` settings](https://stackoverflow.com/questions/30290685/complete-list-of-all-vimrc-configuration-options) 78 | 7. Type `ESC` to return to Commande mode 79 | 8. Type `:wq` to save the changes and exit NeoVim 80 | 81 | Note: if you are new to NeoVim, refer to the below instructions. Learn NeoVim by using it, not by reading about it. Muscle memory takes time and usage: 82 | 1. Start NeoVim via `nvim` 83 | 2. Type `:Tutor` and press Enter 84 | 3. Read the instructions. 85 | - `h` is the left arrow, `j` down arrow, `k` up arrow, and `l` right arrow. 86 | - type `:close` to close windows. Sometimes, their instructions will open up a new window but they don't tell you how to close them via this command until later. 87 | - type `CTRL+W` two times to switch to a different window 88 | - type `:q` to quit NeoVim 89 | 4. After completing it, see the [Vim Cheatsheet](https://www.fcodelabs.com/2018/12/08/Vim-Cheats/) 90 | 5. Then see the first few chapters for [Learn Vimscript the Hard Way](http://learnvimscriptthehardway.stevelosh.com/) to understand how to remap inputted keys to quick commands. 91 | 92 | ### Haskell IDE Engine 93 | 94 | Haskell IDE Engine (i.e. HIE for short) is what I used because I couldn't figure out how to get `ghcide` to work. 95 | 96 | - [Install haskell-ide-engine](https://github.com/haskell/haskell-ide-engine#installation-from-source) 97 | - Install HIE via stack by running `stack ./install.hs stack-hie-8.6.5` while in the project's directory. 98 | - Start NeoVim via `nvim` 99 | - Type `:CocInfo` and press Enter 100 | - Copy and paste the following to get both Haskell and PureScript language servers ([haskell source](https://github.com/haskell/haskell-ide-engine#Coc) & [purescript source](https://github.com/neoclide/coc.nvim/wiki/Language-servers#purescript)) 101 | 102 | ```json 103 | { 104 | "languageserver": { 105 | "haskell": { 106 | "command": "hie-wrapper", 107 | "rootPatterns": [ 108 | ".stack.yaml", 109 | "cabal.config", 110 | "package.yaml" 111 | ], 112 | "filetypes": [ 113 | "hs", 114 | "lhs", 115 | "haskell" 116 | ], 117 | "initializationOptions": { 118 | "languageServerHaskell": { 119 | } 120 | } 121 | }, 122 | "purescript": { 123 | "command": "purescript-language-server", 124 | "args": ["--stdio"], 125 | "filetypes": ["purescript"], 126 | "rootPatterns": ["bower.json", "psc-package.json", "spago.dhall"] 127 | } 128 | } 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /01-Getting-Started/03-Haskell-REPL.md: -------------------------------------------------------------------------------- 1 | # Haskell REPL 2 | 3 | ## Quick Broad Overview 4 | 5 | Assuming your current working directory is still `~/Haskell-project`, run `stack ghci` to start the repl. 6 | 7 | Once started, here's a quick overview of some of the things you can do to make the experience better. Once done here, skim through the [GHCi User Manual](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html): 8 | ``` 9 | -- Change the prompt, so that it doesn't show all modules that are currently imported. 10 | *Main Lib> :set prompt "> " 11 | > -- now the promp is '> ' 12 | 13 | -- Print a number 14 | > 4 15 | 4 16 | 17 | -- Use `:{` to start a multi-line expression 18 | -- and end it with :} 19 | > :{ 20 | *Main Lib| foo :: Int 21 | *Main Lib| foo = 4 22 | *Main Lib| :} 23 | > foo 24 | 4 25 | 26 | -- Fix the "*Main Lib| " prompt 27 | > :set prompt-cont "| " 28 | > :{ 29 | | foo :: Int 30 | | foo = 4 31 | | :} 32 | > foo 33 | 4 34 | 35 | -- Also print the type of the output 36 | > :set +t -- use `:unset +t` to unset 37 | > 4 38 | 4 39 | it :: Num p => p 40 | 41 | -- Show dependencies whose modules can be imported. 42 | -- If your `packages.yml` file includes 43 | -- additional dependencies, they'll 44 | -- show up here. 45 | > :show linker 46 | ----- Linker state ----- 47 | Pkgs: [base-4.12.0.0, integer-gmp-1.0.2.0, ghc-prim-0.5.3, rts-1.0] 48 | Objs: [] 49 | BCOs: [] 50 | 51 | -- Show modules that are currently imported 52 | > :show imports 53 | import Lib 54 | :module +*Main -- added automatically 55 | 56 | -- import a module, whether from your library or from a dependency 57 | > import Data.Int 58 | > :show imports 59 | import Lib 60 | import Data.Int 61 | :module +*Main -- added automatically 62 | 63 | -- unimport a module 64 | > :module - Data.Text 65 | > :show imports 66 | import Lib 67 | :module +*Main -- added automatically 68 | 69 | -- Browse a given module 70 | > :browse Lib 71 | someFunc :: IO () 72 | 73 | -- Browse a given module and show more detail 74 | -- The below example isn't a good one unfortunately. 75 | > :browse! Lib 76 | someFunc :: IO () 77 | 78 | -- Show the source code for an identifier 79 | > :list someFunc 80 | 5 someFunc :: IO () 81 | 6 someFunc = putStrLn "someFunc" 82 | 7 83 | 84 | -- Try calling `:browse Prelude` 85 | 86 | -- Get the type of an expression 87 | > :type [1, 2 ,3] 88 | [1, 2 ,3] :: Num a => [a] 89 | 90 | -- Get the fully evaluated type of an expression...? 91 | > :type +d [1, 2 ,3] 92 | [1, 2 ,3] :: [Integer] 93 | 94 | -- Get the documentation for an entity 95 | > :doc words 96 | 'words' breaks a string up into a list of words, which were delimited 97 | by white space. 98 | 99 | >>> words "Lorem ipsum\ndolor" 100 | ["Lorem","ipsum","dolor"] 101 | 102 | -- Enable a language extension 103 | > :set -XOverloadedStrings 104 | 105 | -- Enable a GHC option 106 | > :set -Wall 107 | 108 | -- Quit the REPL 109 | > :quit 110 | 111 | -- Now all of the settings you set and bindings you made are gone. 112 | 113 | -- Restarting the GHCi... 114 | stack ghci 115 | -- output... 116 | 117 | -- The prompt is back to what it was previously. 118 | *Main Lib> 119 | ``` 120 | To load the GHCi with your default settings, such as `:set +t` or `:set prompt "> "`, create a file in your Haskell project called `.ghci` and list each setting on a new line. 121 | 122 | An example `.ghci` file might look like this: 123 | ``` 124 | :set +t 125 | :set prompt "> " 126 | :set prompt-cont "> " 127 | :set -Wall 128 | :set -XOverloadedStrings 129 | ``` 130 | 131 | ## Scoping Rules in GHCi 132 | 133 | Read through [What's realy in scope at the prompt?](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html#what-s-really-in-scope-at-the-prompt) 134 | -------------------------------------------------------------------------------- /02-Syntax/01-Primitive-DataTypes.md: -------------------------------------------------------------------------------- 1 | # Primitive Data Types 2 | 3 | PureScript defines a few types in the compiler. These are automatically imported via the `Prim` module. Haskell has a package called `base` that includes a number of modules that operate similarly to `Prim`. These are normally imported via the `Prelude` module, which is imported implicitly. We'll talk about imports later. 4 | 5 | ## Comments 6 | 7 | These are the same for both languages. Documentation is different and will be covered later: 8 | 9 | ```purescript 10 | -- single-line comment 11 | {- 12 | multi- 13 | line 14 | comment 15 | -} 16 | ``` 17 | 18 | ```haskell 19 | -- single-line comment 20 | {- 21 | multi- 22 | line 23 | comment 24 | -} 25 | ``` 26 | 27 | ## The `data` Keyword 28 | 29 | ### Basic 30 | 31 | These are the same. 32 | 33 | ```purescript 34 | -- Sum and product types 35 | data TypeConstructor aType bType hktBy1 phantomType 36 | = NoArgs 37 | | Args Type1 Type2 Type3 38 | | FunctionArg (Type1 -> Type2) 39 | | NestedArg (Box Int) 40 | | DoubleNestedArg (Box (Box Int)) 41 | | HigherKindedGenericType1 (hktBy1 Int) 42 | | HigherKindedGenericType2 (hktBy1 aType) 43 | | Recursive (TypeConstructor aType bType hktBy1 phantomType) 44 | | ArgMix Type_ (A -> B) bType (TypeConstructor aType bType hktBy1 phantomType) 45 | ``` 46 | 47 | ```haskell 48 | -- Sum and product types 49 | data TypeConstructor aType bType hktBy1 phantomType 50 | = NoArgs 51 | | Args Type1 Type2 Type3 52 | | FunctionArg (Type1 -> Type2) 53 | | NestedArg (Box Int) 54 | | DoubleNestedArg (Box (Box Int)) 55 | | HigherKindedGenericType1 (hktBy1 Int) 56 | | HigherKindedGenericType2 (hktBy1 aType) 57 | | Recursive (TypeConstructor aType bType hktBy1 phantomType) 58 | | ArgMix Type_ (A -> B) bType (TypeConstructor aType bType hktBy1 phantomType) 59 | ``` 60 | 61 | ### PureScript's "undefined" and Haskell's `undefined` / bottom 62 | 63 | Haskell has a value known as "bottom." It is typically referred to via `undefined`. When it is evaluated, it produces a runtime error. PureScript can produce a similar value but it takes more effort: 64 | 65 | ```purescript 66 | import Effect.Unsafe (unsafePerformEffect) 67 | import Effect.Exception (throwException, error) 68 | 69 | undefined :: forall a. a 70 | undefined = unsafePerformEffect (throwException (error "runtime exception")) 71 | ``` 72 | 73 | ```haskell 74 | undefined :: forall a. a 75 | undefined = error "runtime exception" 76 | ``` 77 | 78 | ### Using Kind Signatures 79 | 80 | PureScript's kind signatures work out-of-box. Haskell requires a language extension called `KindSignatures`. 81 | 82 | ```purescript 83 | data SomeProxy (functor :: Type -> Type) = SomeProxy 84 | ``` 85 | 86 | ```haskell 87 | {-# LANGUAGE KindSignatures #-} 88 | 89 | data SomeProxy (functor :: * -> *) = SomeProxy 90 | ``` 91 | 92 | ### Defining Custom Kinds 93 | 94 | PureScript uses foreign imports to create custom kinds and their values. Haskell uses the `DataKinds` language extension to create such things for you. 95 | 96 | ```purescript 97 | {- 98 | -- type-level type and its type-level values 99 | data MyKind 100 | = First 101 | | Second 102 | -} 103 | foreign import kind MyKind 104 | foreign import data First :: MyKind 105 | foreign import data Second :: MyKind 106 | ``` 107 | 108 | ```haskell 109 | {-# LANGUAGE DataKinds #-} 110 | 111 | data MyKind 112 | = First 113 | | Second 114 | 115 | {- 116 | -- These type-level entities are created as well 117 | 118 | data 'MyKind 119 | = 'First 120 | | 'Second 121 | -} 122 | ``` 123 | 124 | ## The `newtype` Keyword 125 | 126 | These are the same 127 | 128 | ```purescript 129 | newtype Age = Age OriginalType 130 | ``` 131 | 132 | ```haskell 133 | newtype Age = Age OriginalType 134 | ``` 135 | 136 | ## The `type` keyword 137 | 138 | ### Basic 139 | 140 | These are the same 141 | ```purescript 142 | type TypeAlias = OriginalType 143 | ``` 144 | 145 | ```haskell 146 | type TypeAlias = OriginalType 147 | ``` 148 | 149 | ### Using `forall` in the Type Alias Declaration 150 | 151 | PureScript's works out-of-box. Haskell requires a language extension called `LiberalTypeSynonyms` 152 | 153 | ```purescript 154 | type ProduceSomeB b = forall a. a -> b 155 | ``` 156 | 157 | ```haskell 158 | {-# LANGUAGE LiberalTypeSynonyms #-} 159 | type ProduceSomeB b = forall a. a -> b 160 | ``` 161 | 162 | ### Using Kind Signatures 163 | 164 | PureScript's kind signatures work out-of-box. Haskell requires a language extension called `KindSignatures`. 165 | 166 | ```purescript 167 | type MyType (functor :: Type -> Type) a = a 168 | ``` 169 | 170 | ```haskell 171 | {-# LANGUAGE KindSignatures #-} 172 | 173 | type MyType (functor :: * -> *) a = a 174 | ``` 175 | 176 | ## Empty Data Types 177 | 178 | PureScript can define data types that don't have a constructor (usually for phantom types that tag something at the type-level). Haskell requires the `EmptyDataDecls` language extension to be enabled. 179 | 180 | ```purescript 181 | data PhantomType_NoConstructors 182 | ``` 183 | 184 | ```haskell 185 | {-# LANGUAGE EmptyDataDecls #-} 186 | 187 | data PhantomType_NoConstructors 188 | ``` 189 | 190 | ## Primitive-ish Types 191 | 192 | ### Boolean 193 | 194 | ```purescript 195 | trueValue :: Boolean 196 | trueValue = true 197 | 198 | falseValue :: Boolean 199 | falseValue = false 200 | ``` 201 | 202 | ```haskell 203 | trueValue :: Bool 204 | trueValue = True 205 | 206 | falseValue :: Bool 207 | falseValue = False 208 | ``` 209 | 210 | ### Array and List 211 | 212 | #### Basic 213 | 214 | Purescript uses `[]` syntax for Arrays. Haskell uses `[]` syntax for `List`s. 215 | 216 | ```purescript 217 | data List a 218 | = Nil 219 | | Cons a (List a) 220 | 221 | intList :: List Int 222 | intList = Cons 1 (Cons 2 (Cons 3 Nil)) 223 | 224 | infixr 4 Cons as : 225 | 226 | anotherList :: List Int 227 | anotherList = 1 : 2 : 3 : Nil 228 | 229 | intArray :: Array Int 230 | intArray = [1, 2, 3] 231 | ``` 232 | 233 | ```haskell 234 | import Data.Vector (generate) -- i.e. `forall a. Int -> (Int -> a) -> Vector a` 235 | 236 | -- type List a = [a] 237 | 238 | intList :: [Int] -- i.e. `List Int` 239 | intList = [1, 2, 3] -- i.e. 1 : 2 : 3 : Nil where `:` is `Cons` 240 | 241 | intArray :: Vector Int 242 | intArray = generate 4 id -- i.e. `id :: forall a. a -> a` 243 | ``` 244 | 245 | #### Additional Comparisons and Defining Ranges 246 | 247 | In PureScript, we can define a range of values using a function. Haskell has syntax directly for this: 248 | 249 | ```pureScript 250 | [] -- Empty Array 251 | [x] -- Array with 1 element 252 | [x,y,z] -- Array with 3 elements 253 | 254 | -- Not supported 255 | -- [x .. ] -- enumFrom x 256 | -- [x,y ..] -- enumFromThen x y 257 | 258 | x .. y -- `x .. y` is infix for `range x y` from `Data.Array (range)` 259 | 260 | -- Not supported 261 | -- [x,y .. z] -- enumFromThenTo x y z 262 | ``` 263 | 264 | [These examples are taken from the examples in `OverloadedLists` extension](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-OverloadedLists) 265 | ```haskell 266 | [] -- Empty list 267 | [x] -- x : [] 268 | [x,y,z] -- x : y : z : [] 269 | [x .. ] -- enumFrom x 270 | [x,y ..] -- enumFromThen x y 271 | [x .. y] -- enumFromTo x y 272 | [x,y .. z] -- enumFromThenTo x y z 273 | ``` 274 | 275 | #### The `OverloadedLists` Language Extension 276 | 277 | Fortunately, Haskell can enable the `OverloadedLists` extension to make the `[]` syntax work on more things. Here's how it works. There's a type clas called `IsList`: 278 | 279 | ```haskell 280 | class IsList l where 281 | type Item l 282 | 283 | fromList :: [Item l] -> l 284 | toList :: l -> [Item l] 285 | 286 | fromListN :: Int -> [Item l] -> l 287 | fromListN _ = fromList 288 | ``` 289 | 290 | When the extension is enabled, the above examples are desugared to this: 291 | 292 | ```haskell 293 | [] -- fromListN 0 [] 294 | [x] -- fromListN 1 (x : []) 295 | [x,y,z] -- fromListN 3 (x : y : z : []) 296 | [x .. ] -- fromList (enumFrom x) 297 | [x,y ..] -- fromList (enumFromThen x y) 298 | [x .. y] -- fromList (enumFromTo x y) 299 | [x,y .. z] -- fromList (enumFromThenTo x y z) 300 | ``` 301 | 302 | Thus, one can use this syntax to write: 303 | ```haskell 304 | ['0' .. '9'] :: Set Char 305 | [1 .. 10] :: Vector Int 306 | [("default",0), (k1,v1)] :: Map String Int 307 | ['a' .. 'z'] :: Text 308 | ``` 309 | 310 | Returning to our previous example, we could write: 311 | ```haskell 312 | {-# LANGUAGE OverloadedLists #-} 313 | import Data.Vector 314 | 315 | intList :: [Int] -- i.e. `List Int` 316 | intList = [1, 2, 3] -- i.e. 1 : 2 : 3 : Nil where `:` is `Cons` 317 | 318 | intArray :: Vector Int 319 | intArray = [1, 2, 3] 320 | ``` 321 | 322 | ### Char 323 | 324 | PureScript's `Char` is a single character (i.e. UFT-16 code **unit**) and cannot represent code point values greater than `0xFFFF`. Haskell's `Char` is "an enumeration whose values represent Unicode (or equivalently ISO/IEC 10646) code **point**s." 325 | 326 | 327 | ```purescript 328 | aChar :: Char 329 | aChar = 'x' 330 | ``` 331 | 332 | ```haskell 333 | aChar :: Char 334 | aChar = 'x' 335 | ``` 336 | 337 | ### String 338 | 339 | #### The Type 340 | 341 | PureScript's Strings are UTF-16 due to JavaScript. Haskell's `String`s are unfortunately an alias to `[Char]` (i.e. `List Char`). Moreover, its syntax `"example string"` is of type `String`/`[Char]`. 342 | 343 | ```purescript 344 | stringValue :: String 345 | stringValue = "I am a string" 346 | ``` 347 | 348 | ```haskell 349 | type String = [Char] -- aka `List Char` 350 | 351 | notStringValue :: String -- aka `List Char` 352 | notStringValue = "I am a singly-linked list of characters, not a String" 353 | ``` 354 | 355 | #### The Real Haskell String Types 356 | 357 | Due to the above situation, everyone uses `Text` as the main `String` type instead of `[Char]`/`String` or `ByteString` when referring to a binary representation of a `Text` value. 358 | 359 | Since the literal syntax `"string value"` will define a value that has type `[Char`], how can people easily create `Text` or `ByteString` values? They use one of two options: 360 | - Use the corresponding type's `pack`/`unpack` function 361 | - Enable the `OverloadedStrings` language extension. 362 | 363 | ```haskell 364 | import Data.Text (pack) 365 | 366 | realStringValue :: Text 367 | realStringValue = 368 | pack "The output of `pack` will be the String data type you're used to." 369 | ``` 370 | 371 | ```haskell 372 | import Data.ByteString (pack) 373 | 374 | binaryStringValue :: ByteString 375 | binaryStringValue = 376 | -- i.e. "a string value" 377 | pack [97,32,115,116,114,105,110,103,32,118,97,108,117,101] 378 | ``` 379 | 380 | #### The `OverloadedStrings` Language Extension 381 | 382 | Similar to the `OverloadedLists` extension above, the double-quote syntax can be overridden so that it does not refer to Haskell's `[Char]` type. Here's how it works. There's a type class called `IsString`: 383 | 384 | ```haskell 385 | class IsString a where 386 | fromString :: String -> a 387 | ``` 388 | 389 | When `OverloadedStrings` is enabled, this syntax `"text"` becomes `fromString "text"`. As a result, we can rewrite the above `Text` and `ByteString` examples as: 390 | 391 | ```haskell 392 | {-# LANGUAGE OverloadedStrings #-} 393 | import Data.Text 394 | 395 | realStringValue :: Text 396 | realStringValue = 397 | "The output of `pack` will be the String data type you're used to." 398 | ``` 399 | 400 | ```haskell 401 | {-# LANGUAGE OverloadedStrings #-} 402 | import Data.ByteString 403 | 404 | binaryStringValue :: ByteString 405 | binaryStringValue = "a string value" 406 | ``` 407 | 408 | #### Syntax Sugar 409 | 410 | ```purescript 411 | slashy_string_syntax :: String 412 | slashy_string_syntax = 413 | "Enables multi-line strings that \ 414 | \use slashes \ 415 | \regardless of indentation \ 416 | 417 | \and regardless of vertical space between them \ 418 | 419 | \(though you can't put comments in that blank vertical space)" 420 | {- 421 | "This will fail \ 422 | -- oh look a comment that breaks this! 423 | \to compile." 424 | -} 425 | 426 | triple_quote_string_syntax :: String 427 | triple_quote_string_syntax = """ 428 | Multi-line string syntax that also ignores escaped characters, such as 429 | * . $ [] 430 | It's useful for regular expressions 431 | """ 432 | ``` 433 | 434 | ```haskell 435 | slashy_string_syntax :: String 436 | slashy_string_syntax = 437 | "Enables multi-line strings that \ 438 | \use slashes \ 439 | \regardless of indentation \ 440 | 441 | \and regardless of vertical space between them \ 442 | 443 | \(though you can't put comments in that blank vertical space)" 444 | {- 445 | "This will fail \ 446 | -- oh look a comment that breaks this! 447 | \to compile." 448 | -} 449 | 450 | -- This doesn't compile. I'm not sure whether this can be defined in a 451 | -- different way 452 | -- triple_quote_string_syntax :: String 453 | -- triple_quote_string_syntax = """ 454 | -- Multi-line string syntax that also ignores escaped characters, such as 455 | -- * . $ [] 456 | -- It's useful for regular expressions 457 | -- """ 458 | ``` 459 | 460 | ### Int 461 | 462 | ```purescript 463 | anInt :: Int 464 | anInt = -4 + 8 465 | ``` 466 | 467 | Haskell's Integers 468 | 469 | Fixed Size 470 | 471 | | # of Bits | Unsigned | Signed | 472 | | - | - | - | 473 | | 4 | Word | Int | 474 | | 8 | Word8 | Int8 | 475 | | 16 | Word16 | Int16 | 476 | | 32 | Word32 | Int32 | 477 | | 64 | Word64 | Int64 | 478 | 479 | Arbitrarily long: `Integer` 480 | 481 | ```haskell 482 | anInt :: Int -- fixed size; interger overflow can occur 483 | anInt = -4 + 8 484 | 485 | aReallyBigInt :: Integer -- however big you want it 486 | aReallyBigInt = 99999999999999999999999999999999999999999999999999999999999999 487 | ``` 488 | 489 | ### Number 490 | 491 | #### Basic 492 | 493 | PureScript's `Number` is a double-precision floating-point number due to JavaScript. Haskell's corresponding type is `Double`. 494 | 495 | ```purescript 496 | aNumber :: Number 497 | aNumber = 4.0 498 | ``` 499 | 500 | ```haskell 501 | aNumber1 :: Float -- single-precision floating-point numbers 502 | aNumber1 = 4.0 503 | 504 | aNumber2 :: Double -- double-precision floating-point numbers 505 | aNumber2 = 4.0 506 | ``` 507 | 508 | #### Underscore Syntax Sugar for Ints and Numbers 509 | 510 | Since its `0.13.0` release, PureScript allows the usage of underscores to make numeric values more readable. Haskell allows this, too, but only if the `NumericUnderscores` language extension is enabled. 511 | 512 | ```purescript 513 | readableInt :: Int 514 | readableInt = 1_000_000 515 | 516 | readableNumber :: Number 517 | readableNumber = 1_000_000.0 518 | ``` 519 | 520 | ```haskell 521 | {-# LANGUAGE NumericUnderscores #-} 522 | readableInt :: Int 523 | readableInt = 1_000_000 524 | 525 | readableNumber :: Double 526 | readableNumber = 1_000_000.0 527 | ``` 528 | 529 | #### Dealing with Negated Literals 530 | 531 | PureScript handles negative literals properly. Haskell requires parenthesis. One can remove this by enabling [the `NegativeLiterals` language extension](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-NegativeLiterals). See the extension for what else it affects. 532 | 533 | ```purescript 534 | repl> 4 + -4 535 | 0 536 | ``` 537 | 538 | ```haskell 539 | repl> 4 + -4 540 | Precedence parsing error 541 | cannot mix ‘+’ [infixl 6] and prefix `-' [infixl 6] in the same 542 | infix expression 543 | repl> 4 + (-4) 544 | 0 545 | repl> :set -XNegativeLiterals 546 | repl> 4 + -4 547 | 0 548 | ``` 549 | 550 | ## Unit and Row Kinds 551 | 552 | PureScript uses the `Unit` type and `unit` for its value; it uses `()` to denote an empty row and `(a :: String)` for a single closed row of types. 553 | 554 | Haskell uses `()` to refer to both the `Unit` type and `unit` value. 555 | 556 | ```purescript 557 | type EmptyRow = () 558 | type SingleRow = (a :: String) 559 | 560 | data Unit = Unit 561 | 562 | unit :: Unit 563 | unit = Unit 564 | 565 | provideAUnitValue :: Unit 566 | provideAUnitValue = unit 567 | ``` 568 | 569 | ```haskell 570 | -- data () = () -- basic idea... 571 | 572 | provideUnitValue :: () 573 | provideUnitValue = () 574 | ``` 575 | 576 | ## Tuple Syntax 577 | 578 | PureScript's `Tuple` is just like other data types. Haskell has special syntax for it: `(,)`, `(,,)`, etc. 579 | 580 | ```purescript 581 | import Data.Tuple (Tuple(..)) 582 | import Data.Tuple.Nested ((/\)) 583 | 584 | -- data Tuple a b = Tuple a b 585 | 586 | foo :: forall a b. Tuple a b -> a 587 | foo (Tuple a _) = a 588 | 589 | nestedTuple :: forall a b c d. Tuple a (Tuple b (Tuple c (Tuple d))) 590 | nestedTuple = Tuple a (Tuple b (Tuple c (Tuple d))) 591 | 592 | nestedTupleSyntaxSugar :: forall a b c d. a /\ b /\ c /\ d 593 | nestedTupleSyntaxSugar = Tuple a (Tuple b (Tuple c (Tuple d))) 594 | ``` 595 | 596 | ```haskell 597 | type Tuple a b = (a,b) 598 | 599 | foo :: (a, b) -> a 600 | foo (a, _) = a 601 | 602 | nestedTuple :: (a,b,c,d) 603 | nestedTuple = (a,b,c,d) 604 | ``` 605 | 606 | ## Unicode Syntax 607 | 608 | PureScript supports the unicode alternatives to ASCII character sequences. Haskell requires the [`UnicodeSyntax` language extension](https://downloads.haskell.org/~ghc/8.6.5/docs/html/users_guide/glasgow_exts.html#extension-UnicodeSyntax) to be enabled for this support to be added. 609 | 610 | ```purescript 611 | -- Not all Unicode alternatives are shown below 612 | join ∷ ∀ m a. Monad m ⇒ m (m a) → m a 613 | join -- implementation 614 | ``` 615 | 616 | ```haskell 617 | {-# LANGUAGE UnicodeSyntax #-} 618 | 619 | -- Not all Unicode alternatives are shown below 620 | join ∷ ∀ m a. Monad m ⇒ m (m a) → m a 621 | join -- implementation 622 | ``` 623 | -------------------------------------------------------------------------------- /02-Syntax/02-Type-Classes.md: -------------------------------------------------------------------------------- 1 | # Type Classes 2 | 3 | ## Defining Type Classes 4 | 5 | ### Single Parameter Type Classes 6 | 7 | #### Basic 8 | 9 | These are the same for both 10 | 11 | ```purescript 12 | class TypeClassName parameterType where 13 | functionName :: parameterType -> ReturnType 14 | 15 | valueName :: ValueType 16 | ``` 17 | 18 | ```haskell 19 | class TypeClassName parameterType where 20 | functionName :: parameterType -> ReturnType 21 | 22 | valueName :: ValueType 23 | ``` 24 | 25 | #### Relationships 26 | 27 | PureScript's arrow faces towards the required class (i.e. `<=`). 28 | Haskell's arrow faces towards the actual class (i.e. `=>`). 29 | 30 | ```purescript 31 | -- | | - the arrow faces towards required class 32 | class RequiredTypeClass a <= ActualTypeClass a where 33 | functionName :: a -> ReturnType 34 | 35 | class (RequiredTypeClass1 a, RequiredTypeClass2 a {-, ... -}) <= TheTypeClass a where 36 | function :: a -> a 37 | ``` 38 | 39 | ```haskell 40 | -- | | - the arrow faces towards actual class 41 | class RequiredTypeClass a => ActualTypeClass a where 42 | functionName :: a -> ReturnType 43 | 44 | class (RequiredTypeClass1 a, RequiredTypeClass2 a {-, ... -}) => TheTypeClass a where 45 | function :: a -> a 46 | ``` 47 | 48 | ### Empty Type Classes 49 | 50 | Haskell requires you to enable the `MultiParamTypeClasses` language extension to define an empty type class. PureScript's definition works out-of-box. 51 | 52 | ```purescript 53 | class EmptyTypeClass 54 | ``` 55 | 56 | ```haskell 57 | {-# LANGUAGE MultiParamTypeClasses #-} 58 | class EmptyTypeClass 59 | ``` 60 | 61 | ### Type Class Members with Constraints 62 | 63 | PureScript can add type class constraints to its members out-of-box. Haskell requires enabling a language extension called `ConstrainedClassMethods`: 64 | 65 | ```purescript 66 | class SomeClass a where 67 | someFunction :: Eq a => a -> a -> a 68 | ``` 69 | 70 | ```haskell 71 | {-# LANGUAGE ConstrainedClassMethods #-} 72 | 73 | class SomeClass a where 74 | someFunction :: Eq a => a -> a -> a 75 | ``` 76 | 77 | ### Other Extensions to Enable by Default 78 | 79 | - `FlexibleInstances` 80 | - `FlexibleContexts` 81 | 82 | ### Multiple Parameters 83 | 84 | #### Without Functional Dependencies 85 | 86 | Haskell requires you to enable the `MultiParamTypeClasses` language extension to define a type class with multiple parameters. PureScript's definition works out-of-box. 87 | 88 | ```purescript 89 | class MultiParameterTypeClass type1 type2 {- typeN -} where 90 | functionName1 :: type1 -> type2 -> {- typeN -> -} ReturnType 91 | ``` 92 | 93 | ```haskell 94 | {-# LANGUAGE MultiParamTypeClasses #-} 95 | class MultiParameterTypeClass type1 type2 {- typeN -} where 96 | functionName1 :: type1 -> type2 -> {- typeN -> -} ReturnType 97 | ``` 98 | 99 | #### With Functional Dependencies 100 | 101 | PureScript's definition works out-of-box. Haskell requires you to enable the `FunctionalDependencies` language extensions to define a type class with functional dependencies between its multiple parameters. In addition, you might need to enable the `UndecidableInstances` language extension when using functional dependencies; otherwise, the compiler might think your instances are invalid. 102 | 103 | Note: enabling Haskell's `FunctionalDependencies` language extension implies enabling the `MultiParamTypeClasses` language extension. 104 | 105 | ```purescript 106 | class ManyTypesDetermineAnotherType a b c | a b {- n -} -> c where 107 | functionName2 :: a b -> c 108 | 109 | class OneTypeDeterminesManyTypes a b c | a -> b c where 110 | functionName3 :: a -> b c 111 | 112 | class ManyFDRelationships a b c | a b -> c, c -> a b where 113 | functionName4 :: a -> b -> c 114 | ``` 115 | 116 | ```haskell 117 | {-# LANGUAGE FunctionalDependencies #-} 118 | -- ^ implies {-# LANGUAGE MultiParamTypeClasses #-} 119 | 120 | class ManyTypesDetermineAnotherType a b c | a b {- n -} -> c where 121 | functionName2 :: a b -> c 122 | 123 | class OneTypeDeterminesManyTypes a b c | a -> b c where 124 | functionName3 :: a -> b c 125 | 126 | class ManyFDRelationships a b c | a b -> c, c -> a b where 127 | functionName4 :: a -> b -> c 128 | ``` 129 | 130 | ### Using Kind Signatures 131 | 132 | PureScript's kind signatures work out-of-box. Haskell requires a language extension called `KindSignatures`. 133 | 134 | ```purescript 135 | class TypeLevelFunction (input :: InputKind) f | input -> f 136 | ``` 137 | 138 | ```haskell 139 | {-# LANGUAGE KindSignatures #-} 140 | 141 | class TypeLevelFunction (input :: InputKind) f | input -> f 142 | ``` 143 | 144 | ## Constraint Kinds 145 | 146 | ### Context 147 | 148 | Let's say we have the following function: 149 | ```purescript 150 | foo 151 | :: forall m. 152 | MonadReader Config m => 153 | MonadState State m => 154 | MonadWriter String m => 155 | MonadCatch Error m => 156 | m Unit 157 | foo = ... 158 | ``` 159 | 160 | If we have to define 6 other functions that use those same type class constraints, it can lead to a lot of boilerplate. "Constraint kinds" provide a solution to this problem. 161 | 162 | ### Comparison 163 | 164 | Unfortunately, PureScript does not yet allow one to define a constraint kind. However, Haskell does if one enables [the `ConstraintKinds` language extension](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-ConstraintKinds) 165 | 166 | ```purescript 167 | {- 168 | This pseudo-code does not really work 169 | 170 | type ConstraintKind m a = 171 | MonadReader Config m => 172 | MonadState State m => 173 | MonadWriter String m => 174 | MonadCatch Error m => 175 | m a 176 | 177 | foo :: ConstraintKind m Unit 178 | foo = ... 179 | 180 | -} 181 | ``` 182 | 183 | ```haskell 184 | {-# LANGUAGE ConstraintKinds #-} 185 | 186 | type ConstraintKind m = 187 | ( MonadReader Config m 188 | , MonadState State m 189 | , MonadWriter String m 190 | , MonadCatch Error m 191 | ) 192 | 193 | foo :: ConstraintKind m => m Unit 194 | foo = ... 195 | ``` 196 | 197 | ## Defining Type Class Instances 198 | 199 | ### Single Parameter Instances 200 | 201 | #### Basic 202 | 203 | PureScript requires you to name your instance. I think this is a constraint imposed by the JavaScript runtime. Haskell does not. 204 | 205 | ```purescript 206 | data DataTypeName = DataTypeName 207 | 208 | class TypeClassName a where 209 | stringMe :: a -> String 210 | 211 | -- Name needed for the instance (JavaScript-imposed constraint) 212 | -- Names follow a convention: "typeClassNameDataTypeName" 213 | instance nameOfInstance :: TypeClassName DataTypeName where 214 | stringMe _ = "DataTypeName" 215 | ``` 216 | 217 | ```haskell 218 | data DataTypeName = DataTypeName 219 | 220 | class TypeClassName a where 221 | stringMe :: a -> String 222 | 223 | -- No name needed for the instance 224 | instance TypeClassName DataTypeName where 225 | stringMe _ = "DataTypeName" 226 | ``` 227 | 228 | #### Including Type Signatures in the Instances 229 | 230 | PureScript allows you to include the type signature of the type class member you are implementing. Haskell only allows this if the `InstanceSigs` extension is enabled. 231 | 232 | ```purescript 233 | data DataTypeName = DataTypeName 234 | 235 | class TypeClassName a where 236 | stringMe :: a -> String 237 | 238 | -- Including the type signature is allowed 239 | instance TypeClassNameDataTypeName :: TypeClassName DataTypeName where 240 | stringMe :: DataType -> String 241 | stringMe _ = "DataTypeName" 242 | ``` 243 | 244 | ```haskell 245 | {-# LANGUAGE InstanceSigs #-} 246 | data DataTypeName = DataTypeName 247 | 248 | class TypeClassName a where 249 | stringMe :: a -> String 250 | 251 | instance TypeClassName DataTypeName where 252 | stringMe :: DataType -> String 253 | stringMe _ = "DataTypeName" 254 | ``` 255 | 256 | #### Instances Requiring Other Instances 257 | 258 | These work the same in both languages 259 | 260 | ```purescript 261 | instance showMaybe :: (Show a) => Show (Maybe a) where 262 | show Nothing = "Nothing" 263 | show (Just a) = "Just(" <> show a <> ")" 264 | ``` 265 | 266 | ```haskell 267 | instance (Show a) => Show (Maybe a) where 268 | show Nothing = "Nothing" 269 | show (Just a) = "Just(" <> show a <> ")" 270 | ``` 271 | 272 | ### Multiple Paramter Instances 273 | 274 | ```purescript 275 | class Multi one two three where 276 | multiFunction :: one -> two -> three -> String 277 | 278 | instance multiIntBooleanChar :: Multi Int Boolean Char where 279 | multiFunction i b c = "done" 280 | ``` 281 | 282 | ```haskell 283 | {-# LANGUAGE MultiParamTypeClasses #-} 284 | class Multi one two three where 285 | multiFunction :: one -> two -> three -> String 286 | 287 | instance Multi Int Boolean Char where 288 | multiFunction i b c = "done" 289 | ``` 290 | 291 | ### Instance Chains 292 | 293 | PureScript allows the usage of "type class instance chains" to help determine which instance should be used. Haskell [does not appear to have this feature implemented](https://gitlab.haskell.org/ghc/ghc/issues/9334), but there are various ways to get around that. I'm not yet sure what those ways are besides those mentioned in the above link. 294 | 295 | Regardless, [the PureScript community strongly opposes allowing orphan instances](https://github.com/purescript/purescript/issues/1247#issuecomment-512975645) as illustrated by Harry's comment. 296 | 297 | ```purescript 298 | class DoSomething a where 299 | doSomething :: a -> String 300 | 301 | -- Readability note: some put `else instance` on the same line. 302 | -- Others put these on separate lines. 303 | -- else 304 | -- instance 305 | -- I show both below. 306 | instance firstTry :: DoSomething Int where 307 | doSomething = "an int" 308 | else instance secondTry :: DoSomething Number where 309 | doSomething = "a number" 310 | else 311 | instance thirdTry :: DoSomething String where 312 | doSomething = "a string" 313 | else 314 | instance catchall :: DoSomething a where 315 | doSomething = "some unknown value" 316 | ``` 317 | 318 | ```haskell 319 | -- TODO: not sure how Haskell does this yet. 320 | ``` 321 | 322 | ## Deriving Type Class Instances 323 | 324 | ### Non-Newtyped Types 325 | 326 | PureScript's compiler can derive instances for the `Eq`, `Ord`, and `Functor` type classes. For other type classes, it uses the `Generic` type class. 327 | 328 | Haskell can derive many type classes in a more consise manner than PureScript. 329 | 330 | #### Eq and Ord 331 | 332 | PureScript and Haskell can derive instances for `Eq` and `Ord`, but Haskell's syntax is more concise. 333 | 334 | ```purescript 335 | data SomeType = SomeType 336 | 337 | derive instance eqSomeType :: Eq SomeType 338 | derive instance ordSomeType :: Ord SomeType 339 | ``` 340 | 341 | ```haskell 342 | data SomeType = SomeType 343 | deriving (Eq, Ord) 344 | ``` 345 | 346 | #### Functor 347 | 348 | PureScript can derive a functor out-of-box as long as the type has the necessary shape. Haskell requires enabling the `DeriveFunctor` language extension. 349 | 350 | ```purescript 351 | data SomeFunctor a = SomeFunctor a 352 | derive instance functorSomeFunctor :: Functor SomeFunctor 353 | ``` 354 | 355 | ```haskell 356 | {-# LANGUAGE DeriveFunctor #-} 357 | data SomeFunctor a = SomeFunctor a 358 | deriving (Functor) 359 | ``` 360 | 361 | #### Other Type Classes 362 | 363 | PureScript uses the `Generic` type class to derive default implementations for the following type classes: 364 | - via `purescript-generics-rep`: 365 | - Show 366 | - Eq 367 | - Ord 368 | - Bounded 369 | - Enum 370 | - Heyting Algebra 371 | - Semigroup 372 | - Monoid 373 | - Semiring 374 | - Ring 375 | - via `purescript-argonaut-generic`: 376 | - EncodeJson 377 | - DecodeJson 378 | 379 | ```purescript 380 | data SomeType = SomeType 381 | 382 | derive instance genericSomeType :: Generic (SomeType a) _ 383 | 384 | instance showSomeType :: Show SomeType where 385 | show x = genericShow x 386 | ``` 387 | 388 | Haskell can derive instances for the following type classes: 389 | - Show 390 | - Eq 391 | - Ord 392 | - Bounded 393 | - Enum 394 | - Read (just FYI. Don't use this type class (source: "Haskell From First Principles" book) 395 | 396 | Note: Haskell's numerical type class hierarchy works differently than PureScript's. We'll cover that later. 397 | 398 | ```haskell 399 | {-# LANGUAGE DeriveFunctor #-} 400 | {-# LANGUAGE DeriveFoldable #-} 401 | {-# LANGUAGE DeriveTraversable #-} 402 | 403 | data SomeFunctor a = SomeFunctor a 404 | deriving ( 405 | Eq, Ord, Show, Bounded, Enum, 406 | -- ^ These don't require any language extension to be enabled... 407 | Functor, 408 | -- (Satisfy Traversable's dependency upon Functor) 409 | Foldable, 410 | -- ^ only if `DeriveFoldable` extension is enabled 411 | Traversable, 412 | -- ^ only if `DeriveTraversable` extension is enabled 413 | ) 414 | ``` 415 | 416 | ### Newtyped Types 417 | 418 | PureScript can derive an instance if the type that was newtyped has an instance for it. Haskell can do the same only if the `GeneralizedNewtypeDeriving` language extension is enabled. In addition, one might need to enable the `DerivingVia` extension to ensure the correct instance was used. 419 | 420 | ```purescript 421 | class SpecialShow a where 422 | specialShow :: a -> String 423 | 424 | instance SpecialShow Int where 425 | specialShow x = show (x * 100) 426 | 427 | newtype Cent = Cent Int 428 | derive newtype instance specialShowCent :: SpecialShow Cent 429 | ``` 430 | 431 | ```haskell 432 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 433 | 434 | class SpecialShow a where 435 | specialShow :: a -> String 436 | 437 | instance SpecialShow Int where 438 | specialShow x = show (x * 100) 439 | 440 | newtype Cent = Cent Int 441 | deriving (SpecialShow) 442 | ``` 443 | 444 | ```haskell 445 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 446 | {-# LANGUAGE DerivingVia #-} 447 | 448 | -- Given these two type classes 449 | class Semigroup a where 450 | append :: a -> a -> a 451 | 452 | class Semigroup a => Monoid m where 453 | mempty :: a 454 | 455 | -- and these two implementations 456 | newtype Sum = Sum Int 457 | newtype Product = Product Int 458 | 459 | instance Semigroup Sum where append = (+) 460 | instance Monoid Sum where mempty = 0 461 | 462 | instance Semigroup Product where append = (*) 463 | instance Monoid Product where mempty = 1 464 | 465 | -- we can specify that the Semigroup instance uses the Sum Int one, 466 | -- not the Product Int one. 467 | newtype Cent = Cent Int 468 | deriving Semigroup via (Sum Int) 469 | ``` 470 | -------------------------------------------------------------------------------- /02-Syntax/03-Functions-etc.md: -------------------------------------------------------------------------------- 1 | # Functions etc. 2 | 3 | ## Functions 4 | 5 | ### Basic 6 | 7 | The languages work the same for defining and using functions 8 | 9 | ```purescript 10 | function :: Int -> Int 11 | function 4 = 4 12 | 13 | lambda :: Int -> Int 14 | lambda = \x -> x + 1 15 | 16 | higherOrderFunction1 :: Int -> (Int -> Int) -> (Int -> Int) 17 | higherOrderFunction1 x f = \y -> (f y) + x 18 | 19 | typeClassConstrainedFunction :: forall f. Foldable f => f Int -> Int 20 | typeClassConstrainedFunction = foldl (+) 0 21 | ``` 22 | 23 | ```haskell 24 | function :: Int -> Int 25 | function 4 = 4 26 | 27 | lambda :: Int -> Int 28 | lambda = \x -> x + 1 29 | 30 | higherOrderFunction1 :: Int -> (Int -> Int) -> (Int -> Int) 31 | higherOrderFunction1 x f = \y -> (f y) + x 32 | 33 | typeClassConstrainedFunction :: Foldable f => f Int -> Int 34 | typeClassConstrainedFunction = foldl (+) 0 35 | ``` 36 | 37 | ### Partial Functions 38 | 39 | PureScript requires a partial function to be annoated via the `Partial` type class. Haskell has no such distinction; to know whether a function is partial, one would have to look at the source code or already know that that function is partial: 40 | 41 | ```purescript 42 | fromJust :: forall a. Partial => Maybe a -> a 43 | fromJust (Just a) = a 44 | ``` 45 | 46 | ```haskell 47 | fromJust :: forall a. Maybe a -> a 48 | fromJust (Just a) = a 49 | fromJust _ = error "This is a partial function. You should have used it better." 50 | ``` 51 | 52 | ## Let / In / Where Statements 53 | 54 | ```purescript 55 | whereUsage :: Int -> Int 56 | whereUsage x = x + y 57 | where y = 4 58 | 59 | letUsage :: Int -> Int 60 | letUsage x = let y = 4 in x + y 61 | 62 | letWhereUsage :: Int -> Int 63 | letWhereUsage x = 64 | let y = 4 65 | in x + y + z 66 | where z = 10 67 | ``` 68 | 69 | ```haskell 70 | whereUsage :: Int -> Int 71 | whereUsage x = x + y 72 | where y = 4 73 | 74 | letUsage :: Int -> Int 75 | letUsage x = let y = 4 in x + y 76 | 77 | letWhereUsage :: Int -> Int 78 | letWhereUsage x = 79 | let y = 4 80 | in x + y + z 81 | where z = 10 82 | ``` 83 | 84 | ### A Note on Let Generalization 85 | 86 | To improve type inference, PureScript does not generalize `let` bindings to be as polymorphic as possible by default. Unfortunately, Haskell does by default. If one enables the `MonoLocalBinds` extension, Haskell will be more conservative, resulting in behavior that is more akin to PureScript's type inference behavior. 87 | 88 | ## Abbreviated Function Bodies 89 | 90 | These are the same in both languages. 91 | 92 | ```purescript 93 | abbreviatedFunction2 :: Int -> String 94 | abbreviatedFunction2 = show {- is the same as... 95 | abbreviatedFunction2 x = show x -} 96 | ``` 97 | 98 | ```haskell 99 | abbreviatedFunction2 :: Int -> String 100 | abbreviatedFunction2 = show {- is the same as... 101 | abbreviatedFunction2 x = show x -} 102 | ``` 103 | 104 | ## Pattern Matching 105 | 106 | ### Basic 107 | 108 | ```purescript 109 | -- Given a data type like this: 110 | data Alphabet 111 | = A 112 | | B 113 | | C 114 | 115 | mkString :: Alphabet -> String 116 | mkString A = "A" 117 | mkString B = "B" 118 | mkString C = "C" 119 | ``` 120 | 121 | ```haskell 122 | -- Given a data type like this: 123 | data Alphabet 124 | = A 125 | | B 126 | | C 127 | 128 | mkString :: Alphabet -> String 129 | mkString A = "A" 130 | mkString B = "B" 131 | mkString C = "C" 132 | ``` 133 | 134 | ### Matching Specific Values and a Catch-All Binding 135 | 136 | ```purescript 137 | literalValue :: Int -> String 138 | literalValue 0 = "0" 139 | literalValue 1 = "1" 140 | literalValue 2 = "2" 141 | literalValue _ = "catch-all" 142 | ``` 143 | 144 | ```haskell 145 | literalValue :: Int -> String 146 | literalValue 0 = "0" 147 | literalValue 1 = "1" 148 | literalValue 2 = "2" 149 | literalValue _ = "catch-all" 150 | ``` 151 | 152 | ### List and Array Pattern Matching 153 | 154 | PureScript's `[]` pattern matching syntax works only on `Array`s. Haskell's works on `List`s, but can be made to work on any `Sequence`-like data structure if one enables the `OverloadedLists` language extension. Moreover, PureScript does not allow the usage of `1 : Nil` in the pattern matching of a list whereas Haskell does. 155 | 156 | ```purescript 157 | array :: Array Int -> String 158 | array [] = "an empty array" 159 | array [0] = "an array with one value that is 0" 160 | array [0, 1] = "an array with two values, 0 and 1" 161 | array [0, 1, a, b] = "an array with four values, starting with 0 and 1 \ 162 | \ and binding the third and fouth to names 'a' and 'b'" 163 | array [-1, _ ] = "an array of two values, '-1' and another value that \ 164 | \ will not be used in the body of this function." 165 | array _ = "catchall for arrays." 166 | 167 | list :: List Int -> String 168 | list Nil = "empty list" 169 | list (Cons 1 Nil) = "List with one value that is 1" {- 170 | list (1 : Nil) = "this would not compile" -} 171 | list (Cons x (Cons y tail)) = "another example" 172 | ``` 173 | 174 | ```haskell 175 | {-# LANGUAGE OverloadedLists #-} 176 | 177 | import Data.Vector 178 | 179 | array :: Vector Int -> String 180 | array [] = "an empty array" 181 | array [0] = "an array with one value that is 0" 182 | array [0, 1] = "an array with two values, 0 and 1" 183 | array [0, 1, a, b] = "an array with four values, starting with 0 and 1 \ 184 | \ and binding the third and fouth to names 'a' and 'b'" 185 | array [-1, _ ] = "an array of two values, '-1' and another value that \ 186 | \ will not be used in the body of this function." 187 | array _ = "catchall for arrays." 188 | 189 | list :: List Int -> String 190 | list [] = "empty list" 191 | list 1 : [] = "List with one value that is 1" 192 | list x : y : tail = "another example" 193 | ``` 194 | 195 | ### Unwrapping Data Constructors 196 | 197 | The languages work the same. 198 | 199 | ```purescript 200 | data A_Type 201 | = AnInt Int 202 | | Outer A_Type -- recursive type! 203 | | Inner Int 204 | 205 | f :: A_Type -> String 206 | f (Inner 0) = "a value of type Inner whose value is 0" 207 | f (Inner int) = "a value of type Inner, binding its value to 'int' \ 208 | \name for usage in function body" 209 | f (Outer (Inner int)) = "a value of type Outer, whose Inner value is bound \ 210 | \to `int` name for usage in function body" 211 | f object@(AnInt 4) = "a value of type AnInt whose value is '4', \ 212 | \binding the entire object to the `object` name for \ 213 | \usage in function body" 214 | f _ = "ignores input and matches everything; \ 215 | \acts as a default / catch all case" 216 | ``` 217 | 218 | ```haskell 219 | data A_Type 220 | = AnInt Int 221 | | Outer A_Type -- recursive type! 222 | | Inner Int 223 | 224 | f :: A_Type -> String 225 | f (Inner 0) = "a value of type Inner whose value is 0" 226 | f (Inner int) = "a value of type Inner, binding its value to 'int' \ 227 | \name for usage in function body" 228 | f (Outer (Inner int)) = "a value of type Outer, whose Inner value is bound \ 229 | \to `int` name for usage in function body" 230 | f object@(AnInt 4) = "a value of type AnInt whose value is '4', \ 231 | \binding the entire object to the `object` name for \ 232 | \usage in function body" 233 | f _ = "ignores input and matches everything; \ 234 | \acts as a default / catch all case" 235 | ``` 236 | 237 | ### Regular Guards and Pattern Guards 238 | 239 | PureScript's can use regular guards and pattern guards. Haskell can use regular guards, but pattern guards must be enabled via a language extension called `PatternGuards`. 240 | 241 | ```purescript 242 | q :: Int -> Int -> String 243 | q x y | x == 3 = "simple guard" 244 | | x == 5, y == 5 = "multiple guards" 245 | | (Just 2) <- tryStuff x = "pattern guard" 246 | 247 | | (Just 2) <- tryStuff x 248 | , y == 4 = "pattern guard and regular guard" 249 | 250 | | (Just a) <- tryStuff x 251 | , (Just b) <- tryStuff y = "last example" 252 | 253 | | otherwise = "catch-all" 254 | ``` 255 | 256 | ```haskell 257 | {-# LANGUAGE PatternGuards #-} 258 | 259 | q :: Int -> Int -> String 260 | q x y | x == 3 = "simple guard" 261 | | x == 5, y == 5 = "multiple guards" 262 | | (Just 2) <- tryStuff x = "pattern guard" -- extension makes this work 263 | 264 | | (Just 2) <- tryStuff x 265 | , y == 4 = "pattern guard and regular guard" 266 | 267 | | (Just a) <- tryStuff x 268 | , (Just b) <- tryStuff y = "last example" 269 | 270 | | otherwise = "catch-all" 271 | ``` 272 | 273 | ## Usage of `forall` 274 | 275 | ### Basic 276 | 277 | PureScript requires an explicit `forall` when using polymorphic types. Haskell does not. 278 | 279 | ```purescript 280 | identity :: forall a. a -> a 281 | ``` 282 | 283 | ```haskell 284 | id :: a -> a 285 | ``` 286 | 287 | ### The Meaning of the Same Type in Multiple Contexts 288 | 289 | ```purescript 290 | doFoo :: forall a. Monoid a => a -> a 291 | doFoo value = value <> anotherValue 292 | where 293 | anotherValue :: a -- this `a` is the SAME as the `a` above 294 | anotherValue = mempty 295 | ``` 296 | 297 | ```haskell 298 | -- notice that an explicit `forall` is missing in this example 299 | doFoo :: Monoid a => a -> a 300 | doFoo value = value <> anotherValue 301 | where 302 | anotherValue :: a -- this `a` is DIFFERENT than the `a` above 303 | anotherValue = mempty 304 | ``` 305 | 306 | ```haskell 307 | {-# LANGUAGE ScopedTypeVariables #-} 308 | 309 | -- notice that an explicit `forall` is required in this example 310 | doFoo :: forall a. Monoid a => a -> a 311 | doFoo value = value <> anotherValue 312 | where 313 | anotherValue :: a -- this `a` is the SAME as the `a` above 314 | anotherValue = mempty 315 | ``` 316 | 317 | ### Nested Foralls (i.e. Rank-N Types) 318 | 319 | Rank-N Types work out-of-box with PureScript. Haskell requires a language extension: [`RankNTypes`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-RankNTypes). 320 | 321 | ```purescript 322 | foo :: forall a. (forall b. b -> a) -> a 323 | ``` 324 | 325 | ```haskell 326 | {-# LANGUAGE RankNTypes #-} 327 | 328 | foo :: forall a. (forall b. b -> a) -> a 329 | ``` 330 | 331 | 332 | ### Using Kind Signatures 333 | 334 | PureScript's kind signatures work out-of-box. Haskell requires a language extension called `KindSignatures`. 335 | 336 | ```purescript 337 | foo :: forall (someKind :: SomeKind) b. Proxy someKind -> b 338 | foo = -- ... 339 | ``` 340 | 341 | ```haskell 342 | {-# LANGUAGE KindSignatures #-} 343 | 344 | foo :: forall (someKind :: SomeKind) b. Proxy someKind -> b 345 | foo = -- ... 346 | ``` 347 | 348 | ## Case Statements 349 | 350 | ### Single Variable 351 | 352 | ```purescript 353 | foo :: String -> String 354 | foo x = case x of 355 | "some value" -> "first match" 356 | _ -> "catch all" 357 | ``` 358 | 359 | ```haskell 360 | foo :: String -> String 361 | foo x = case x of 362 | "some value" -> "first match" 363 | _ -> "catch all" 364 | ``` 365 | 366 | ### Multi Variable 367 | 368 | ```purescript 369 | foo :: String -> String -> String 370 | foo x = case x, y of 371 | "some value", "another value" -> "first match" 372 | _ -> "catch all" 373 | ``` 374 | 375 | ```haskell 376 | -- TODO: This fails to compile. Not sure whether this can be fixed or not... 377 | foo :: String -> String -> String 378 | foo x = case x, y of 379 | "some value", "another value" -> "first match" 380 | _ -> "catch all" 381 | ``` 382 | 383 | ### Syntax Sugar 384 | 385 | PureScript can use `case _ of` syntax to omit the name of the binding. Haskell requires the language extension `LambdaCase` and modifies its syntax slightly. 386 | 387 | ```purescript 388 | foo :: String -> String 389 | foo = case _ of 390 | "some value" -> "first match" 391 | _ -> "catch all" 392 | ``` 393 | 394 | ```haskell 395 | {-# LANGUAGE LambdaCase #-} 396 | 397 | foo :: String -> String 398 | foo = \case 399 | "some value" -> "first match" 400 | _ -> "catch all" 401 | ``` 402 | 403 | ## Passing Constructs as Arguments to Functions 404 | 405 | Constructs that are passed to functions as arguments work out-of-box with PureScript. Haskell requires the `BlockArguments` language extensions. 406 | ```purescript 407 | foo someFunction useArgument = someFunction do 408 | a <- comp1 409 | b <- comp2 \argument -> useArgument argument 410 | comp3 case b of 411 | Foo a -> a 412 | pure a 413 | ``` 414 | 415 | ```haskell 416 | foo someFunction useArgument = someFunction $ do -- notice the $ here 417 | a <- comp1 418 | b <- comp2 (\argument -> useArgument argument) -- and parenthesis here 419 | comp3 $ case b of -- and $ again here 420 | Foo a -> a 421 | pure a 422 | ``` 423 | 424 | ```haskell 425 | {-# LANGUAGE BlockArguments #-} 426 | foo someFunction = someFunction do -- no $ here 427 | a <- comp1 428 | comp2 \argument -> useArgument argument -- no parenthesis here 429 | comp3 case b of -- no $ here 430 | Foo a -> a 431 | pure a 432 | ``` 433 | 434 | ## Annotating a polymorphic type with a more specific type signature 435 | 436 | PureScript does not have syntax for type application whereas Haskell does via the extension, `TypeApplications`. 437 | 438 | ```purescript 439 | class Foldable f where 440 | foldl :: forall a b. (b -> a -> b) -> b -> f a -> f b 441 | 442 | foo :: Int 443 | foo = 444 | let intArrayFold = foldl :: (Int -> Int -> Int) -> Int -> Array Int -> Int 445 | in intArrayFold (+) 0 [1, 2, 3] 446 | ``` 447 | 448 | ```haskell 449 | -- extension not enabled 450 | foo :: Int 451 | foo = 452 | let intArrayFold = foldl :: (Int -> Int -> Int) -> Int -> Array Int -> Int 453 | in intArrayFold (+) 0 [1, 2, 3] 454 | ``` 455 | 456 | ```haskell 457 | {-# LANGUAGE TypeApplications #-} 458 | foo :: Int 459 | foo = 460 | let intArrayFold = foldl @Vector @Int @Int 461 | in intArrayFold (+) 0 [1, 2, 3] 462 | -------------------------------------------------------------------------------- /02-Syntax/05-Record-Syntax.md: -------------------------------------------------------------------------------- 1 | # Record Syntax 2 | 3 | PureScript has very readable Record syntax. Haskell's unfortunately sucks and is widely considered to be a "wart" in the language. 4 | 5 | ## Definition 6 | 7 | ```purescript 8 | type EmptyRow = () 9 | type SingleRow = (a :: String) 10 | type SimpleRecord = Record (a :: String) 11 | type SimpleRecord' = Record SimpleRow 12 | 13 | type BetterRecord = { a :: String } 14 | ``` 15 | 16 | ```haskell 17 | type Unit = () 18 | 19 | data PoorRecord = Record { poorRecordA :: String } 20 | 21 | -- ^ The above is apparently syntax sugar for... 22 | ------------------------------------------------ 23 | data PoorRecord = Record String 24 | 25 | simpleRecordA :: PoorRecord -> String 26 | simpleRecordA (PoorRecord string) = string 27 | ------------------------------------------------ 28 | -- ... which causes namespacing issues. Thus, 29 | -- one typically uses "qualified imports" 30 | -- (i.e. in PureScript, we would write `import Module as M` 31 | -- and use it via `M.functionName`) or do what I did above 32 | -- and prefix the value with the type's name so that it 33 | -- doesn't clash as often. 34 | -- ~ Source: https://stackoverflow.com/a/5520803 35 | ``` 36 | 37 | ## Creating a Record 38 | 39 | ### By Specifying Values 40 | 41 | ```purescript 42 | type BetterRecord = { a :: String } 43 | 44 | createRecord :: String -> BetterRecord 45 | createRecord value = { a: value } 46 | ``` 47 | 48 | ```haskell 49 | data PoorRecord = Record { poorRecordA :: String } 50 | 51 | createRecord :: String -> PoorRecord 52 | createRecord value = Record { a = value } 53 | ``` 54 | 55 | ### By Referring to Values in Context 56 | 57 | ```purescript 58 | type TwoValueRecord = { first :: String, second :: String } 59 | 60 | computation :: forall m. Monad m => m TwoValueRecord 61 | computation = do 62 | first <- getFirst 63 | second <- getSecond 64 | pure { first, second } 65 | ``` 66 | 67 | ```haskell 68 | {-# LANGUAGE RecordWildCards #-} 69 | 70 | data TwoValueRecord = Record { first :: String, second :: String } 71 | 72 | computation :: forall m. Monad m => m TwoValueRecord 73 | computation = do 74 | first <- getFirst 75 | second <- getSecond 76 | pure Record{..} -- beware of naming collisions 77 | ``` 78 | 79 | ## Getting a field 80 | 81 | ### Using Regular Syntax 82 | ```purescript 83 | type BetterRecord = { a :: String } 84 | 85 | getField :: BetterRecord -> String 86 | getField record = record.a 87 | ``` 88 | 89 | ```haskell 90 | data PoorRecord = Record { poorRecordA :: String } 91 | 92 | getField :: PoorRecord -> String 93 | getField record = poorRecordA record 94 | ``` 95 | 96 | ### Using Annotations 97 | 98 | ```purescript 99 | import Record (get) 100 | import Data.Symbol (SProxy(..)) 101 | 102 | getField2 :: forall otherRows. { a :: String | otherRows } -> String 103 | getField2 record = get (SProxy :: SProxy "a") record 104 | ``` 105 | 106 | ```haskell 107 | -- This class is defined in GHC.Records 108 | class HasField (x :: k) r a | x r -> a where 109 | getField :: r -> a 110 | 111 | -- Thus, one can use it like so: 112 | getField2 :: HasField "a" record String => record -> String 113 | getField2 record = getField @"a" record 114 | ``` 115 | 116 | ## Updating a single field 117 | 118 | ```purescript 119 | type AB_Record = { a :: String, b :: String } 120 | 121 | setField :: AB_Record -> String 122 | setField record = record { a = "some new value while B is the same" } 123 | ``` 124 | 125 | ```haskell 126 | data AB_Record = AB_Record { a :: String, b :: String } 127 | 128 | setField :: AB_Record -> String 129 | setField record = record { a = "some new value while B is the same" } 130 | ``` 131 | 132 | ## Pattern Matching 133 | 134 | ### Match on a field and don't rebind it to a new name 135 | 136 | ```purescript 137 | type BetterRecord = { a :: String } 138 | 139 | patternMatch1 :: BetterRecord -> String 140 | patternMatch1 { a } = a 141 | ``` 142 | 143 | ```haskell 144 | {-# LANGUAGE NamedFieldPuns #-} 145 | data PoorRecord = Record { poorRecordA :: String } 146 | 147 | -- Only possible of NamedFieldPuns extension is enabled 148 | patternMatch1 :: PoorRecord -> String 149 | patternMatch1 (Record { a }) = a 150 | ``` 151 | 152 | ### Match on a field and bind it to a new name 153 | 154 | ```purescript 155 | type BetterRecord = { a :: String } 156 | 157 | patternMatch2 :: BetterRecord -> String 158 | patternMatch2 { a: differentName } = differentName 159 | ``` 160 | 161 | ```haskell 162 | data PoorRecord = Record { a :: String } 163 | 164 | patternMatch2 :: PoorRecord -> String 165 | patternMatch2 (Record { a = differentName }) = differentName 166 | ``` 167 | 168 | ### Match on a specific value for a given field 169 | 170 | ```purescript 171 | patternMatch3 :: BetterRecord -> String 172 | patternMatch3 { a: "actual value" } = "i.e. matching against a specific value" 173 | patternMatch3 { a: allOtherValues } = allOtherValues 174 | ``` 175 | 176 | ```haskell 177 | data PoorRecord = Record { a :: String } 178 | 179 | patternMatch3 :: PoorRecord -> String 180 | patternMatch3 (Record { a = "actual value" }) = "i.e. matching against a specific value" 181 | patternMatch3 (Record { a = allOtherValues }) = allOtherValues 182 | ``` 183 | 184 | ### Row Polymorphism and Records 185 | 186 | ```purescript 187 | type OpenRecord extraRows = { a :: String | extraRows } 188 | 189 | polymorphicGet :: forall otherRows. OpenRecord otherRows -> String 190 | polymorphicGet record = record.a 191 | 192 | -- "value" == rowPolymorphicRecord { a: "value" } 193 | -- "value" == rowPolymorphicRecord { a: "value", b: "still works" } 194 | -- "value" == rowPolymorphicRecord { b: "still works", a: "value" } 195 | 196 | polymorphicSet 197 | :: forall otherRows. 198 | String -> 199 | OpenRecord otherRows -> 200 | OpenRecord otherRows 201 | polymorphicSet newValue record = record { a = newValue } 202 | ``` 203 | 204 | ```haskell 205 | -- Not sure whether Haskell can define an open Record type 206 | ``` 207 | -------------------------------------------------------------------------------- /02-Syntax/06-Control-Flow.md: -------------------------------------------------------------------------------- 1 | # Control Flow 2 | 3 | ## If Then Else Statements 4 | 5 | ### Basic 6 | 7 | ```purescript 8 | foo :: String -> String 9 | foo x = 10 | if x == "a" then "foo" 11 | else if x == "b" then "bar" 12 | else "baz" 13 | ``` 14 | 15 | ```haskell 16 | foo :: String -> String 17 | foo x y = 18 | if x == "a" then "foo" 19 | else if x == "b" and y == "c" then "bar" 20 | else "baz" 21 | ``` 22 | 23 | ### Cleaner Syntax 24 | 25 | The same example above can be rewritten via this Haskell-only syntax. 26 | 27 | ```haskell 28 | {-# LANGUAGE MultiWayIf #-} 29 | 30 | foo2 :: String -> String -> string 31 | foo2 x y = 32 | if | x == "a" -> "foo" 33 | | x == "b" and y == "c" -> "bar" 34 | | otherwise -> "baz" 35 | ``` 36 | 37 | ## Applicative Do Notation 38 | 39 | Applicative do works out-of-box with PureScript. Haskell requires the `ApplicativeDo` language extension. 40 | 41 | ```purescript 42 | fooDesugared :: Maybe Int 43 | fooDesugared = \a b _ -> a + b 44 | <$> comp1 45 | <*> comp2 46 | <*> comp3 47 | 48 | fooSugared :: Maybe Int 49 | fooSugared = ado 50 | a <- comp1 51 | b <- comp2 52 | comp3 53 | in a + b 54 | ``` 55 | 56 | ```purescript 57 | {-# LANGUAGE ApplicativeDo #-} 58 | 59 | fooSugared :: Maybe Int 60 | fooSugared = ado 61 | a <- comp1 62 | b <- comp2 63 | comp3 64 | in a + b 65 | ``` 66 | 67 | ## Rebindable Do and Ado Notation 68 | 69 | PureScript can rebind do and ado notation out-of-box using two methods. Haskell can rebind its syntax when the `RebindaleSyntax` extension is enabled. It can rebind `do` notation, but different functions have to be rebound than PureScript's corresponding version. I don't think Haskell can rebind `ado` notation. Lastly, Haskell can rebind more of its syntax than PureScript can. 70 | 71 | ```purescript 72 | foo = 73 | -- local rebinding 74 | let 75 | bind = {- impl -} 76 | discard = {- impl -} 77 | in do 78 | a <- comp1 79 | comp2 80 | pure a 81 | 82 | foo2 = MyModule.do -- qualified rebinding 83 | a <- comp1 84 | comp2 85 | pure a 86 | ``` 87 | 88 | ```haskell 89 | foo = 90 | -- local rebinding 91 | let 92 | >>= = {- impl -} 93 | pure = {- impl -} 94 | in do 95 | a <- comp1 96 | comp2 97 | pure a 98 | ``` 99 | 100 | ## Monads that Indicate Native Side Effects 101 | 102 | PureScript uses `Effect` for synchronous effects due to the JavaScript backend constraint. To simulate asychronous effects, they use `Aff`. `Aff` on a JavaScript backend allows "concurrent but not parallel" programming. In other words, only one Aff "Fiber" can be running at a given time. 103 | Haskell uses `IO`, which works very similarly to `Aff`. However, Haskell's `IO` enables "concurrent and parallel" programming. In other words, multiple threads can be executing a Haskell program at a given time. 104 | 105 | ```purescript 106 | main :: Effect Unit 107 | main = launchAff_ do 108 | liftEffect $ log "some message" 109 | ``` 110 | 111 | ```haskell 112 | main :: IO () 113 | main = putStrLn "some message" 114 | ``` 115 | -------------------------------------------------------------------------------- /02-Syntax/07-Lazy-by-Default.md: -------------------------------------------------------------------------------- 1 | # Lazy by Default 2 | 3 | ## Converting "strict by default" code to "lazy by default" code 4 | 5 | PureScript is strict by default (a constraint imposed by the JavaScript backend). Thus, when we define data types and functions like below... 6 | ```purescript 7 | data Foo = Foo Int String 8 | 9 | type Stuff = 10 | { myInt :: Int 11 | , myBool :: Boolean 12 | } 13 | 14 | fooToStuff :: Foo -> Stuff 15 | fooToStuff (Foo i s) = { myInt: i, myBool: s == "meh" } 16 | 17 | stuffRender :: Stuff -> String 18 | stuffRender record = (show record.myInt) <> " , " <> (show record.myBool) 19 | 20 | main :: Effect Unit 21 | main = log (render (fooToStuff (Foo 4 "no"))) 22 | ``` 23 | ... we evaluate `Foo 4 "no"` right away to produce a `Foo`, then pass it into `fooToStuff`, which converts it to `Stuff` immediately, and `show` converts it into a `String` and `log` prints that value to the console. 24 | 25 | Depending on your perspective, Haskell is for better or worse lazy by default. If we rewrote the above code as Haskell and did NOT make it strict, this is what we would write in Haskell. 26 | 27 | ```haskell 28 | data Foo = Foo Int String 29 | 30 | data Stuff = Stuff 31 | { stuffMyInt :: Int 32 | , stuffMyBool :: Boolean 33 | } 34 | 35 | fooToStuff :: Foo -> Stuff 36 | fooToStuff (Foo i s) = Stuff { stuffMyInt = i, stuffMyBool = s == "meh" } 37 | 38 | stuffRender :: Stuff -> String 39 | stuffRender (Stuff myInt myBool) = (show myInt) <> " , " <> (show myBool) 40 | 41 | main :: IO () 42 | main = putStrLn (render (fooToStuff (Foo 4 "no"))) 43 | ``` 44 | 45 | If we ran the above Haskell code, this is what it would look like if we were running PureScript instead. In short, all values are wrapped in a thunk/closure and only the final `Effect` causes these thunks/closures to be run: 46 | ```purescript 47 | -- A closure, a thunk, or whatever you want to call it 48 | type Lazy a = Unit -> a 49 | 50 | force :: Lazy a -> a 51 | force thunk = thunk unit 52 | 53 | -- Notice that everything is lazy now... 54 | data Foo = Foo (Lazy Int) (Lazy String) 55 | 56 | type Stuff = 57 | { myInt :: Lazy Int 58 | , myBool :: Lazy Boolean 59 | } 60 | 61 | main :: Effect Unit 62 | main = 63 | let 64 | -- First it builds up a ton of thunks/closures 65 | lazyInt = \_ -> 4 66 | lazyString = \_ -> "no" 67 | 68 | -- PS' Foo data constructor is strict, 69 | -- so I use this syntax to show that it's being constructed lazily 70 | _Foo = \_ -> Foo lazyInt lazyString 71 | 72 | fooToStuff = \_ -> case force _Foo of 73 | Foo lazyInt lazyString -> { myInt: lazyInt 74 | , myBool: \_ -> (force lazyString) == "meh" 75 | } 76 | stuffRender = \_ -> 77 | let 78 | record = force fooToStuff 79 | realInt = force record.myInt 80 | realBool = force record.myBool 81 | in 82 | -- Technically, `show` here would be another thunk that gets forced 83 | -- or evaluated (e.g. `force (show realInt)`), but it was easier 84 | -- to provide this example if it operated in a strict manner. 85 | (show realInt) <> " , " <> (show realBool) 86 | 87 | log = \_ -> force stuffRender 88 | 89 | in 90 | -- Now that all thunks have been created, it finally forces the thunk / 91 | -- evaluates the closure. Each in turn forces the 92 | -- next thunk / evaluates the next closure until 93 | -- the program finshes executing. 94 | force log 95 | ``` 96 | 97 | Obviously, the above program would not be as performant as it could be. Knowing when, where, and how to make Haskell strict is often a challenge new learners face. 98 | 99 | ## Haskell's Laziness 100 | 101 | ### Pros and Cons of Laziness 102 | 103 | In a blog post to which I will link later in this file, Michael Snoyman says that "laziness is sometimes an asset, and sometimes a liability." 104 | 105 | For a summary of the benefits of laziness, see [this StackOverflow answer](https://stackoverflow.com/a/265548/4846512). 106 | 107 | The two main issues that can arise with laziness are: 108 | 1. space leaks - [A more formal definition](https://stackoverflow.com/a/46007799/4846512) 109 | 2. unpredictable runtime behavior/performance 110 | 111 | ### Thunks vs Weak Head Normal Form vs Normal Form 112 | 113 | Values in Haskell are in one of three forms: 114 | - unevaluated thunks 115 | - Weak Head Normal Form (WHNF) 116 | - Normal Form (NF) 117 | 118 | There is another concept known as Head Normal Form (HNF), but Haskell does not use this. 119 | 120 | The following StackOverflow answer provides [a visualization describing the difference between thunks, Weak Head Normal Form (WHNF), and Normal Form (NF)](https://stackoverflow.com/a/9342882). 121 | 122 | ### Avoiding the Negative Aspects of Laziness 123 | 124 | Read [All about Strictness](https://www.fpcomplete.com/blog/2017/09/all-about-strictness) to understand how Haskell's laziness works and how to avoid the negative side of it. 125 | 126 | The rest of this section summarizes his points and links to other things that were mentioned in the above post or its Reddit thread: 127 | - 4 key functions (the last one was mentioned in the Reddit thread and Snoyman didn't include it in his post) 128 | - `seq first second` means that after evaluating `seq` both `first` and `second` will have been evaluated to Weak-Head Normal Form. However, GHC is free to evaluate these in whichever order it chooses. If one's code is pure, one will not notice a difference. If one uses impure code (e.g. `trace`), then one can observe the difference. See [the documentation on `seq`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#v:seq) 129 | - `deepseq first second` works the same as `sea` except that both `first` and `second` will have been evaluated to Normal Form rather than Weak Head Normal Form. A runtime exception will occur if `first`, `second`, or any of their contained thunks will produce a runtime exception when evaluated. See [the documentation on `deepseq`](https://hackage.haskell.org/package/deepseq-1.4.4.0/docs/Control-DeepSeq.html#v:deepseq) 130 | - `force (some expression)` means "when `some expression` is evaluated to Weak-Head Normal Form, evaluate it to Normal Form instead. See [the documentation on `force`](https://hackage.haskell.org/package/deepseq-1.4.4.0/docs/Control-DeepSeq.html#v:force) 131 | - `pseq first second` works the same as `seq` but guarantees that `first` is evaluated before `second` because its first argument is strict and its second argument is lazy. See [the documentation on `pseq`](https://hackage.haskell.org/package/parallel-3.2.2.0/docs/Control-Parallel.html#v:pseq). 132 | - [`BangPatterns`] are a language extension that makes `!` act as syntax sugar for `seq`. However, some expressions cannot be expressed using that. So, one should still famliarize themselves with `seq`. 133 | - Usage of BangPatterns (i.e. `!`) and what they mean in various contexts: 134 | - In a pattern match (e.g. `foo (Container !value) = --`) means "Evaluate this value to WHNF when a pattern match occurs". 135 | - In a let binding (e.g. `let !value = --`) means "Evaluate this value to WHNF before its used anywhere else." 136 | - In a data declaration (e.g. `data Foo = Foo !Int`) means "Whenever you evaluate a value of type `Foo` to WHNF (e.g. pattern match on it), you must also evaluate the strict-annotated fields it contains to WHNF" (edited but I'm basically quoting Snoyman here). 137 | - > You're probably asking a pretty good question right now: "how do I know if I should use a strictness annotation on my data fields?" This answer is slightly controversial, but my advice and recommended best practice: **unless you know that you want laziness for a field, make it strict.** 138 | - Given `newtype Foo = Foo Int`, calling `case Foo undefined of {Foo _ -> ...}` is the same as calling `case undefined of {_ -> ...}` because `Foo` only exists at compile-time, not runtime. `undefined` is never evaluated/observed due to using `_` so the program doesn't crash. 139 | - The `$` functions: 140 | - `f $ a` is the same as `f a` 141 | - `f $! a` is the same as [`f weakHeadNormalFormA`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#v:-36--33-) 142 | - `f $!! a` is the same as [`f normalFormA`](https://hackage.haskell.org/package/deepseq-1.4.4.0/docs/Control-DeepSeq.html#v:-36--33--33-) 143 | - Strictness of data structures 144 | - "spine strict" = the "structure" of the data type can be evaluated without evaluating any of its values. I think this tends to occur with recursive data types like `List`. 145 | - "value strict" = the values in a data type are strict. If any of them contain an `undefined`, they will produce a runtime error when evaluated. 146 | - Lazy fold left (i.e. `foldl`) vs Strict fold left (i.e. `foldl'`) 147 | - It tends to be safer to use `foldl'` rather than `foldl`. The former is the strict version whereas the latter is the lazy version. 148 | - [You cannot have any control over strictness with `foldl` but you can have complete control over strictness with `foldl'`](https://www.reddit.com/r/haskell/comments/6zl88c/all_about_strictness/dmwd8z2/) 149 | - "You can build up as many chains of evaluation using `seq` and `deepseq` as you want in your program. But ultimately, unless you force evaluation via some `IO` action of the value at the top of the chain, it will all remain an unevaluated thunk." 150 | 151 | ## A Guide to Fixing Space Leaks 152 | 153 | Finding and fixing space leaks seems to require using profiler tools. See [Leaking Space](https://queue.acm.org/detail.cfm?id=2538488) to understand the concept better and tips for fixing them. 154 | -------------------------------------------------------------------------------- /02-Syntax/08-Modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | 3 | ## Location of Pragmas 4 | 5 | Not all Pragmas go here (e.g. INLINE, etc.), but this is where language extensions and GHC options go if they are enabled for the given file or override default values for the given file. 6 | 7 | ```Haskell 8 | {-# LANGUAGE LambdaCase #-} 9 | {-# OPTIONS_GHC -Wno-name-shadowing #-} 10 | module ModuleName 11 | ( exportedFunction 12 | , exportedValue 13 | ) where 14 | 15 | import ModuleName 16 | 17 | exportedFunction :: Show a => a -> String 18 | exportedFunction = show 19 | 20 | exportedValue :: Int 21 | exportedValue = 4 22 | ``` 23 | 24 | ## Importing 25 | 26 | Since PureScript does not allow Orphan Instances, type class instances are imported if they are used in the module. Haskell requires importing the module where the instance is defined. * 27 | 28 | | PureScript Idea | PureScript Syntax | Haskell Syntax | Haskell Explanation 29 | | - | - | - | - | 30 | | import all exported entities from a module | `import Foo` | `import Foo` | Same as PureScript, except one can also use `Foo.function` to refer to `function`. | 31 | | import only specific exported entities from a module | `import Foo(bar)` | `import Foo(bar)` | Same as PureScript, except one can also use `Foo.function` to refer to `function`. | 32 | | import all exported entities from a module, but references to them MUST begin with `F` (e.g. `F.function`) | `import Foo as F` | `import qualified Foo as F` | Same as PureScript | 33 | | -- | -- | `import Foo as M` | import all exported entities from a module. One can refer to a function via `function` or via `M.function`. (Useful when only some names clash between two modules) | 34 | | -- | -- | `import Foo as M (bar)` | import only specific entities from a module. One can refer to a function via `bar` or via `M.bar`. (Useful when only some names clash between two modules) | 35 | | -- | -- | `import Foo ()` | * import nothing except type class instances from a module. | 36 | 37 | See Haskell's table that summarizes its imports in the [HaskellWiki's `Import` page](https://wiki.haskell.org/Import). 38 | 39 | ### Normal Imports 40 | 41 | ```purescript 42 | -- import values from a module 43 | import ModuleValues (value1, value2) 44 | 45 | -- imports functions from a module 46 | import ModuleFunctions (function1, function2) 47 | 48 | -- imports function alias from a module 49 | import ModuleFunctionAliases ((/=), (===), (>>**>>)) 50 | 51 | -- imports type class from the module 52 | import ModuleTypeClass (class TypeClass) 53 | 54 | -- import a type but none of its constructors 55 | import ModuleDataType (DataType) 56 | 57 | -- import a type and one of its constructors 58 | import ModuleDataType (DataType(Constructor1)) 59 | 60 | -- import a type and some of its constructors 61 | import ModuleDataType (DataType(Constructor1, Constructor2)) 62 | 63 | -- import a type and all of its constructors 64 | import ModuleDataType (DataType(..)) 65 | 66 | -- import a type alias 67 | import ModuleTypeAlias (type MyTypeAlias) 68 | 69 | -- import a kind and its value 70 | import ModuleKind (kind ImportedKind, ImportedKindValue) 71 | 72 | -- import a type class instance 73 | -- nothing needs to be done here. 74 | ``` 75 | 76 | ```haskell 77 | -- import values from a module 78 | import ModuleValues (value1, value2) 79 | 80 | -- imports functions from a module 81 | import ModuleFunctions (function1, function2) 82 | 83 | -- imports function alias from a module 84 | import ModuleFunctionAliases ((/=), (===), (>>**>>)) 85 | 86 | -- imports type class from the module 87 | import ModuleTypeClass (TypeClass) 88 | 89 | -- import a type but none of its constructors 90 | import ModuleDataType (DataType) 91 | 92 | -- import a type and one of its constructors 93 | import ModuleDataType (DataType(Constructor1)) 94 | 95 | -- import a type and some of its constructors 96 | import ModuleDataType (DataType(Constructor1, Constructor2)) 97 | 98 | -- import a type and all of its constructors 99 | import ModuleDataType (DataType(..)) 100 | 101 | -- import a kind and its value 102 | import ModuleKind (ImportedKind, ImportedKindValue) 103 | 104 | -- import nothing but type class instances 105 | import ModuleWithInstances () 106 | ``` 107 | 108 | ### Dealing with Entity Name Clashes 109 | 110 | #### Basic 111 | 112 | ```purescript 113 | -- resolve name conflicts using "hiding" keyword 114 | import ModuleNameClash1 (sameFunctionName1) 115 | import ModuleNameClash2 hiding (sameFunctionName1) 116 | 117 | -- resolve name conflicts using module aliases 118 | import ModuleNameClash1 as M1 119 | import ModuleNameClash2 as M2 120 | ``` 121 | 122 | ```haskell 123 | import ModuleNameClash1 (sameFunctionName1) 124 | import ModuleNameClash2 hiding (sameFunctionName1) 125 | 126 | -- resolve name conflicts using module aliases 127 | import qualified ModuleNameClash1 as M1 128 | import qualified ModuleNameClash2 as M2 129 | ``` 130 | 131 | #### Package Imports 132 | 133 | Haskell also allows one to use the [`PackageImports`](https://limperg.de/ghc-extensions/#packageimports) language extension to import modules with the same name. 134 | 135 | ### Importing Type Aliases that Use Symbolic Names 136 | 137 | PureScript's works out-of-box whereas Haskell must enable the [`ExplicitNamespaces` extension](https://limperg.de/ghc-extensions/#explicitnamespaces) 138 | 139 | ```purescript 140 | {- 141 | -- i.e. in another file... 142 | module ModuleTypeAlias (type (~>)) where 143 | 144 | type NaturalTransformation f g = forall a. f a -> f g 145 | infixr 4 type NaturalTransformation as ~> 146 | -} 147 | 148 | -- import a type alias that uses infix notation 149 | import ModuleTypeAlias (type (~>)) 150 | ``` 151 | 152 | ```haskell 153 | {-# LANGUAGE ExplicitNamespaces #-} 154 | import ModuleTypeAlias (type (~>)) 155 | ``` 156 | 157 | ## Exporting 158 | 159 | Haskell syntax below has not yet been tested. 160 | 161 | ```purescript 162 | module ExampleExports 163 | -- exports go here by just writing the name 164 | ( value 165 | 166 | , function, (>@>>>) -- aliases must be wrapped in parenthesis 167 | 168 | -- when exporting type classes, there are two rules: 169 | -- - you must precede the type class name with the keyword 'class' 170 | -- - you must also export the type class' members (or face compilation errors) 171 | , class TypeClass, tcFunction 172 | 173 | -- when exporting modules, you must precede the module name with 174 | -- the keyword 'module' 175 | , module ExportedModule 176 | 177 | -- The type is exported, but no one can create an value of it 178 | -- outside of this module 179 | , ExportDataType1_ButNotItsConstructors 180 | 181 | -- The type is exported and only one of its constructors is exported. Thus, 182 | -- everyone else can create a `Constructor2A' value but not a 183 | -- `Constructor2B` value. That one can only be created inside this module. 184 | , ExportDataType2_AndOneOfItsConstructors(Constructor2A) 185 | 186 | -- The type is exported and some of its constructors are exported. Thus, 187 | -- everyone else can create a `Constructor3A' value 188 | -- and a `Constructor3B` value, but not a `Constructor3C` value, which 189 | -- can only be created inside this module. 190 | , ExportDataType3_AndSomeOfItsConstructors(Constructor3A, Constructor3B) 191 | 192 | , ExportDataType4_AndAllOfItsConstructors(..) -- syntax sugar for 'all constructors' 193 | 194 | -- Type aliases can also be exported 195 | , ExportedTypeAlias 196 | 197 | -- When type aliases are aliased using infix notation, one must export 198 | -- both the type alias, and the infix notation where 'type' must precede 199 | -- the infix notation 200 | , ExportedTypeAlias_InfixNotation, type (<|<>|>) 201 | 202 | -- Data constructor alias; exporting the alias requires you 203 | -- to also export the constructor it aliases 204 | , ExportedDataType4_InfixNotation(Infix_Constructor), (<||||>) 205 | 206 | , module Exports 207 | 208 | -- Kinds require the `kind` keyword to precede them 209 | , kind ExportedKind 210 | , ExportedKindValue 211 | ) where 212 | 213 | -- Re-export modules 214 | import Module1 (anInt1) as Exports 215 | import Module2 (anInt2) as Exports 216 | import Module3 (anInt3) as Exports 217 | import Module4.SubModule1 (someFunction) as Exports 218 | 219 | import ExportedModule 220 | ``` 221 | 222 | ```haskell 223 | module ExampleExports 224 | -- exports go here by just writing the name 225 | ( value 226 | 227 | , function, (>@>>>) -- aliases must be wrapped in parenthesis 228 | 229 | -- when exporting type classes, there are two rules: 230 | -- - you must precede the type class name with the keyword 'class' 231 | -- - you must also export the type class' members (or face compilation errors) 232 | , class TypeClass, tcFunction 233 | 234 | -- when exporting modules, you must precede the module name with 235 | -- the keyword 'module' 236 | , module ExportedModule 237 | 238 | -- The type is exported, but no one can create an value of it 239 | -- outside of this module 240 | , ExportDataType1_ButNotItsConstructors 241 | 242 | -- The type is exported and only one of its constructors is exported. Thus, 243 | -- everyone else can create a `Constructor2A' value but not a 244 | -- `Constructor2B` value. That one can only be created inside this module. 245 | , ExportDataType2_AndOneOfItsConstructors(Constructor2A) 246 | 247 | -- The type is exported and some of its constructors are exported. Thus, 248 | -- everyone else can create a `Constructor3A' value 249 | -- and a `Constructor3B` value, but not a `Constructor3C` value, which 250 | -- can only be created inside this module. 251 | , ExportDataType3_AndSomeOfItsConstructors(Constructor3A, Constructor3B) 252 | 253 | , ExportDataType4_AndAllOfItsConstructors(..) -- syntax sugar for 'all constructors' 254 | 255 | -- Type aliases can also be exported 256 | , ExportedTypeAlias 257 | 258 | -- When type aliases are aliased using infix notation, one must export 259 | -- both the type alias, and the infix notation where 'type' must precede 260 | -- the infix notation 261 | , ExportedTypeAlias_InfixNotation, type (<|<>|>) 262 | 263 | -- Data constructor alias; exporting the alias requires you 264 | -- to also export the constructor it aliases 265 | , ExportedDataType4_InfixNotation(Infix_Constructor), (<||||>) 266 | 267 | , module Exports 268 | 269 | -- I think kinds get exported like `data` exports 270 | , ExportedKind 271 | , ExportedKindValue 272 | ) where 273 | 274 | -- Re-export modules 275 | import Module1 (anInt1) as Exports 276 | import Module2 (anInt2) as Exports 277 | import Module3 (anInt3) as Exports 278 | import Module4.SubModule1 (someFunction) as Exports 279 | 280 | import ExportedModule 281 | ``` 282 | -------------------------------------------------------------------------------- /02-Syntax/09-Documentation.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | PureScript documentation gets converted to Markdown. It does not support links to other modules, functions, values, type classes, or their instances. (While this can be 'hacked in' by inserting a link that will "just so happen" to get rendered as a link to the corresponding entity, it's a hack.) 4 | 5 | Haskell's documentation is a lot more powerful. See [Haddock User Guide](https://www.haskell.org/haddock/doc/html/index.html) for full explanation. 6 | 7 | ## Basic Concepts 8 | 9 | ```purescript 10 | -- Only documentation version 11 | 12 | -- | This is documentation 13 | -- | and every line must include that vertical bar (i.e. '|') 14 | -- | 15 | -- | This is a new paragraph. 16 | -- This is a comment, not documentation. 17 | ``` 18 | 19 | ```haskell 20 | -- First documentation version 21 | 22 | -- | This is documentation 23 | -- and every comment that follows is considered part of the documentation 24 | -- Once a non-comment line is reached, the documentation stops. 25 | -- 26 | -- This begins a new paragraph 27 | -- This is documentation, not a comment. 28 | 29 | -- Second documentation version 30 | {-|A multi-line 31 | documentation 32 | that is sometimes easier to write 33 | -} 34 | 35 | -- Third version 36 | 37 | entity -- ^ Documentation for entity 38 | 39 | entity2 40 | -- ^ Documentation for entity2 41 | ``` 42 | 43 | ## Values 44 | 45 | ```purescript 46 | -- | Documentation on a value 47 | value :: Int 48 | value = 4 49 | ``` 50 | 51 | ```haskell 52 | -- | Documentation on a value 53 | value :: Int 54 | value = 4 55 | 56 | value2 :: Int 57 | -- ^ This is also documentation. 58 | value2 = 4 59 | 60 | {-| This is a multi-line 61 | documentation 62 | -} 63 | value3 :: Int 64 | value3 = 4 65 | ``` 66 | 67 | ## Functions 68 | 69 | ```purescript 70 | -- | Documentation on a function 71 | function :: Int -> String 72 | function _ = "easy" 73 | ``` 74 | 75 | ```haskell 76 | -- | Documentation on a function2 77 | function2 78 | :: Int -- ^ Doc on Int argument 79 | -> String -- ^ Doc on String argument 80 | function2 _ = "easy" 81 | 82 | function3 83 | :: forall a. 84 | a -> 85 | -- ^ Doc on `a` argument 86 | String 87 | -- ^ Doc on String argument 88 | function3 _ = "easy" 89 | ``` 90 | 91 | ## Data Types 92 | 93 | ```purescript 94 | -- | Documentation on a given data type 95 | data SomeData 96 | -- | Documentation on a particular data constructor 97 | = SomeData 98 | 99 | -- | Documentation on a given type alias 100 | type MyType = String 101 | 102 | -- | Documentation on a given newtype 103 | newtype SmallInt = SmallInt Int 104 | ``` 105 | 106 | ```haskell 107 | -- | Documentation on a given data type 108 | data SomeData 109 | -- | Documentation on a particular data constructor 110 | = SomeData 111 | | OtherData -- ^ Also documentation for constructor 112 | 113 | -- | Documentation on a given type alias 114 | type MyType = String 115 | 116 | -- | Documentation on a given newtype 117 | newtype SmallInt = SmallInt Int 118 | ``` 119 | 120 | ## Records 121 | 122 | 123 | ```purescript 124 | -- | Documentation for a record 125 | type Record = { foo :: String, bar :: Baz } 126 | ``` 127 | 128 | ```haskell 129 | -- | Documentation on a given data type 130 | data Record = 131 | Record { -- | Documentation on `foo` field 132 | recordFoo :: String 133 | , recordBar :: Baz -- ^ Documentation on `bar` field 134 | } 135 | -- | Documentation on a given type alias 136 | type MyType = String 137 | 138 | -- | Documentation on a given newtype 139 | newtype SmallInt = SmallInt Int 140 | ``` 141 | 142 | ## Type Classes Definitions 143 | 144 | ```purescript 145 | -- | Documentation on a given type class 146 | class MyClass a b | a -> b where 147 | -- | Documentation for a particular function/value defined in a type class 148 | myFunction :: a -> b 149 | ``` 150 | 151 | ```haskell 152 | -- | Documentation on a given type class 153 | class MyClass a b | a -> b where 154 | -- | Documentation for a particular function/value defined in a type class 155 | myFunction :: a -> b 156 | 157 | otherValue :: a -- ^ This value's documentation 158 | ``` 159 | 160 | ## Type Class Instances 161 | 162 | ```purescript 163 | -- | Documentation for a particular instance of a type class 164 | instance example :: MyClass String Int where 165 | myFunction _ = 4 166 | ``` 167 | 168 | ```haskell 169 | -- | Documentation for a particular instance of a type class 170 | instance MyClass String Int where 171 | myFunction _ = 4 172 | ``` 173 | 174 | ## Modules 175 | 176 | ```purescript 177 | -- | Docs for module 178 | module MyModule 179 | ( foo 180 | , bar 181 | , baz 182 | ) where 183 | ``` 184 | 185 | ```haskell 186 | -- | Docs for module 187 | module MyModule 188 | ( foo -- * Header1 189 | , bar -- ** Header2 190 | , baz -- *** Header3 191 | 192 | , comesAfterBaz 193 | ) where 194 | ``` 195 | -------------------------------------------------------------------------------- /02-Syntax/10-Special-Compiler-Features.md: -------------------------------------------------------------------------------- 1 | # Special Compiler Features 2 | 3 | ## Typed Holes 4 | 5 | ```purescript 6 | foo :: String 7 | foo = ?help 8 | ``` 9 | 10 | ```haskell 11 | foo :: String 12 | foo = _ 13 | 14 | foo2 :: String 15 | foo2 = _help -- same thing 16 | ``` 17 | 18 | ## Typed Wildcards 19 | 20 | ### Fail and Print Inferred Type 21 | 22 | These will produce a compiler **error** and print the inferred type. 23 | ```purescript 24 | foo :: ?Help 25 | foo = "a string" 26 | ``` 27 | 28 | ```haskell 29 | foo :: _ 30 | foo = "a string" 31 | ``` 32 | 33 | ### Warn and Use Inferred Type 34 | 35 | These will produce compiler **warning** and print and use the inferred type 36 | 37 | ```purescript 38 | bar :: _ 39 | bar = "a string" 40 | ``` 41 | 42 | ```haskell 43 | {-# LANGUAGE PartialTypeSignatures #-} 44 | 45 | bar :: _ 46 | bar = "a string" 47 | 48 | baz :: _someType -- also works 49 | baz = "a string" 50 | ``` 51 | -------------------------------------------------------------------------------- /02-Syntax/11-Haskell-Only.md: -------------------------------------------------------------------------------- 1 | # Haskell Only 2 | 3 | This file covers things that only exist in Haskell and have no correspondent in PureScript. 4 | 5 | ## View Patterns 6 | 7 | To make code less verbose, the [ViewPatterns](https://limperg.de/ghc-extensions/#viewpatterns) language extension allows you to pattern match using the result of a function applied to the argument. 8 | 9 | If this type of pattern match will be used throughout your project via an import, you should also enable the [PatternSynonyms](https://limperg.de/ghc-extensions/#patternsynonyms) language extension, which makes view patterns a bit more readable. 10 | 11 | ## PolyKinds 12 | 13 | In PureScript, we need to use one `Proxy` type for each type-level value (e.g. `SProxy`, `BProxy`, `OProxy`, `RProxy`, etc.). Haskell has [`PolyKinds`](https://limperg.de/ghc-extensions/#polykinds) (i.e. polymorphic kinds), which allows developers to use one `Proxy` type that can be used to store various type-level values. PureScript will likely get PolyKinds sometime in the future. 14 | 15 | ## GADTs 16 | 17 | Read through the [Haskell wikibook on GADTs](https://en.wikibooks.org/wiki/Haskell/GADT#Summary) to understand how they work and what they enable. In short, they allow you to use phantom types to encode additional information at the type-level. As a result, some runtime errors can be checked at compile time. 18 | 19 | Read through [`GADTSyntax`](https://limperg.de/ghc-extensions/#gadtsyntax) to understnd how the syntax works and then [`GADTs`](https://limperg.de/ghc-extensions/#gadts) to understand how they can be modified. 20 | 21 | ## Type Families 22 | 23 | In PureScript, we use multi-parameter type classes and functional dependencies to do type-level programming in a way similar to the Prolog programming language. [`TypeFamilies`](https://limperg.de/ghc-extensions/#typefamilies) and [`TypeFamilyDependencies`](https://limperg.de/ghc-extensions/#typefamilydependencies) provide type-level programming but with a bit more readability than the Prolog way. 24 | 25 | AFAIK, type families are just a different way to do type-level programming; I believe that what one can do with type families can also be done with multi-parameter type classes and functional dependencies. 26 | 27 | ## Recursive `do` 28 | 29 | See the [`RecursiveDo`](https://limperg.de/ghc-extensions/#recursivedo) language extension. 30 | 31 | ## Template Haskell 32 | 33 | This is a somewhat controversial extension. Many use it to reduce the time spent writing boilerplate correctly. 34 | 35 | See the [`TemplateHaskell`](https://limperg.de/ghc-extensions/#templatehaskell) language extension. 36 | 37 | ## Postfix Symbolic Aliases 38 | 39 | See the [`PostfixOperators`](https://limperg.de/ghc-extensions/#postfixoperators) language extension 40 | 41 | ## List Comprehensions 42 | 43 | You may need to know the "list comprehension" syntax to read other's source code or to write one-liners. I don't think I would recommend using this syntax. 44 | 45 | In PureScript, we might write the following: 46 | ```purescript 47 | someValue :: List String 48 | someValue = do 49 | elemFromList1 <- list1 50 | elemFromList2 <- list2 51 | pure (show (elemFromList1 + elemFromList2)) 52 | -- where 53 | -- list1 = 1 : 2 : 3 : Nil 54 | -- list2 = 4 : 5 : 6 : Nil 55 | ``` 56 | 57 | We could write the same thing in Haskell using `do` notation. Or we could use "list comprehensions." The same code above can be rewritten to the following: 58 | ```haskell 59 | someValue = 60 | [ show (elemFromList1 + elemFromList2) | elemFromList1 <- list1, elemFromList2 <- list2 ] 61 | ``` 62 | -------------------------------------------------------------------------------- /02-Syntax/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | A few things to keep in mind while comparing the syntax of these two languages: 4 | - Haskell has been in development for a long time. Thus, some syntax decisions were made a long time ago and haven't been changed due to backwards compatibility. Others would be fixed but there hasn't been enough consensus as to what the solution should be. Others can be fixed by enabling some language extensions. 5 | - PureScript was created only a few years ago. It learned from a number of Haskell's "mistakes" and took a different route. 6 | - PureScript's initial compilation target is JavaScript. This target backend has constraints that influenced the design of PureScript's syntax. 7 | 8 | ## Reading the Code Examples 9 | 10 | A short summary of the differences will appear before each section. PureScript's code is presented before Haskell's code. 11 | -------------------------------------------------------------------------------- /03-Gotchas.md: -------------------------------------------------------------------------------- 1 | # Gotchas 2 | 3 | ## `compose` and other symbolic operators 4 | 5 | | PureScript | Haskell | 6 | | - | - | 7 | | `thenDoY <<< doX` | `thenDoY . doX` | 8 | | `arg # function` | `arg & function` | 9 | 10 | ## Prelude is Unsafe 11 | 12 | AFAICT, Haskell's default Prelude has at least the following issues: 13 | - it includes `Partial` functions (e.g. [here's a (potentially non-exhaustive) list of such functions](https://github.com/strake/rfcs/blob/ceae95ab61def026bff950d5edf42ed2cfad01b7/texts/0000-scrub-partial-functions-from-prelude.rst#summary)). Since there isn't a `Partial` type class to indicate these, one must know which functions to avoid. 14 | - most of its text-related functions use `List Char` (i.e. `String` in Haskell), not `Text` (i.e. what most programmers think of when we say `String`). 15 | - a number of common data types and functions aren't included. These must be obtained via libraries outside of Prelude, such as: 16 | - [vector](https://hackage.haskell.org/package/vector) to get PureScript-like `Array`s 17 | - [text](https://hackage.haskell.org/package/text) to get PureScript-like `String`s 18 | - [deepseq](https://hackage.haskell.org/package/deepseq) to get functions that can make things strict 19 | - [unordered-containers](https://hackage.haskell.org/package/unordered-containers) to get hashing-based containers (e.g. `HashMap`s, `Set`s, etc.). 20 | - various other problems mentioned in [Punt the Prelude : Inside 245-5D](http://blog.ezyang.com/2010/05/punt-the-prelude/) 21 | 22 | As a result, [many have attempted to define an alternative Prelude]((https://guide.aelve.com/haskell/alternative-preludes-zr69k1hc)) that deals with some of these issues. Unfortunately, I'm not sure whether using such alternatives is ultimately worth it. 23 | 24 | ## Handling Exceptions - Synchronous and Asynchronous 25 | 26 | - https://tech.fpcomplete.com/haskell/tutorial/exceptions 27 | - https://markkarpov.com/tutorial/exceptions.html 28 | 29 | ## Numeric Hierarchy 30 | 31 | The numeric type class hierarchy work differently in PureScript than it does in Haskell. Thus, some have come up with the [numeric-prelude](https://hackage.haskell.org/package/numeric-prelude-0.1.3.4) to account for some of these shortcomings. Again, I don't know how frequently this library is used, nor what tradeoffs are made by using it. 32 | 33 | ![Haskell Numeric Type Class Hierarchy](./assets/Haskell-Numeric-Type-Class-Hierarchy.svg) 34 | 35 | For a venn-diagram-basd way to visualize it, see [Haskell: Numeric Type Classes - How Numbers Work!](https://gist.github.com/CMCDragonkai/fab0980b3325e8a788c9#file-the_diagram-png) 36 | 37 | Read over their docs to get an idea for what functions they define. 38 | - [Num](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Num) 39 | - [Real](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Real) 40 | - [Integral](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Integral) 41 | - [Fractional](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Fractional) 42 | - [RealFrac](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:RealFrac) 43 | - [Floating](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Floating) 44 | - [RealFloat](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:RealFloat) 45 | -------------------------------------------------------------------------------- /04-Build-Tools/package-template.yml: -------------------------------------------------------------------------------- 1 | # For an explanation on each field below, see 2 | # https://www.haskell.org/cabal/users-guide/cabal-projectindex.html#cap-package.cabal%20fields 3 | # 4 | # hpack simply makes this less boilerplatey and simpler to write. 5 | 6 | # Project - Essentials 7 | name: foo 8 | version: 0.1.0.0 9 | 10 | # Project - Metadata 11 | github: "githubuser/foo" 12 | #git: 13 | # ^ no effect of github given 14 | #homepage: 15 | # ^ defaults to `#readme` if github is not blank 16 | #bug-reports: 17 | # ^ defaults to `/issues` if github is not blank 18 | license: BSD3 19 | #license-file 20 | # ^ Defaults to `LICENSE` if file exists 21 | author: "Author name here" 22 | maintainer: "example@example.com" 23 | # ^ defaults to author if blank 24 | copyright: "2019 Author name here" 25 | 26 | # Project- Metadata used when publishing package 27 | # synopsis: Short description of your package 28 | # category: Web 29 | 30 | # stability: 31 | 32 | # To avoid duplicated efforts in documentation and dealing with the 33 | # complications of embedding Haddock markup inside cabal files, it is 34 | # common to point users to the README.md file. 35 | description: Please see the README on GitHub at 36 | 37 | extra-source-files: 38 | - README.md 39 | - ChangeLog.md 40 | 41 | # Make developer experience similar to PureScript 42 | ghc-options: 43 | - -Wall 44 | - -Wincomplete-uni-patterns 45 | - -Wincomplete-record-updates 46 | - -Wmissing-export-lists 47 | - -Widentities 48 | - -Wredundant-constraints 49 | - -Wpartial-fields 50 | - -Wcompat 51 | - -fprint-expanded-synonyms 52 | - -fprint-explicit-foralls 53 | - -ferror-spans 54 | - -fwarn-tabs 55 | 56 | # Make developer experience similar to PureScript 57 | default-extensions: 58 | - KindSignatures 59 | - LiberalTypeSynonyms 60 | - EmptyDataDecls 61 | - OverloadedLists 62 | - OverloadedStrings 63 | - NumericUnderscores 64 | - NegativeLiterals 65 | - MultiParamTypeClasses 66 | - ConstrainedClassMethods 67 | - FlexibleInstances 68 | - FlexibleContexts 69 | - FunctionalDependencies 70 | - ConstraintKinds 71 | - InstanceSigs 72 | - DeriveFunctor 73 | - DeriveFoldable 74 | - DeriveTraversable 75 | - GeneralizedNewtypeDeriving 76 | - DerivingVia 77 | - MonoLocalBinds 78 | - PatternGuards 79 | - ScopedTypeVariables 80 | - RankNTypes 81 | - LambdaCase 82 | - BlockArguments 83 | - TypeApplications 84 | - RecordWildCards 85 | - NamedFieldPuns 86 | - ApplicativeDo 87 | - BangPatterns 88 | - PartialTypeSignatures 89 | - ExplicitNamespaces 90 | 91 | # The library contains all of our application code. The executable 92 | # defined below is just a thin wrapper. 93 | library: 94 | source-dirs: src 95 | # Enable additional extensions to the above 96 | #default-extensions: 97 | #- TemplateHaskell 98 | 99 | # Enable additional GHC options to the above 100 | #ghc-options: 101 | #- -Werror 102 | 103 | # Conditionally enable extensions or options 104 | # (e.g. `stack build --flag project-name:enable`) 105 | #when: 106 | #- condition: (flag(enable)) 107 | # default-extensions: 108 | # - TemplateHaskell 109 | # ghc-options: 110 | # - -O2 111 | 112 | # Toggle between specific configurations 113 | # (e.g. `stack build --flag project-name:toggle-on`) 114 | #when: 115 | #- condition: (flag(toggle-on)) 116 | # then: 117 | # default-extensions: 118 | # - TemplateHaskell 119 | # ghc-options: 120 | # - -O2 121 | # else: 122 | # default-extensions: 123 | # - TemplateHaskell 124 | # ghc-options: 125 | # - -O2 126 | 127 | executables: 128 | 129 | # stack exec -- project-name-exe 130 | project-name-exe: 131 | main: Main.hs 132 | source-dirs: app 133 | ghc-options: 134 | - -threaded 135 | - -rtsopts 136 | - -with-rtsopts=-N 137 | 138 | dependencies: 139 | - project-name 140 | 141 | # Test suite 142 | tests: 143 | project-name-test: 144 | main: Spec.hs 145 | source-dirs: test 146 | ghc-options: 147 | - -threaded 148 | - -rtsopts 149 | - -with-rtsopts=-N 150 | dependencies: 151 | - project-name 152 | 153 | # Define flags so that `stack build --flag project-name:flag-name` works 154 | flags: 155 | flag-name: 156 | description: Explanation of what this flag does 157 | manual: false 158 | default: false 159 | enable: 160 | description: Turns on other options 161 | manual: false 162 | default: false 163 | toggle-on: 164 | description: Uses 'then' options, not 'else' options 165 | manual: false 166 | default: false 167 | -------------------------------------------------------------------------------- /05-Yesod-and-Servant.md: -------------------------------------------------------------------------------- 1 | # Yesod and Servant 2 | 3 | ## Overview of Both 4 | 5 | Servant 6 | - uses template-haskell to reduce boilerplate for some things 7 | - uses type-level programming to guarantee that routes are correct 8 | - uses type-level programming to associate a type-level route with its corresponding handler 9 | - uses type classes extensively to do practically everything, including serialization. 10 | 11 | | Pros | Cons | 12 | | - | - | 13 | | - easy to understand and use | - restricts you to using type class codecs (e.g. `aeson`) rather than value-level codecs (e.g. `purescript-codec`) | 14 | | - provides good documentation on routes out-of-box | | 15 | | - handles the boring boilerplate for you | | 16 | 17 | Philosophically, I disagree with Servant's design tradeoffs due to the type class based codecs due to reading [Some Thoughts on Typeclass-based Codecs](http://code.slipthrough.net/2018/03/13/thoughts-on-typeclass-codecs/) 18 | 19 | Yesod (wip) 20 | - uses template-haskell to reduce boilerplate for some things 21 | 22 | ## Real World Demos of Yesod and Servant 23 | 24 | - [Real World - Yesod (server) + Persistent-created SQL](https://github.com/tzemanovic/haskell-yesod-realworld-example-app) 25 | - [Real World - Servant (server) + hand-written SQL](https://github.com/dorlowd/haskell-servant-realworld-example-app) 26 | - [Real World - Servant (server) + BEAM-created SQL](https://github.com/boxyoman/haskell-realworld-example) 27 | 28 | ## Performance and Benchmarks 29 | 30 | Both frameworks use `warp` as their web server. [The Performance of Open Source Applications](https://www.aosabook.org/en/posa/warp.html) explains why warp is fast. 31 | 32 | See how Yesod and Servant compare with other frameworks via the [TechEmpower Framework Benchmakrs](https://www.techempower.com/benchmarks/). As always, take these benchmarks with a grain of salt and read over their [methodology for producing these results](https://www.techempower.com/benchmarks/#section=motivation&hw=ph&test=fortune). 33 | 34 | ## Overview of Dependencies 35 | 36 | ### Both 37 | 38 | - `warp` functions very similiarly to [`purescript-httpure`](https://www.aosabook.org/en/posa/warp.html) 39 | 40 | ### Yesod 41 | 42 | TODO 43 | 44 | ### Servant 45 | 46 | TODO 47 | -------------------------------------------------------------------------------- /20-yesod/README.md: -------------------------------------------------------------------------------- 1 | # Yesod 2 | 3 | This folder's contents were generated via `stack new my-yesod yesod-postgres`. Then they were edited by hand. This folder is still a WIP. 4 | 5 | Changes made: 6 | - Changed Stack's LTS resolver to 14.12 7 | - Removed version boundaries on all depenedencies in `package.yml` 8 | - Use meta-language to document Route syntax (See `config/routes` and `Handler/Routes.hs` files) 9 | - Use meta-language to document Persistent syntax (See `config/models` and `Model.hs` and `Model/*.hs` files) 10 | - Deleted files related to Shakespearean files (Hamlet, Julius, Cassius/Lucius): 11 | - Deleted `/template/*` files 12 | - Deleted `/static/*` files 13 | - Deleted `.dir-locals.el` file 14 | 15 | ## Overview of Directories and Files 16 | 17 | ``` 18 | /app -- source code for main.hs (live version) and devel.hs (test version) 19 | -- no reason to ever touch these files once created 20 | 21 | /config -- all configuration- and Template Haskell-related things 22 | /favicon.ico -- self-explanatory 23 | /keter.yml -- configuration for deploying this web application via Keter 24 | /models -- define your database's tables via Persistent's Template Haskell syntax 25 | /robots.txt -- self-explanatory 26 | /routes -- define your web server's routes via Template Haskell 27 | /settings.yml -- define settings for live version. For example 28 | -- - the location of the static directory 29 | -- - database configuration (e.g. username, password, etc.) 30 | -- - copyright statement 31 | -- - analytics code 32 | /test-settings.yml -- define settings for test version 33 | /src 34 | /Handler -- All routes defined in `/config/routes` 35 | -- will be handled in this folder 36 | /Import -- Directory for storing any files containing 37 | -- any types/functions/values that should 38 | -- be accessible throughout most/all of the application. 39 | /NoFoundation.hs -- Defines most of the imports 40 | -- for Yesod and its dependencies 41 | -- in one file. 42 | /Settings 43 | StaticFiles.hs -- Template Haskell for determining 44 | -- which static files to include 45 | -- using compile-time verificatoin 46 | /Application.hs -- Boilerplate code to make Yesod work 47 | -- as a web application. You likely 48 | -- will not need to touch this much. 49 | /Foundation.hs -- Core code for changing how the 50 | -- application works. You will be editing 51 | -- this. 52 | /Import.hs -- This file is imported everywhere 53 | -- Any types/functions/values that need to be 54 | -- accessible in most of your application should 55 | -- be imported here as `import Module as Import` 56 | /Model.hs 57 | /Settings.hs 58 | /static 59 | /templates 60 | /test 61 | ``` 62 | 63 | ## Database Setup 64 | 65 | After installing Postgres ([Linux installation](https://www.postgresql.org/download/linux/ubuntu/) / [Installation from source code](https://www.postgresql.org/docs/12/install-short.html), run: 66 | 67 | ``` 68 | # Switch to the `postgres` user 69 | su postgres 70 | # Type in password 71 | 72 | # Create the user 73 | createuser my-yesod --superuser --pwprompt 74 | # Type in 'my-yesod' for the password 75 | # Note: see `/config/settings.yml` to change the 76 | # password Yesod will use to something else 77 | 78 | # Create the live and test databases 79 | createdb my-yesod --owner my-yesod 80 | createdb my-yesod_test --owner my-yesod 81 | ``` 82 | 83 | ## Development 84 | 85 | Build the `yesod-bin` tool: 86 | ```bash 87 | stack build --copy-compiler-tool yesod-bin 88 | ``` 89 | 90 | Start a development server: 91 | ```bash 92 | stack exec -- yesod devel 93 | ``` 94 | 95 | As your code changes, your site will be automatically recompiled and redeployed to localhost. 96 | 97 | ## Tests 98 | 99 | ``` 100 | stack test --flag my-yesod:library-only --flag my-yesod:dev 101 | ``` 102 | 103 | (Because `yesod devel` passes the `library-only` and `dev` flags, matching those flags means you don't need to recompile between tests and development, and it disables optimization to speed up your test compile times). 104 | -------------------------------------------------------------------------------- /20-yesod/app/DevelMain.hs: -------------------------------------------------------------------------------- 1 | -- | Running your app inside GHCi. 2 | -- 3 | -- This option provides significantly faster code reload compared to 4 | -- @yesod devel@. However, you do not get automatic code reload 5 | -- (which may be a benefit, depending on your perspective). To use this: 6 | -- 7 | -- 1. Start up GHCi 8 | -- 9 | -- $ stack ghci my-yesod:lib --no-load --work-dir .stack-work-devel 10 | -- 11 | -- 2. Load this module 12 | -- 13 | -- > :l app/DevelMain.hs 14 | -- 15 | -- 3. Run @update@ 16 | -- 17 | -- > DevelMain.update 18 | -- 19 | -- 4. Your app should now be running, you can connect at http://localhost:3000 20 | -- 21 | -- 5. Make changes to your code 22 | -- 23 | -- 6. After saving your changes, reload by running: 24 | -- 25 | -- > :r 26 | -- > DevelMain.update 27 | -- 28 | -- You can also call @DevelMain.shutdown@ to stop the app 29 | -- 30 | -- There is more information about this approach, 31 | -- on the wiki: https://github.com/yesodweb/yesod/wiki/ghci 32 | -- 33 | -- WARNING: GHCi does not notice changes made to your template files. 34 | -- If you change a template, you'll need to either exit GHCi and reload, 35 | -- or manually @touch@ another Haskell module. 36 | 37 | module DevelMain where 38 | 39 | import Prelude 40 | import Application (getApplicationRepl, shutdownApp) 41 | 42 | import Control.Monad ((>=>)) 43 | import Control.Concurrent 44 | import Data.IORef 45 | import Foreign.Store 46 | import Network.Wai.Handler.Warp 47 | import GHC.Word 48 | 49 | -- | Start or restart the server. 50 | -- newStore is from foreign-store. 51 | -- A Store holds onto some data across ghci reloads 52 | update :: IO () 53 | update = do 54 | mtidStore <- lookupStore tidStoreNum 55 | case mtidStore of 56 | -- no server running 57 | Nothing -> do 58 | done <- storeAction doneStore newEmptyMVar 59 | tid <- start done 60 | _ <- storeAction (Store tidStoreNum) (newIORef tid) 61 | return () 62 | -- server is already running 63 | Just tidStore -> restartAppInNewThread tidStore 64 | where 65 | doneStore :: Store (MVar ()) 66 | doneStore = Store 0 67 | 68 | -- shut the server down with killThread and wait for the done signal 69 | restartAppInNewThread :: Store (IORef ThreadId) -> IO () 70 | restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do 71 | killThread tid 72 | withStore doneStore takeMVar 73 | readStore doneStore >>= start 74 | 75 | 76 | -- | Start the server in a separate thread. 77 | start :: MVar () -- ^ Written to when the thread is killed. 78 | -> IO ThreadId 79 | start done = do 80 | (port, site, app) <- getApplicationRepl 81 | forkFinally 82 | (runSettings (setPort port defaultSettings) app) 83 | -- Note that this implies concurrency 84 | -- between shutdownApp and the next app that is starting. 85 | -- Normally this should be fine 86 | (\_ -> putMVar done () >> shutdownApp site) 87 | 88 | -- | kill the server 89 | shutdown :: IO () 90 | shutdown = do 91 | mtidStore <- lookupStore tidStoreNum 92 | case mtidStore of 93 | -- no server running 94 | Nothing -> putStrLn "no Yesod app running" 95 | Just tidStore -> do 96 | withStore tidStore $ readIORef >=> killThread 97 | putStrLn "Yesod app is shutdown" 98 | 99 | tidStoreNum :: Word32 100 | tidStoreNum = 1 101 | 102 | modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO () 103 | modifyStoredIORef store f = withStore store $ \ref -> do 104 | v <- readIORef ref 105 | f v >>= writeIORef ref 106 | -------------------------------------------------------------------------------- /20-yesod/app/devel.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PackageImports #-} 2 | import "my-yesod" Application (develMain) 3 | import Prelude (IO) 4 | 5 | main :: IO () 6 | main = develMain 7 | -------------------------------------------------------------------------------- /20-yesod/app/main.hs: -------------------------------------------------------------------------------- 1 | import Prelude (IO) 2 | import Application (appMain) 3 | 4 | main :: IO () 5 | main = appMain 6 | -------------------------------------------------------------------------------- /20-yesod/config/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanMartinez/purescript-to-haskell/148632fef18c5ae9a4262a8934343a7966e89e01/20-yesod/config/favicon.ico -------------------------------------------------------------------------------- /20-yesod/config/keter.yml: -------------------------------------------------------------------------------- 1 | # After you've edited this file, remove the following line to allow 2 | # `yesod keter` to build your bundle. 3 | user-edited: false 4 | 5 | # A Keter app is composed of 1 or more stanzas. The main stanza will define our 6 | # web application. See the Keter documentation for more information on 7 | # available stanzas. 8 | stanzas: 9 | 10 | # Your Yesod application. 11 | - type: webapp 12 | 13 | # Name of your executable. You are unlikely to need to change this. 14 | # Note that all file paths are relative to the keter.yml file. 15 | # 16 | # The path given is for Stack projects. If you're still using cabal, change 17 | # to 18 | # exec: ../dist/build/my-yesod/my-yesod 19 | exec: ../dist/bin/my-yesod 20 | 21 | # Command line options passed to your application. 22 | args: [] 23 | 24 | hosts: 25 | # You can specify one or more hostnames for your application to respond 26 | # to. The primary hostname will be used for generating your application 27 | # root. 28 | - www.my-yesod.com 29 | 30 | # Enable to force Keter to redirect to https 31 | # Can be added to any stanza 32 | requires-secure: false 33 | 34 | # Static files. 35 | - type: static-files 36 | hosts: 37 | - static.my-yesod.com 38 | root: ../static 39 | 40 | # Uncomment to turn on directory listings. 41 | # directory-listing: true 42 | 43 | # Redirect plain domain name to www. 44 | - type: redirect 45 | 46 | hosts: 47 | - my-yesod.com 48 | actions: 49 | - host: www.my-yesod.com 50 | # secure: false 51 | # port: 80 52 | 53 | # Uncomment to switch to a non-permanent redirect. 54 | # status: 303 55 | 56 | # Use the following to automatically copy your bundle upon creation via `yesod 57 | # keter`. Uses `scp` internally, so you can set it to a remote destination 58 | # copy-to: user@host:/opt/keter/incoming/ 59 | 60 | # You can pass arguments to `scp` used above. This example limits bandwidth to 61 | # 1024 Kbit/s and uses port 2222 instead of the default 22 62 | # copy-to-args: 63 | # - "-l 1024" 64 | # - "-P 2222" 65 | 66 | # If you would like to have Keter automatically create a PostgreSQL database 67 | # and set appropriate environment variables for it to be discovered, uncomment 68 | # the following line. 69 | # plugins: 70 | # postgres: true 71 | -------------------------------------------------------------------------------- /20-yesod/config/models: -------------------------------------------------------------------------------- 1 | -- By default this file is used by `persistFileWith` in Model.hs (which is imported by Foundation.hs) 2 | -- Syntax for this file here: https://github.com/yesodweb/persistent/blob/master/docs/Persistent-entity-syntax.md 3 | 4 | Table sql=real_table_name 5 | columnName DataType default='Foo' sqltype=varchar(3) sql=real_column_name 6 | columnNameThatCanBeNull DataType Maybe 7 | columnThatIsNullByDefault DataType default=NULL 8 | column1 Int 9 | column2 Int 10 | column3 Int 11 | UniqueColumns column1 column2 12 | UniqueColumns2 column3 13 | deriving Eq Show Ord 14 | 15 | -- This doesn't compile for some reason. Not sure why. 16 | -- ParentTable 17 | -- id Int 18 | -- foo Text 19 | -- 20 | -- ChildTable 21 | -- parentRow Int 22 | -- someOther ParentTable 23 | 24 | User 25 | ident Text 26 | password Text Maybe 27 | UniqueUser ident 28 | deriving Typeable 29 | Email 30 | email Text 31 | userId UserId Maybe 32 | verkey Text Maybe 33 | UniqueEmail email 34 | Comment json -- Adding "json" causes ToJSON and FromJSON instances to be derived. 35 | message Text 36 | userId UserId Maybe 37 | deriving Eq 38 | deriving Show 39 | -------------------------------------------------------------------------------- /20-yesod/config/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /20-yesod/config/routes: -------------------------------------------------------------------------------- 1 | -- By default this file is used by `parseRoutesFile` in Foundation.hs 2 | -- Syntax for this file here: https://www.yesodweb.com/book/routing-and-handlers 3 | -- and (advanced stuff) here: https://www.yesodweb.com/book/route-attributes 4 | 5 | -- For the rest, see 'Routes.hs' file 6 | 7 | -- Full Syntax 8 | -- - Not Hierarchial (does not end with ':') 9 | -- Route DataTypeR HTTP_Method HTTP_Method !routeAttribute 10 | -- 11 | -- - Hierarchial (ends with ':') 12 | -- Route DataTypeR HTTP_Method HTTP_Method !routeAttribute: 13 | 14 | -- Basic concept 15 | / HomeR GET POST 16 | 17 | -- static path 18 | /path PathR GET 19 | 20 | /allHttpMethods AllHttpMethodsR 21 | 22 | -- dynamic path 23 | /foo/#Int PathPieceR GET 24 | -- /bar/#ThreeInts MultiPathPieceR GET 25 | 26 | -- dynamic path - multiple arguments 27 | -- /multiple/*Int MultipleIntArgsR GET 28 | 29 | -- disabling overlap checking 30 | /first/0 FirstR GET 31 | !/first/#Int AfterFirstR GET 32 | 33 | -- adding route attributes 34 | /attribute AttributeR GET !attribute 35 | 36 | /hierarchialRoute HierarchialR: 37 | /route1 Route1R GET 38 | /route2 Route2R GET 39 | /subhierarchy SubHierarchyR: 40 | /final FinalR GET 41 | 42 | /hierarchyWithAttribute HierarchyAndAttributeR !outerHierarchyAttribute: 43 | /innerRoute InnerRouteR GET 44 | /subRoute SubRouteR !innerHierarchyAttribute: 45 | /endRoute EndRouteR GET 46 | 47 | -- Invalid syntax: a route cannot respond to a request (as indicated 48 | -- by 'GET') and be a hierarchical route at the same time (as indicated 49 | -- by the ':'). 50 | -- 51 | -- Uncomment these two lines to see for yourself 52 | -- See `src/Handler/Routes.hs` file for corresponding section. 53 | --/hierarchy HierarchyAndHttpMethod GET: 54 | -- /innerValRoute InnerValRouteR GET 55 | 56 | 57 | -- Authorized Needed Routes 58 | /authorizedByPath AuthorizedByPathR GET 59 | 60 | /authorizedByTopLevelAttribute AuthByTLAttribR !requiresAuthorization: 61 | /first AuthedFirstR GET 62 | /second AuthedSecondR GET 63 | 64 | -------------------------------------- 65 | 66 | /handlerFunctions HandlerFunctionsR GET POST 67 | 68 | -------------------------------------- 69 | 70 | -- See 'Common.hs' file 71 | /favicon.ico FaviconR GET 72 | /robots.txt RobotsR GET 73 | 74 | -- These are specially handled by Yesod 75 | /static StaticR Static appStatic 76 | /auth AuthR Auth getAuth 77 | 78 | ------------------------------------ 79 | 80 | -- Subsites 81 | 82 | -- Full Syntax 83 | -- Route DataTypeR SubSiteType foundationToSubsiteFunction 84 | -------------------------------------------------------------------------------- /20-yesod/config/settings.yml: -------------------------------------------------------------------------------- 1 | # Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. 2 | # See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables 3 | 4 | static-dir: "_env:YESOD_STATIC_DIR:static" 5 | host: "_env:YESOD_HOST:*4" # any IPv4 host 6 | port: "_env:YESOD_PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. 7 | ip-from-header: "_env:YESOD_IP_FROM_HEADER:false" 8 | 9 | # Default behavior: determine the application root from the request headers. 10 | # Uncomment to set an explicit approot 11 | #approot: "_env:YESOD_APPROOT:http://localhost:3000" 12 | 13 | # By default, `yesod devel` runs in development, and built executables use 14 | # production settings (see below). To override this, use the following: 15 | # 16 | # development: false 17 | 18 | # Optional values with the following production defaults. 19 | # In development, they default to the inverse. 20 | # 21 | # detailed-logging: false 22 | # should-log-all: false 23 | # reload-templates: false 24 | # mutable-static: false 25 | # skip-combining: false 26 | # auth-dummy-login : false 27 | 28 | # NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:YESOD_PGPASS:'123'") 29 | # See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings 30 | 31 | database: 32 | user: "_env:YESOD_PGUSER:my-yesod" 33 | password: "_env:YESOD_PGPASS:my-yesod" 34 | host: "_env:YESOD_PGHOST:localhost" 35 | port: "_env:YESOD_PGPORT:5432" 36 | # See config/test-settings.yml for an override during tests 37 | database: "_env:YESOD_PGDATABASE:my-yesod" 38 | poolsize: "_env:YESOD_PGPOOLSIZE:10" 39 | 40 | copyright: Insert copyright statement here 41 | #analytics: UA-YOURCODE 42 | -------------------------------------------------------------------------------- /20-yesod/config/test-settings.yml: -------------------------------------------------------------------------------- 1 | database: 2 | # NOTE: By design, this setting prevents the PGDATABASE environment variable 3 | # from affecting test runs, so that we don't accidentally affect the 4 | # production database during testing. If you're not concerned about that and 5 | # would like to have environment variable overrides, you could instead use 6 | # something like: 7 | # 8 | # database: "_env:PGDATABASE:my-yesod_test" 9 | database: my-yesod_test 10 | 11 | auth-dummy-login: true 12 | -------------------------------------------------------------------------------- /20-yesod/package.yaml: -------------------------------------------------------------------------------- 1 | name: my-yesod 2 | version: "0.0.0" 3 | 4 | # Note: the dependency ranges were removed. I'm relying upon the 5 | # Stack LTS resolver to ensure these build together. 6 | dependencies: 7 | - base 8 | 9 | # Prelude + TH 10 | - classy-prelude 11 | - safe # expose safer wrappers around unsafe Prelude functions 12 | - template-haskell 13 | 14 | # Core: Non Text 15 | - unordered-containers 16 | - containers 17 | - vector 18 | - time 19 | 20 | # Core: Text 21 | - bytestring 22 | - text 23 | - case-insensitive 24 | 25 | # Domain: Error-Handling 26 | - monad-control 27 | 28 | # Domain: Loggers 29 | - monad-logger 30 | - fast-logger 31 | 32 | # Domain: Seralization 33 | - yaml 34 | - aeson 35 | 36 | # Domain: Cookies 37 | - cookie 38 | 39 | # API: Web Application Interface 40 | - wai 41 | - wai-extra 42 | - wai-logger 43 | 44 | # Really Unsafe Pointer-based Storage? 45 | - foreign-store 46 | 47 | # Infrastructure: Web Server 48 | - warp 49 | - http-client-tls 50 | 51 | # Infrastructure: File System 52 | - directory 53 | - file-embed 54 | 55 | # Infrastructure: Streaming IO 56 | - conduit 57 | - classy-prelude-conduit 58 | - http-conduit 59 | 60 | # API/Infrastructure: Database 61 | - persistent 62 | - persistent-postgresql 63 | - persistent-template 64 | 65 | # Hamlet, Julius, Cassius/Lucius 66 | - shakespeare 67 | 68 | # JS Minifier 69 | - hjsmin 70 | 71 | # Default Type Class 72 | - data-default 73 | 74 | # Yesod 75 | - yesod 76 | - yesod-core 77 | - yesod-auth 78 | - yesod-static 79 | - yesod-form 80 | - classy-prelude-yesod 81 | 82 | ghc-options: 83 | - -Wall 84 | - -Wincomplete-uni-patterns 85 | - -Wincomplete-record-updates 86 | - -Widentities 87 | - -Wredundant-constraints 88 | - -Wpartial-fields 89 | - -Wcompat 90 | - -fprint-expanded-synonyms 91 | - -fprint-explicit-foralls 92 | - -ferror-spans 93 | - -fwarn-tabs 94 | 95 | # Make developer experience similar to PureScript 96 | default-extensions: 97 | - KindSignatures 98 | - LiberalTypeSynonyms 99 | - EmptyDataDecls 100 | - OverloadedLists 101 | - OverloadedStrings 102 | - NumericUnderscores 103 | - NegativeLiterals 104 | - MultiParamTypeClasses 105 | - ConstrainedClassMethods 106 | - FlexibleInstances 107 | - FlexibleContexts 108 | - FunctionalDependencies 109 | - ConstraintKinds 110 | - InstanceSigs 111 | - DeriveFunctor 112 | - DeriveFoldable 113 | - DeriveTraversable 114 | - GeneralizedNewtypeDeriving 115 | - DerivingVia 116 | - MonoLocalBinds 117 | - PatternGuards 118 | - ScopedTypeVariables 119 | - RankNTypes 120 | - LambdaCase 121 | - BlockArguments 122 | - TypeApplications 123 | - RecordWildCards 124 | - NamedFieldPuns 125 | - ApplicativeDo 126 | - BangPatterns 127 | - PartialTypeSignatures 128 | - ExplicitNamespaces 129 | 130 | # The library contains all of our application code. The executable 131 | # defined below is just a thin wrapper. 132 | library: 133 | source-dirs: src 134 | # Haskell-Only stuff 135 | default-extensions: 136 | - TemplateHaskell 137 | - PolyKinds 138 | 139 | when: 140 | - condition: (flag(dev)) || (flag(library-only)) 141 | then: 142 | ghc-options: 143 | - -O0 144 | cpp-options: -DDEVELOPMENT 145 | else: 146 | ghc-options: 147 | - -O2 148 | #- -Werror 149 | 150 | # Runnable executable for our application 151 | executables: 152 | my-yesod: 153 | main: main.hs 154 | source-dirs: app 155 | ghc-options: 156 | - -threaded 157 | - -rtsopts 158 | - -with-rtsopts=-N 159 | dependencies: 160 | - my-yesod 161 | when: 162 | - condition: flag(library-only) 163 | buildable: false 164 | 165 | # Test suite 166 | tests: 167 | my-yesod-test: 168 | main: Spec.hs 169 | source-dirs: test 170 | # Haskell-Only stuff 171 | default-extensions: 172 | - TemplateHaskell 173 | - PolyKinds 174 | dependencies: 175 | - my-yesod 176 | - hspec 177 | - yesod-test 178 | 179 | # Define flags used by "yesod devel" to make compilation faster 180 | flags: 181 | library-only: 182 | description: Build for use with "yesod devel" 183 | manual: false 184 | default: false 185 | dev: 186 | description: Turn on development settings, like auto-reload templates. 187 | manual: false 188 | default: false 189 | -------------------------------------------------------------------------------- /20-yesod/src/Application.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# OPTIONS_GHC -fno-warn-orphans #-} 7 | module Application 8 | ( getApplicationDev 9 | , appMain 10 | , develMain 11 | , makeFoundation 12 | , makeLogWare 13 | -- * for DevelMain 14 | , getApplicationRepl 15 | , shutdownApp 16 | -- * for GHCI 17 | , handler 18 | , db 19 | ) where 20 | 21 | import Control.Monad.Logger (liftLoc, runLoggingT) 22 | import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, 23 | pgPoolSize, runSqlPool) 24 | import Import 25 | import Language.Haskell.TH.Syntax (qLocation) 26 | import Network.HTTP.Client.TLS (getGlobalManager) 27 | import Network.Wai (Middleware) 28 | import Network.Wai.Handler.Warp (Settings, defaultSettings, 29 | defaultShouldDisplayException, 30 | runSettings, setHost, 31 | setOnException, setPort, getPort) 32 | import Network.Wai.Middleware.RequestLogger (Destination (Logger), 33 | IPAddrSource (..), 34 | OutputFormat (..), destination, 35 | mkRequestLogger, outputFormat) 36 | import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, 37 | toLogStr) 38 | 39 | -- Import all relevant handler modules here. 40 | -- Don't forget to add new modules to your cabal file! 41 | import Handler.Common 42 | import Handler.Routes 43 | 44 | -- This line actually creates our YesodDispatch instance. It is the second half 45 | -- of the call to mkYesodData which occurs in Foundation.hs. Please see the 46 | -- comments there for more details. 47 | mkYesodDispatch "App" resourcesApp 48 | 49 | -- | This function allocates resources (such as a database connection pool), 50 | -- performs initialization and returns a foundation datatype value. This is also 51 | -- the place to put your migrate statements to have automatic database 52 | -- migrations handled by Yesod. 53 | makeFoundation :: AppSettings -> IO App 54 | makeFoundation appSettings = do 55 | -- Some basic initializations: HTTP connection manager, logger, and static 56 | -- subsite. 57 | appHttpManager <- getGlobalManager 58 | appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger 59 | appStatic <- 60 | (if appMutableStatic appSettings then staticDevel else static) 61 | (appStaticDir appSettings) 62 | 63 | -- We need a log function to create a connection pool. We need a connection 64 | -- pool to create our foundation. And we need our foundation to get a 65 | -- logging function. To get out of this loop, we initially create a 66 | -- temporary foundation without a real connection pool, get a log function 67 | -- from there, and then create the real foundation. 68 | let mkFoundation appConnPool = App {..} 69 | -- The App {..} syntax is an example of record wild cards. For more 70 | -- information, see: 71 | -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html 72 | tempFoundation = mkFoundation $ error "connPool forced in tempFoundation" 73 | logFunc = messageLoggerSource tempFoundation appLogger 74 | 75 | -- Create the database connection pool 76 | pool <- flip runLoggingT logFunc $ createPostgresqlPool 77 | (pgConnStr $ appDatabaseConf appSettings) 78 | (pgPoolSize $ appDatabaseConf appSettings) 79 | 80 | -- Perform database migration using our application's logging settings. 81 | runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc 82 | 83 | -- Return the foundation 84 | return $ mkFoundation pool 85 | 86 | -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and 87 | -- applying some additional middlewares. 88 | makeApplication :: App -> IO Application 89 | makeApplication foundation = do 90 | logWare <- makeLogWare foundation 91 | -- Create the WAI application and apply middlewares 92 | appPlain <- toWaiAppPlain foundation 93 | return $ logWare $ defaultMiddlewaresNoLogging appPlain 94 | 95 | makeLogWare :: App -> IO Middleware 96 | makeLogWare foundation = 97 | mkRequestLogger def 98 | { outputFormat = 99 | if appDetailedRequestLogging $ appSettings foundation 100 | then Detailed True 101 | else Apache 102 | (if appIpFromHeader $ appSettings foundation 103 | then FromFallback 104 | else FromSocket) 105 | , destination = Logger $ loggerSet $ appLogger foundation 106 | } 107 | 108 | 109 | -- | Warp settings for the given foundation value. 110 | warpSettings :: App -> Settings 111 | warpSettings foundation = 112 | setPort (appPort $ appSettings foundation) 113 | $ setHost (appHost $ appSettings foundation) 114 | $ setOnException (\_req e -> 115 | when (defaultShouldDisplayException e) $ messageLoggerSource 116 | foundation 117 | (appLogger foundation) 118 | $(qLocation >>= liftLoc) 119 | "yesod" 120 | LevelError 121 | (toLogStr $ "Exception from Warp: " ++ show e)) 122 | defaultSettings 123 | 124 | -- | For yesod devel, return the Warp settings and WAI Application. 125 | getApplicationDev :: IO (Settings, Application) 126 | getApplicationDev = do 127 | settings <- getAppSettings 128 | foundation <- makeFoundation settings 129 | wsettings <- getDevSettings $ warpSettings foundation 130 | app <- makeApplication foundation 131 | return (wsettings, app) 132 | 133 | getAppSettings :: IO AppSettings 134 | getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv 135 | 136 | -- | main function for use by yesod devel 137 | develMain :: IO () 138 | develMain = develMainHelper getApplicationDev 139 | 140 | -- | The @main@ function for an executable running this site. 141 | appMain :: IO () 142 | appMain = do 143 | -- Get the settings from all relevant sources 144 | settings <- loadYamlSettingsArgs 145 | -- fall back to compile-time values, set to [] to require values at runtime 146 | [configSettingsYmlValue] 147 | 148 | -- allow environment variables to override 149 | useEnv 150 | 151 | -- Generate the foundation from the settings 152 | foundation <- makeFoundation settings 153 | 154 | -- Generate a WAI Application from the foundation 155 | app <- makeApplication foundation 156 | 157 | -- Run the application with Warp 158 | runSettings (warpSettings foundation) app 159 | 160 | 161 | -------------------------------------------------------------- 162 | -- Functions for DevelMain.hs (a way to run the app from GHCi) 163 | -------------------------------------------------------------- 164 | getApplicationRepl :: IO (Int, App, Application) 165 | getApplicationRepl = do 166 | settings <- getAppSettings 167 | foundation <- makeFoundation settings 168 | wsettings <- getDevSettings $ warpSettings foundation 169 | app1 <- makeApplication foundation 170 | return (getPort wsettings, foundation, app1) 171 | 172 | shutdownApp :: App -> IO () 173 | shutdownApp _ = return () 174 | 175 | 176 | --------------------------------------------- 177 | -- Functions for use in development with GHCi 178 | --------------------------------------------- 179 | 180 | -- | Run a handler 181 | handler :: Handler a -> IO a 182 | handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h 183 | 184 | -- | Run DB queries 185 | db :: ReaderT SqlBackend Handler a -> IO a 186 | db = handler . runDB 187 | -------------------------------------------------------------------------------- /20-yesod/src/Foundation.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | 6 | module Foundation where 7 | 8 | import Import.NoFoundation 9 | import Database.Persist.Sql (ConnectionPool, runSqlPool) 10 | import Text.Jasmine (minifym) 11 | import Control.Monad.Logger (LogSource) 12 | 13 | -- Used only when in "auth-dummy-login" setting is enabled. 14 | import Yesod.Auth.Dummy 15 | 16 | import Yesod.Auth.OpenId (authOpenId, IdentifierType (Claimed)) 17 | import Yesod.Default.Util (addStaticContentExternal) 18 | import Yesod.Core.Types (Logger) 19 | import qualified Yesod.Core.Unsafe as Unsafe 20 | -- import qualified Data.CaseInsensitive as CI 21 | -- import qualified Data.Text.Encoding as TE 22 | 23 | -- | The foundation datatype for your application. This can be a good place to 24 | -- keep settings and values requiring initialization before your application 25 | -- starts running, such as database connections. Every handler will have 26 | -- access to the data present here. 27 | data App = App 28 | { appSettings :: AppSettings 29 | , appStatic :: Static -- ^ Settings for static file serving. 30 | , appConnPool :: ConnectionPool -- ^ Database connection pool. 31 | , appHttpManager :: Manager 32 | , appLogger :: Logger 33 | } 34 | 35 | data MenuItem = MenuItem 36 | { menuItemLabel :: Text 37 | , menuItemRoute :: Route App 38 | , menuItemAccessCallback :: Bool 39 | } 40 | 41 | data MenuTypes 42 | = NavbarLeft MenuItem 43 | | NavbarRight MenuItem 44 | 45 | -- This is where we define all of the routes in our application. For a full 46 | -- explanation of the syntax, please see: 47 | -- http://www.yesodweb.com/book/routing-and-handlers 48 | -- 49 | -- Note that this is really half the story; in Application.hs, mkYesodDispatch 50 | -- generates the rest of the code. Please see the following documentation 51 | -- for an explanation for this split: 52 | -- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules 53 | -- 54 | -- This function also generates the following type synonyms: 55 | -- (deprecated! see comment below) type Handler = HandlerT App IO 56 | -- (deprecated! see commnt below) type Widget = WidgetT App IO () 57 | mkYesodData "App" $(parseRoutesFile "config/routes") 58 | 59 | -- Handler/HandlerT is deprecated: 60 | -- https://hackage.haskell.org/package/yesod-core-1.6.16.1/docs/Yesod-Core-Handler.html#t:HandlerT 61 | -- Widget/WidgetT is deprecated∷ 62 | -- https://hackage.haskell.org/package/yesod-core-1.6.16.1/docs/Yesod-Core-Widget.html#t:WidgetT 63 | type HandlerX = HandlerFor App 64 | type WidgetX = WidgetFor App 65 | 66 | -- | A convenient synonym for creating forms. 67 | type Form x = Html -> MForm (HandlerFor App) (FormResult x, Widget) 68 | 69 | -- | A convenient synonym for database access functions. 70 | type DB a = forall (m :: * -> *). 71 | (MonadIO m) => ReaderT SqlBackend m a 72 | 73 | -- | Timeout in minutes. Used in `defaultClientSessionBackend`. 74 | clientSessionTimeout :: Int 75 | clientSessionTimeout = 120 -- minutes 76 | 77 | -- | Timeout in minutes. Used in `sslOnlyMiddleware`. Ensures that 78 | -- HTTP requests will not be made for as long as (if not longer than) 79 | -- the client session cookie's timeout. To increase the timeout, change `0` to 80 | -- a positive value. 81 | strictTransportSecurityTimeout :: Int 82 | strictTransportSecurityTimeout = 83 | clientSessionTimeout + 0 -- minutes 84 | 85 | -- Please see the documentation for the Yesod typeclass. There are a number 86 | -- of settings which can be configured by overriding methods here. 87 | instance Yesod App where 88 | -- === Application-related things === 89 | 90 | -- Controls the base of generated URLs. For more information on modifying, 91 | -- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot 92 | approot :: Approot App 93 | approot = ApprootRequest $ \app req -> 94 | case appRoot $ appSettings app of 95 | Nothing -> getApprootText guessApproot app req 96 | Just root -> root 97 | 98 | -- Store session data on the client in encrypted cookies, 99 | -- default session idle timeout is 120 minutes 100 | -- Also ensure that these sessions are only transmitted via HTTPS 101 | makeSessionBackend :: App -> IO (Maybe SessionBackend) 102 | makeSessionBackend _ = sslOnlySessions $ fmap Just $ defaultClientSessionBackend 103 | clientSessionTimeout 104 | "config/client_session_key.aes" 105 | 106 | -- Yesod Middleware allows you to run code before and after each handler function. 107 | -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. 108 | -- Some users may also want to add the defaultCsrfMiddleware, which: 109 | -- a) Sets a cookie with a CSRF token in it. 110 | -- b) Validates that incoming write requests include that token in either a header or POST parameter. 111 | -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware 112 | -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. 113 | -- Also add `sslOnlyMiddleware` with same client session timeout as 114 | yesodMiddleware :: ToTypedContent res => Handler res -> Handler res 115 | yesodMiddleware = 116 | (sslOnlyMiddleware strictTransportSecurityTimeout) . 117 | defaultYesodMiddleware . defaultCsrfMiddleware 118 | 119 | -- === Path-related things === 120 | 121 | --cleanPath :: site -> [Text] -> Either [Text] [Text] 122 | --cleanPath = default implementation should be fine 123 | 124 | --joinPath :: site -> T.Text -> [T.Text] -> [(T.Text, T.Text)] -> Builder 125 | --joinPath = default implementation should be fine 126 | 127 | -- === Route-related things === 128 | 129 | -- The page to be redirected to when authentication is required. 130 | authRoute 131 | :: App 132 | -> Maybe (Route App) 133 | authRoute _ = Just $ AuthR LoginR 134 | 135 | isAuthorized 136 | :: Route App -- ^ The route the user is visiting. 137 | -> Bool -- ^ Whether or not this is a "write" request. 138 | -> Handler AuthResult 139 | -- Routes requiring authorization 140 | isAuthorized AuthorizedByPathR _ = pure (Unauthorized "Requires authorization") 141 | isAuthorized route@(AuthByTLAttribR _) _ = 142 | if "requiresAuthorization" `member` routeAttrs route 143 | then do 144 | muser <- maybeAuthId 145 | case muser of 146 | Nothing -> pure AuthenticationRequired 147 | Just _ -> pure Authorized 148 | else pure (Unauthorized "Needs authentication") 149 | 150 | -- All other routes don't require authorization. 151 | isAuthorized _ _ = pure Authorized 152 | 153 | -- === Miscellaneous Application things === 154 | 155 | -- maximumContentLength :: App -> Maybe (Route site) -> Maybe Word64 156 | -- maximumContentLength _ _ = Just $ 2 * 1024 * 1024 -- 2 megabytes 157 | 158 | -- fileUpload :: App -> RequestBodyLength -> FileUpload 159 | -- fileUpload _ _ 160 | -- -- default shown below 161 | -- | when req body > 50kb || chunked req body = store in temp file 162 | -- | otherwise = store in memory 163 | 164 | -- === Logger-related things === 165 | 166 | -- What messages should be logged. The following includes all messages when 167 | -- in development, and warnings and errors in production. 168 | shouldLogIO :: App -> LogSource -> LogLevel -> IO Bool 169 | shouldLogIO app _source level = 170 | return $ 171 | appShouldLogAll (appSettings app) 172 | || level == LevelWarn 173 | || level == LevelError 174 | 175 | makeLogger :: App -> IO Logger 176 | makeLogger = return . appLogger 177 | 178 | -- === Server-Side Rendering-related things === 179 | 180 | -- defaultLayout :: Widget -> Handler Html 181 | -- defaultLayout {- widget -} = do 182 | -- master <- getYesod 183 | -- mmsg <- getMessage 184 | 185 | -- muser <- maybeAuthPair 186 | -- mcurrentRoute <- getCurrentRoute 187 | 188 | -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. 189 | -- (title, parents) <- breadcrumbs 190 | 191 | -- pure $ toHtml "foo" 192 | 193 | --errorHandler :: ErrorResponse -> HandlerFor App TypedContent 194 | --errorHandler = defaultErrorHandler 195 | 196 | -- This function creates static content files in the static folder 197 | -- and names them based on a hash of their content. This allows 198 | -- expiration dates to be set far in the future without worry of 199 | -- users receiving stale content. 200 | addStaticContent 201 | :: Text -- ^ The file extension 202 | -> Text -- ^ The MIME content type 203 | -> LByteString -- ^ The contents of the file 204 | -> Handler (Maybe (Either Text (Route App, [(Text, Text)]))) 205 | addStaticContent ext mime content = do 206 | master <- getYesod 207 | let staticDir = appStaticDir $ appSettings master 208 | addStaticContentExternal 209 | minifym 210 | genFileName 211 | staticDir 212 | (StaticR . flip StaticRoute []) 213 | ext 214 | mime 215 | content 216 | where 217 | -- Generate a unique filename based on the content itself 218 | genFileName lbs = "autogen-" ++ base64md5 lbs 219 | 220 | -- Define breadcrumbs. 221 | -- instance YesodBreadcrumbs App where 222 | -- Takes the route that the user is currently on, and returns a tuple 223 | -- of the 'Text' that you want the label to display, and a previous 224 | -- breadcrumb route. 225 | -- breadcrumb 226 | -- :: Route App -- ^ The route the user is visiting currently. 227 | -- -> Handler (Text, Maybe (Route App)) 228 | -- breadcrumb HomeR = return ("Home", Nothing) 229 | -- breadcrumb (AuthR _) = return ("Login", Just HomeR) 230 | -- breadcrumb ProfileR = return ("Profile", Just HomeR) 231 | -- breadcrumb _ = return ("home", Nothing) 232 | 233 | -- How to run database actions. 234 | instance YesodPersist App where 235 | type YesodPersistBackend App = SqlBackend 236 | runDB :: SqlPersistT (HandlerFor App) a -> HandlerFor App a 237 | runDB action = do 238 | master <- getYesod 239 | runSqlPool action $ appConnPool master 240 | 241 | instance YesodPersistRunner App where 242 | getDBRunner :: HandlerFor App (DBRunner App, Handler ()) 243 | getDBRunner = defaultGetDBRunner appConnPool 244 | 245 | instance YesodAuth App where 246 | type AuthId App = UserId 247 | 248 | -- Where to send a user after successful login 249 | loginDest :: App -> Route App 250 | loginDest _ = HomeR 251 | -- Where to send a user after logout 252 | logoutDest :: App -> Route App 253 | logoutDest _ = HomeR 254 | -- Override the above two destinations when a Referer: header is present 255 | redirectToReferer :: App -> Bool 256 | redirectToReferer _ = True 257 | 258 | authenticate :: (MonadHandler m, HandlerSite m ~ App) 259 | => Creds App -> m (AuthenticationResult App) 260 | authenticate creds = liftHandler $ runDB $ do 261 | x <- getBy $ UniqueUser $ credsIdent creds 262 | case x of 263 | Just (Entity uid _) -> return $ Authenticated uid 264 | Nothing -> Authenticated <$> insert User 265 | { userIdent = credsIdent creds 266 | , userPassword = Nothing 267 | } 268 | 269 | -- You can add other plugins like Google Email, email or OAuth here 270 | authPlugins :: App -> [AuthPlugin App] 271 | authPlugins app = [authOpenId Claimed []] ++ extraAuthPlugins 272 | -- Enable authDummy login if enabled. 273 | where extraAuthPlugins = [authDummy | appAuthDummyLogin $ appSettings app] 274 | 275 | -- | Access function to determine if a user is logged in. 276 | isAuthenticated :: HandlerFor App AuthResult 277 | isAuthenticated = do 278 | muid <- maybeAuthId 279 | return $ case muid of 280 | Nothing -> Unauthorized "You must login to access this page" 281 | Just _ -> Authorized 282 | 283 | instance YesodAuthPersist App 284 | 285 | -- This instance is required to use forms. You can modify renderMessage to 286 | -- achieve customized and internationalized form validation messages. 287 | instance RenderMessage App FormMessage where 288 | renderMessage :: App -> [Lang] -> FormMessage -> Text 289 | renderMessage _ _ = defaultFormMessage 290 | 291 | -- Useful when writing code that is re-usable outside of the Handler context. 292 | -- An example is background jobs that send email. 293 | -- This can also be useful for writing code that works across multiple Yesod applications. 294 | instance HasHttpManager App where 295 | getHttpManager :: App -> Manager 296 | getHttpManager = appHttpManager 297 | 298 | unsafeHandler :: App -> HandlerFor App a -> IO a 299 | unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger 300 | 301 | -- Note: Some functionality previously present in the scaffolding has been 302 | -- moved to documentation in the Wiki. Following are some hopefully helpful 303 | -- links: 304 | -- 305 | -- https://github.com/yesodweb/yesod/wiki/Sending-email 306 | -- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain 307 | -- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding 308 | -------------------------------------------------------------------------------- /20-yesod/src/Handler/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | -- | Common handler functions. 5 | module Handler.Common where 6 | 7 | import Data.FileEmbed (embedFile) 8 | import Import 9 | 10 | -- These handlers embed files in the executable at compile time to avoid a 11 | -- runtime dependency, and for efficiency. 12 | 13 | getFaviconR :: Handler TypedContent 14 | getFaviconR = do cacheSeconds $ 60 * 60 * 24 * 30 -- cache for a month 15 | return $ TypedContent "image/x-icon" 16 | $ toContent $(embedFile "config/favicon.ico") 17 | 18 | getRobotsR :: Handler TypedContent 19 | getRobotsR = return $ TypedContent typePlain 20 | $ toContent $(embedFile "config/robots.txt") 21 | -------------------------------------------------------------------------------- /20-yesod/src/Handler/Routes.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | module Handler.Routes where 3 | 4 | import Import 5 | import qualified Data.CaseInsensitive as CI 6 | import Data.CaseInsensitive (CI(..)) 7 | import Web.Cookie (setCookieName, setCookiePath, setCookieExpires, setCookieValue, setCookieMaxAge, setCookieDomain, setCookieSecure, setCookieHttpOnly, setCookieSameSite, defaultSetCookie, sameSiteStrict) 8 | 9 | -- ## Preface 10 | -- 11 | -- Normally, we'd use Handler here. However, it's deprecated. 12 | -- https://hackage.haskell.org/package/yesod-core-1.6.16.1/docs/Yesod-Core-Handler.html#t:HandlerT 13 | -- 14 | -- so, we've defined our own type alias in `Foundation.hs` to replace it 15 | -- called `HandlerX` and we'll be using it here below. 16 | -- type HandlerX a = HandleFor App a 17 | 18 | -- ## Route Examples 19 | 20 | -- ### No Authorization needed 21 | 22 | getHomeR :: HandlerX String 23 | getHomeR = pure "home - get" 24 | 25 | postHomeR :: HandlerX String 26 | postHomeR = pure "home - post" 27 | 28 | handleAllHttpMethodsR :: HandlerX String 29 | handleAllHttpMethodsR = pure "all http methods handled regardless of method" 30 | 31 | getPathR :: HandlerX String 32 | getPathR = pure "path" 33 | 34 | getPathPieceR :: Int -> HandlerX String 35 | getPathPieceR i = pure ("path piece " <> show i) 36 | 37 | -- getMultiPathPieceR :: ThreeInts -> HandlerX String 38 | -- getMultiPathPieceR (ThreeInts (a, b, c)) = pure ("path pieces: " <> show (a + b + c)) 39 | 40 | -- getMultipleIntArgsR :: [Int] -> HandlerX String 41 | -- getMultipleIntArgsR list = pure (foldl' (\acc next -> acc <> show next) "" list) 42 | 43 | getFirstR :: HandlerX String 44 | getFirstR = pure "first" 45 | 46 | getAttributeR :: HandlerX String 47 | getAttributeR = pure "attribute route" 48 | 49 | getAfterFirstR :: Int -> HandlerX String 50 | getAfterFirstR i = pure ("after first: " <> show i) 51 | 52 | getRoute1R :: HandlerX String 53 | getRoute1R = pure "route 1 r" 54 | 55 | getRoute2R :: HandlerX String 56 | getRoute2R = pure "route 2 r" 57 | 58 | getFinalR :: HandlerX String 59 | getFinalR = pure "Final R" 60 | 61 | getInnerRouteR :: HandlerX String 62 | getInnerRouteR = pure "inner route" 63 | 64 | getSubRouteR :: HandlerX String 65 | getSubRouteR = pure "sub route" 66 | 67 | getEndRouteR :: HandlerX String 68 | getEndRouteR = pure "end route" 69 | 70 | -- See `config/routes` file for corresponding section. 71 | -- Invalid syntax: a route cannot respond to a request (as indicated 72 | -- by 'GET') and be a hierarchical route at the same time (as indicated 73 | -- by the ':'). 74 | -- 75 | -- Uncomment these lines to see for yourself. 76 | --getHierarchyAndHttpMethod :: HandlerX String 77 | --getHierarchyAndHttpMethod = pure "hierarchy and http method" 78 | -- 79 | --getInnerValRouteR :: HandlerX String 80 | --getInnerValRouteR = pure "inner val route" 81 | 82 | -- ### Authorization needed 83 | 84 | getAuthorizedByPathR :: HandlerX String 85 | getAuthorizedByPathR = pure "authorized by path" 86 | 87 | getAuthedFirstR :: HandlerX String 88 | getAuthedFirstR = pure "first - authorized by top-level attribute" 89 | 90 | getAuthedSecondR :: HandlerX String 91 | getAuthedSecondR = pure "second - authorized by top-level attribute" 92 | 93 | -- ### Handler Functions 94 | 95 | getHandlerFunctionsR :: HandlerX String 96 | getHandlerFunctionsR = do 97 | yesod <- getYesod 98 | yesod2 <- ask 99 | configValue <- asks \yesod3 -> {- getValue -} yesod3 100 | 101 | -- #### Query Parameters 102 | 103 | maybeParameterValue <- lookupGetParam "queryParam" 104 | maybeListParameterValues <- lookupGetParams "queryParam" 105 | 106 | -- #### Headers 107 | 108 | -- any header 109 | addHeader "some-header" "value" 110 | replaceOrAddHeader "some-header-two" "value" 111 | 112 | -- these won't work until next request 113 | let someHeader = "some-header" :: CI ByteString 114 | maybeHeaderValue <- lookupHeader someHeader 115 | maybeListHeaderValues <- lookupHeaders someHeader 116 | 117 | -- Sets the language for user's session 118 | setLanguage "en-US" 119 | 120 | -- List can include values in this order: 121 | -- user session, then parameter, then cookie, then "Accept-Language" header 122 | listOfLanguages <- languages 123 | 124 | -- ##### Specific Headers 125 | 126 | -- Content-Disposition: attachment; filename="file-name.jpg" 127 | addContentDispositionFileName "file-name.jpg" 128 | 129 | -- Cache-Control: max-age=20, public 130 | cacheSeconds 20 131 | 132 | -- Expires: HTTP-date 133 | nowUtcTime <- liftIO getCurrentTime 134 | neverExpires -- some date in 2037 135 | alreadyExpired -- date in past, so don't cache it 136 | expiresAt nowUtcTime 137 | 138 | -- #### Cookies 139 | 140 | setCsrfCookie 141 | 142 | twoHourExpiration <- getExpires 120 -- minutes 143 | let myCookie = 144 | defaultSetCookie { setCookieName = "cookie-name" 145 | , setCookieValue = "cookie-value" 146 | , setCookiePath = Just "/" 147 | , setCookieExpires = Just twoHourExpiration 148 | , setCookieMaxAge = Nothing 149 | , setCookieDomain = Nothing 150 | , setCookieHttpOnly = True 151 | , setCookieSecure = True 152 | , setCookieSameSite = Just sameSiteStrict 153 | } 154 | 155 | setCookie myCookie 156 | 157 | -- these won't work until next request 158 | maybeCookieValue <- lookupCookie "cookie-name" 159 | maybeListCookieValues <- lookupCookies "some-cookie" 160 | 161 | deleteCookie "key" "path" 162 | 163 | -- #### Sessions 164 | 165 | sessionMap <- getSession 166 | 167 | maybeTextValue <- lookupSession "key" 168 | maybeByteStringValue <- lookupSessionBS "key" 169 | 170 | setSession "key" "text value" 171 | setSessionBS "key" "bytestring value" 172 | 173 | deleteSession "key" 174 | 175 | clearSession 176 | 177 | -- post-redirect-get pattern 178 | setMessage "some message value" 179 | someMessageValue <- getMessage 180 | 181 | pure "get handler functions" 182 | 183 | postHandlerFunctionsR :: HandlerX String 184 | postHandlerFunctionsR = do 185 | maybeParameterValue <- lookupPostParam "param" 186 | maybeListParameterValues <- lookupPostParams "param" 187 | 188 | pure "handler function - post" 189 | -------------------------------------------------------------------------------- /20-yesod/src/Import.hs: -------------------------------------------------------------------------------- 1 | module Import 2 | ( module Import 3 | ) where 4 | 5 | import Foundation as Import 6 | import Import.NoFoundation as Import 7 | -------------------------------------------------------------------------------- /20-yesod/src/Import/NoFoundation.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | module Import.NoFoundation 3 | ( module Import 4 | ) where 5 | 6 | import ClassyPrelude.Yesod as Import 7 | import Model as Import 8 | import Settings as Import 9 | import Settings.StaticFiles as Import 10 | import Yesod.Auth as Import 11 | import Yesod.Core.Types as Import (loggerSet) 12 | import Yesod.Default.Config2 as Import 13 | -------------------------------------------------------------------------------- /20-yesod/src/Model.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | module Model where 5 | 6 | import ClassyPrelude.Yesod 7 | import Database.Persist.Quasi 8 | 9 | import Model.DataType 10 | 11 | -- You can define all of your database entities in the entities file. 12 | -- You can find more information on persistent and how to declare entities 13 | -- at: 14 | -- http://www.yesodweb.com/book/persistent/ 15 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] 16 | $(persistFileWith lowerCaseSettings "config/models") 17 | -------------------------------------------------------------------------------- /20-yesod/src/Model/DataType.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | module Model.DataType where 3 | 4 | import ClassyPrelude.Yesod 5 | import Database.Persist.TH () 6 | 7 | data DataType = DataType 8 | deriving (Show, Read, Eq, Ord) 9 | derivePersistField "DataType" 10 | -------------------------------------------------------------------------------- /20-yesod/src/Settings.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE TemplateHaskell #-} 5 | -- | Settings are centralized, as much as possible, into this file. This 6 | -- includes database connection settings, static file locations, etc. 7 | -- In addition, you can configure a number of different aspects of Yesod 8 | -- by overriding methods in the Yesod typeclass. That instance is 9 | -- declared in the Foundation.hs file. 10 | module Settings where 11 | 12 | import ClassyPrelude.Yesod 13 | import qualified Control.Exception as Exception 14 | import Data.Aeson (Result (..), fromJSON, withObject, 15 | (.!=), (.:?)) 16 | import Data.FileEmbed (embedFile) 17 | import Data.Yaml (decodeEither') 18 | import Database.Persist.Postgresql (PostgresConf) 19 | import Language.Haskell.TH.Syntax (Exp, Name, Q) 20 | import Network.Wai.Handler.Warp (HostPreference) 21 | import Yesod.Default.Config2 (applyEnvValue, configSettingsYml) 22 | import Yesod.Default.Util (WidgetFileSettings, 23 | widgetFileNoReload, 24 | widgetFileReload) 25 | 26 | -- | Runtime settings to configure this application. These settings can be 27 | -- loaded from various sources: defaults, environment variables, config files, 28 | -- theoretically even a database. 29 | data AppSettings = AppSettings 30 | { appStaticDir :: String 31 | -- ^ Directory from which to serve static files. 32 | , appDatabaseConf :: PostgresConf 33 | -- ^ Configuration settings for accessing the database. 34 | , appRoot :: Maybe Text 35 | -- ^ Base for all generated URLs. If @Nothing@, determined 36 | -- from the request headers. 37 | , appHost :: HostPreference 38 | -- ^ Host/interface the server should bind to. 39 | , appPort :: Int 40 | -- ^ Port to listen on 41 | , appIpFromHeader :: Bool 42 | -- ^ Get the IP address from the header when logging. Useful when sitting 43 | -- behind a reverse proxy. 44 | 45 | , appDetailedRequestLogging :: Bool 46 | -- ^ Use detailed request logging system 47 | , appShouldLogAll :: Bool 48 | -- ^ Should all log messages be displayed? 49 | , appReloadTemplates :: Bool 50 | -- ^ Use the reload version of templates 51 | , appMutableStatic :: Bool 52 | -- ^ Assume that files in the static dir may change after compilation 53 | , appSkipCombining :: Bool 54 | -- ^ Perform no stylesheet/script combining 55 | 56 | -- Example app-specific configuration values. 57 | , appCopyright :: Text 58 | -- ^ Copyright text to appear in the footer of the page 59 | , appAnalytics :: Maybe Text 60 | -- ^ Google Analytics code 61 | 62 | , appAuthDummyLogin :: Bool 63 | -- ^ Indicate if auth dummy login should be enabled. 64 | } 65 | 66 | instance FromJSON AppSettings where 67 | parseJSON = withObject "AppSettings" $ \o -> do 68 | let defaultDev = 69 | #ifdef DEVELOPMENT 70 | True 71 | #else 72 | False 73 | #endif 74 | appStaticDir <- o .: "static-dir" 75 | appDatabaseConf <- o .: "database" 76 | appRoot <- o .:? "approot" 77 | appHost <- fromString <$> o .: "host" 78 | appPort <- o .: "port" 79 | appIpFromHeader <- o .: "ip-from-header" 80 | 81 | dev <- o .:? "development" .!= defaultDev 82 | 83 | appDetailedRequestLogging <- o .:? "detailed-logging" .!= dev 84 | appShouldLogAll <- o .:? "should-log-all" .!= dev 85 | appReloadTemplates <- o .:? "reload-templates" .!= dev 86 | appMutableStatic <- o .:? "mutable-static" .!= dev 87 | appSkipCombining <- o .:? "skip-combining" .!= dev 88 | 89 | appCopyright <- o .: "copyright" 90 | appAnalytics <- o .:? "analytics" 91 | 92 | appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev 93 | 94 | return AppSettings {..} 95 | 96 | -- | Settings for 'widgetFile', such as which template languages to support and 97 | -- default Hamlet settings. 98 | -- 99 | -- For more information on modifying behavior, see: 100 | -- 101 | -- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile 102 | widgetFileSettings :: WidgetFileSettings 103 | widgetFileSettings = def 104 | 105 | -- | How static files should be combined. 106 | combineSettings :: CombineSettings 107 | combineSettings = def 108 | 109 | -- The rest of this file contains settings which rarely need changing by a 110 | -- user. 111 | 112 | widgetFile :: String -> Q Exp 113 | widgetFile = (if appReloadTemplates compileTimeAppSettings 114 | then widgetFileReload 115 | else widgetFileNoReload) 116 | widgetFileSettings 117 | 118 | -- | Raw bytes at compile time of @config/settings.yml@ 119 | configSettingsYmlBS :: ByteString 120 | configSettingsYmlBS = $(embedFile configSettingsYml) 121 | 122 | -- | @config/settings.yml@, parsed to a @Value@. 123 | configSettingsYmlValue :: Value 124 | configSettingsYmlValue = either Exception.throw id 125 | $ decodeEither' configSettingsYmlBS 126 | 127 | -- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@. 128 | compileTimeAppSettings :: AppSettings 129 | compileTimeAppSettings = 130 | case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of 131 | Error e -> error e 132 | Success settings -> settings 133 | 134 | -- The following two functions can be used to combine multiple CSS or JS files 135 | -- at compile time to decrease the number of http requests. 136 | -- Sample usage (inside a Widget): 137 | -- 138 | -- > $(combineStylesheets 'StaticR [style1_css, style2_css]) 139 | 140 | combineStylesheets :: Name -> [Route Static] -> Q Exp 141 | combineStylesheets = combineStylesheets' 142 | (appSkipCombining compileTimeAppSettings) 143 | combineSettings 144 | 145 | combineScripts :: Name -> [Route Static] -> Q Exp 146 | combineScripts = combineScripts' 147 | (appSkipCombining compileTimeAppSettings) 148 | combineSettings 149 | -------------------------------------------------------------------------------- /20-yesod/src/Settings/StaticFiles.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | module Settings.StaticFiles where 4 | 5 | import Settings (appStaticDir, compileTimeAppSettings) 6 | import Yesod.Static (staticFiles) 7 | 8 | -- This generates easy references to files in the static directory at compile time, 9 | -- giving you compile-time verification that referenced files exist. 10 | -- Warning: any files added to your static directory during run-time can't be 11 | -- accessed this way. You'll have to use their FilePath or URL to access them. 12 | -- 13 | -- For example, to refer to @static/js/script.js@ via an identifier, you'd use: 14 | -- 15 | -- js_script_js 16 | -- 17 | -- If the identifier is not available, you may use: 18 | -- 19 | -- StaticFile ["js", "script.js"] [] 20 | staticFiles (appStaticDir compileTimeAppSettings) 21 | -------------------------------------------------------------------------------- /20-yesod/stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | # 21 | # Jordan - Updated resolver to LTS-14.11 so that we use 22 | # more recent versions of libraries 23 | resolver: lts-14.12 24 | 25 | # User packages to be built. 26 | # Various formats can be used as shown in the example below. 27 | # 28 | # packages: 29 | # - some-directory 30 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | packages: 35 | - . 36 | # Dependency packages to be pulled from upstream that are not in the resolver. 37 | # These entries can reference officially published versions as well as 38 | # forks / in-progress versions pinned to a git hash. For example: 39 | # 40 | # extra-deps: 41 | # - acme-missiles-0.3 42 | # - git: https://github.com/commercialhaskell/stack.git 43 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 44 | # 45 | # extra-deps: [] 46 | 47 | # Override default flag values for local packages and extra-deps 48 | # flags: {} 49 | 50 | # Extra package databases containing global packages 51 | # extra-package-dbs: [] 52 | 53 | # Control whether we use the GHC we find on the path 54 | # system-ghc: true 55 | # 56 | # Require a specific version of stack, using version ranges 57 | # require-stack-version: -any # Default 58 | # require-stack-version: ">=2.1" 59 | # 60 | # Override the architecture used by stack, especially useful on Windows 61 | # arch: i386 62 | # arch: x86_64 63 | # 64 | # Extra directories used by stack for building 65 | # extra-include-dirs: [/path/to/dir] 66 | # extra-lib-dirs: [/path/to/dir] 67 | # 68 | # Allow a newer minor version of GHC than the snapshot specifies 69 | # compiler-check: newer-minor 70 | -------------------------------------------------------------------------------- /20-yesod/stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 545658 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/12.yaml 11 | sha256: 26b807457213126d26b595439d705dc824dbb7618b0de6b900adc2bf6a059406 12 | original: lts-14.12 13 | -------------------------------------------------------------------------------- /20-yesod/static/placeholder.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanMartinez/purescript-to-haskell/148632fef18c5ae9a4262a8934343a7966e89e01/20-yesod/static/placeholder.txt -------------------------------------------------------------------------------- /20-yesod/templates/placeholder.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanMartinez/purescript-to-haskell/148632fef18c5ae9a4262a8934343a7966e89e01/20-yesod/templates/placeholder.txt -------------------------------------------------------------------------------- /20-yesod/test/Handler/CommonSpec.hs: -------------------------------------------------------------------------------- 1 | module Handler.CommonSpec (spec) where 2 | 3 | import TestImport 4 | 5 | spec :: Spec 6 | spec = withApp $ do 7 | describe "robots.txt" $ do 8 | it "gives a 200" $ do 9 | get RobotsR 10 | statusIs 200 11 | it "has correct User-agent" $ do 12 | get RobotsR 13 | bodyContains "User-agent: *" 14 | describe "favicon.ico" $ do 15 | it "gives a 200" $ do 16 | get FaviconR 17 | statusIs 200 18 | -------------------------------------------------------------------------------- /20-yesod/test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /20-yesod/test/TestImport.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | module TestImport 4 | ( module TestImport 5 | , module X 6 | ) where 7 | 8 | import Application (makeFoundation, makeLogWare) 9 | import ClassyPrelude as X hiding (delete, deleteBy, Handler) 10 | import Database.Persist as X hiding (get) 11 | import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, rawSql, unSingle, connEscapeName) 12 | import Foundation as X 13 | import Model as X 14 | import Test.Hspec as X 15 | import Text.Shakespeare.Text (st) 16 | import Yesod.Default.Config2 (useEnv, loadYamlSettings) 17 | import Yesod.Auth as X 18 | import Yesod.Test as X 19 | import Yesod.Core.Unsafe (fakeHandlerGetLogger) 20 | 21 | runDB :: SqlPersistM a -> YesodExample App a 22 | runDB query = do 23 | app <- getTestYesod 24 | liftIO $ runDBWithApp app query 25 | 26 | runDBWithApp :: App -> SqlPersistM a -> IO a 27 | runDBWithApp app query = runSqlPersistMPool query (appConnPool app) 28 | 29 | runHandler :: Handler a -> YesodExample App a 30 | runHandler handler = do 31 | app <- getTestYesod 32 | fakeHandlerGetLogger appLogger app handler 33 | 34 | 35 | withApp :: SpecWith (TestApp App) -> Spec 36 | withApp = before $ do 37 | settings <- loadYamlSettings 38 | ["config/test-settings.yml", "config/settings.yml"] 39 | [] 40 | useEnv 41 | foundation <- makeFoundation settings 42 | wipeDB foundation 43 | logWare <- liftIO $ makeLogWare foundation 44 | return (foundation, logWare) 45 | 46 | -- This function will truncate all of the tables in your database. 47 | -- 'withApp' calls it before each test, creating a clean environment for each 48 | -- spec to run in. 49 | wipeDB :: App -> IO () 50 | wipeDB app = runDBWithApp app $ do 51 | tables <- getTables 52 | sqlBackend <- ask 53 | 54 | let escapedTables = map (connEscapeName sqlBackend . DBName) tables 55 | query = "TRUNCATE TABLE " ++ intercalate ", " escapedTables 56 | rawExecute query [] 57 | 58 | getTables :: DB [Text] 59 | getTables = do 60 | tables <- rawSql [st| 61 | SELECT table_name 62 | FROM information_schema.tables 63 | WHERE table_schema = 'public'; 64 | |] [] 65 | 66 | return $ map unSingle tables 67 | 68 | -- | Authenticate as a user. This relies on the `auth-dummy-login: true` flag 69 | -- being set in test-settings.yaml, which enables dummy authentication in 70 | -- Foundation.hs 71 | authenticateAs :: Entity User -> YesodExample App () 72 | authenticateAs (Entity _ u) = do 73 | request $ do 74 | setMethod "POST" 75 | addPostParam "ident" $ userIdent u 76 | setUrl $ AuthR $ PluginR "dummy" [] 77 | 78 | -- | Create a user. The dummy email entry helps to confirm that foreign-key 79 | -- checking is switched off in wipeDB for those database backends which need it. 80 | createUser :: Text -> YesodExample App (Entity User) 81 | createUser ident = runDB $ do 82 | user <- insertEntity User 83 | { userIdent = ident 84 | , userPassword = Nothing 85 | } 86 | _ <- insert Email 87 | { emailEmail = ident 88 | , emailUserId = Just $ entityKey user 89 | , emailVerkey = Nothing 90 | } 91 | return user 92 | -------------------------------------------------------------------------------- /21-servant/README.md: -------------------------------------------------------------------------------- 1 | # My-Servant 2 | 3 | Folder is still a WIP. 4 | 5 | Goals: 6 | - [Done] Document route syntax 7 | - [WIP] Document corresponding handler syntax 8 | 9 | 10 | ## Compilation instructions 11 | 12 | ```bash 13 | # See https://github.com/alphaHeavy/lzma-conduit/issues/17 14 | sudo apt-get install liblzma-dev 15 | 16 | stack setup 17 | stack build 18 | ``` 19 | -------------------------------------------------------------------------------- /21-servant/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /21-servant/app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import MinimalExample 4 | import ServantSyntax 5 | 6 | main :: IO () 7 | main = 8 | runStartApp 9 | -- runDiffMonad 10 | 11 | runStartApp :: IO () 12 | runStartApp = do 13 | putStrLn "Running startApp" 14 | startApp 15 | 16 | runDiffMonad :: IO () 17 | runDiffMonad = do 18 | putStrLn "Running differentMonadServer" 19 | webAppUsingDifferentMonad 20 | 21 | {- 22 | 23 | -- for 'runStartApp' 24 | curl http://localhost:8080/ 25 | 26 | -- for 'runDiffMonad' 27 | curl http://localhost:8080/simpleRoute 28 | -} 29 | -------------------------------------------------------------------------------- /21-servant/package.yaml: -------------------------------------------------------------------------------- 1 | name: my-servant 2 | version: 0.1.0.0 3 | github: "githubuser/my-servant" 4 | license: BSD3 5 | author: "Author name here" 6 | maintainer: "example@example.com" 7 | copyright: "2019 Author name here" 8 | 9 | extra-source-files: 10 | - README.md 11 | 12 | # Metadata used when publishing your package 13 | # synopsis: Short description of your package 14 | # category: Web 15 | 16 | # To avoid duplicated efforts in documentation and dealing with the 17 | # complications of embedding Haddock markup inside cabal files, it is 18 | # common to point users to the README.md file. 19 | description: Please see the README on GitHub at 20 | 21 | dependencies: 22 | - base 23 | 24 | # TH 25 | - safe # expose safer wrappers around unsafe Prelude functions 26 | - template-haskell 27 | 28 | # Core: Non Text 29 | - unordered-containers 30 | - containers 31 | - vector 32 | - time 33 | 34 | # Core: Text 35 | - bytestring 36 | - text 37 | - case-insensitive 38 | 39 | # Core: HTTP Types 40 | - http-types 41 | 42 | # Core: Effects 43 | - transformers 44 | - mtl 45 | 46 | # Domain: Seralization 47 | - aeson 48 | 49 | # API/Infrastructure: Database 50 | - persistent 51 | - persistent-postgresql 52 | - persistent-template 53 | 54 | # API: Web Application Interface 55 | - wai 56 | - wai-extra 57 | - wai-logger 58 | 59 | # Infrastructure: Web Server 60 | - warp 61 | - http-client-tls 62 | 63 | # Servant 64 | - servant 65 | 66 | - servant-auth 67 | 68 | - servant-docs 69 | - servant-auth-docs 70 | 71 | - servant-swagger 72 | - servant-swagger-ui 73 | - servant-auth-swagger 74 | 75 | - servant-server 76 | - servant-auth-server 77 | 78 | - servant-multipart 79 | 80 | library: 81 | source-dirs: src 82 | # Make developer experience similar to PureScript 83 | default-extensions: 84 | - KindSignatures 85 | - LiberalTypeSynonyms 86 | - EmptyDataDecls 87 | - OverloadedLists 88 | - OverloadedStrings 89 | - NumericUnderscores 90 | - NegativeLiterals 91 | - MultiParamTypeClasses 92 | - ConstrainedClassMethods 93 | - FlexibleInstances 94 | - FlexibleContexts 95 | - FunctionalDependencies 96 | - ConstraintKinds 97 | - InstanceSigs 98 | - DeriveFunctor 99 | - DeriveFoldable 100 | - DeriveTraversable 101 | - GeneralizedNewtypeDeriving 102 | - DerivingVia 103 | - MonoLocalBinds 104 | - PatternGuards 105 | - ScopedTypeVariables 106 | - RankNTypes 107 | - LambdaCase 108 | - BlockArguments 109 | - TypeApplications 110 | - RecordWildCards 111 | - NamedFieldPuns 112 | - ApplicativeDo 113 | - BangPatterns 114 | - PartialTypeSignatures 115 | - ExplicitNamespaces 116 | # Haskell-Only stuff 117 | - TemplateHaskell 118 | - PolyKinds 119 | 120 | ghc-options: 121 | - -Wall 122 | - -Wincomplete-uni-patterns 123 | - -Wincomplete-record-updates 124 | - -Wmissing-export-lists 125 | - -Widentities 126 | - -Wredundant-constraints 127 | - -Wpartial-fields 128 | - -Wcompat 129 | - -fprint-expanded-synonyms 130 | - -fprint-explicit-foralls 131 | - -ferror-spans 132 | - -fwarn-tabs 133 | - -O0 134 | 135 | executables: 136 | my-servant-exe: 137 | main: Main.hs 138 | source-dirs: app 139 | ghc-options: 140 | - -threaded 141 | - -rtsopts 142 | - -with-rtsopts=-N 143 | dependencies: 144 | - my-servant 145 | 146 | tests: 147 | my-servant-test: 148 | main: Spec.hs 149 | source-dirs: test 150 | ghc-options: 151 | - -threaded 152 | - -rtsopts 153 | - -with-rtsopts=-N 154 | dependencies: 155 | - my-servant 156 | - quickcheck 157 | - servant-quickcheck 158 | - hspec 159 | - hspec-wai 160 | - hspec-wai-json 161 | - aeson 162 | -------------------------------------------------------------------------------- /21-servant/src/MinimalExample.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeOperators #-} 4 | module MinimalExample 5 | ( startApp 6 | , webApp 7 | ) where 8 | 9 | import Data.Aeson 10 | import Data.Aeson.TH 11 | import Network.Wai 12 | import Network.Wai.Handler.Warp 13 | import Servant 14 | 15 | {- 16 | These routes were made to look more like Yesod's in the following ways: 17 | - Routes have an 'R' suffix to indicate they are a route 18 | - A route's corresponding handler is defined immediately below it 19 | -} 20 | 21 | -- GET / 22 | type HomeR = Get '[PlainText] String 23 | 24 | homeR :: Handler String 25 | homeR = pure "Hello world!" 26 | 27 | 28 | -- GET /foo 29 | type FooR = "foo" :> Get '[PlainText] String 30 | 31 | fooR :: Handler String 32 | fooR = pure "foo value" 33 | 34 | 35 | -- GET / 36 | -- GET /foo 37 | type Routes = HomeR :<|> FooR 38 | 39 | server :: Server Routes 40 | server = homeR :<|> fooR 41 | 42 | 43 | -- Helps type inference 44 | routesProxy :: Proxy Routes 45 | routesProxy = Proxy 46 | 47 | -- Defines the web application 48 | webApp :: Application 49 | webApp = serve routesProxy server 50 | 51 | -- Runs the web application using Warp 52 | startApp :: IO () 53 | startApp = run 8080 webApp 54 | -------------------------------------------------------------------------------- /21-servant/src/ServantSyntax.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeOperators #-} 4 | module ServantSyntax 5 | ( 6 | -- startApp 7 | -- app 8 | webAppUsingDifferentMonad 9 | ) where 10 | 11 | import Data.Aeson 12 | import Data.Aeson.TH 13 | import Control.Monad.Trans.Reader (ReaderT, runReaderT) 14 | import Control.Monad.Reader.Class 15 | import qualified Data.ByteString.Lazy as LBS 16 | import qualified Network.HTTP.Types.Header as HTTP 17 | import Data.Typeable (Typeable) 18 | import Network.Wai 19 | import Network.Wai.Handler.Warp 20 | import Servant 21 | import Data.List (foldl') 22 | 23 | -- ## Type Definitions for Servant 24 | 25 | newtype ExceptT_ error monad output 26 | = ExceptT_ (monad (Either error output)) 27 | -- ~ IO (Either error output) 28 | 29 | newtype Handler_ output 30 | = Handler (ExceptT_ ServerError IO output) 31 | -- ~ ExceptT_ (IO (Either ServerError output) 32 | -- ~ IO (Either ServerError output) 33 | 34 | -- Definition of ServerError with 'X' prefixing all of its named entities 35 | -- to prevent name clash in this file 36 | -- Rather than creating this each time, one should use the default values 37 | -- (e.g. `err500`) and override the specific part they need (e.g. add headers, 38 | -- change the reason phrase, add a body, etc.). 39 | data XServerError = XServerError 40 | { xerrHTTPCode :: Int 41 | , xerrReasonPhrase :: String 42 | , xerrBody :: LBS.ByteString 43 | , xerrHeaders :: [HTTP.Header] 44 | } 45 | deriving (Show, Eq, Read, Typeable) 46 | 47 | -- ## Servant's Type-Level Route Syntax 48 | 49 | -- ### Syntax for Specifying HTTP Method Used in Request 50 | 51 | -- #### Returning a Single Value 52 | 53 | -- The underscore is necessary here to prevent naming clashes with 54 | -- real type aliases used (i.e. read "Get_" and "Get") 55 | -- type Method_ = Verb Method ReturnedStatusCode 56 | type Get_ = Verb 'GET 200 57 | type Post_ = Verb 'POST 200 58 | type Put_ = Verb 'PUT 200 59 | type Delete_ = Verb 'DELETE 200 60 | 61 | type PostCreated = Verb 'POST 201 62 | type PostAccepted = Verb 'POST 202 63 | 64 | -- #### Returning a Stream of Values 65 | 66 | type StreamGet_ = Stream 'GET 200 67 | -- and same for other HTTP methods 68 | 69 | type ReturnType' = String 70 | 71 | -- 3 standard framing strategies 72 | 73 | type ContentType = PlainText 74 | 75 | type StreamingRouteNewlineFraming 76 | = StreamGet NewlineFraming ContentType (SourceIO ReturnType') 77 | 78 | type StreamingRouteNetstringFraming 79 | = StreamGet NetstringFraming ContentType (SourceIO ReturnType') 80 | 81 | type StreamingRouteNoFraming 82 | = StreamGet NoFraming ContentType (SourceIO ReturnType') 83 | 84 | -- ### Route Syntax 85 | 86 | type ReturnType = String 87 | 88 | -- GET request on the `/` path which returns a `ReturnType` in 4 89 | -- different ways (i.e. Content-Type header): 90 | -- PlainText, JSON, FormUrlEncoded, or an OctetStream 91 | type RootRoute = Get '[PlainText, JSON, FormUrlEncoded, OctetStream] ReturnType 92 | 93 | -- GET request on the `/` path which returns a stream of 94 | -- `ReturnType` values as PlainText. 95 | type RootRoute' = StreamGet NoFraming PlainText ReturnType 96 | 97 | rootRouteServer :: Server RootRoute 98 | rootRouteServer = pure "home" 99 | 100 | -- /singlePiece 101 | type SinglePathPieceRoute = "singlePiece" :> Get '[PlainText] ReturnType 102 | 103 | singlePathPieceR :: Server SinglePathPieceRoute 104 | singlePathPieceR = pure "single piece" 105 | 106 | -- Route acts like a "product type" 107 | -- /piece1/piece2 108 | type MultiPathPieceRoute = "piece1" :> "piece2" :> Get '[PlainText] ReturnType 109 | 110 | multiPathPieceR :: Server MultiPathPieceRoute 111 | multiPathPieceR = pure "multi piece" 112 | 113 | -- Route acts like a "sum type" 114 | -- /first 115 | -- /second 116 | type TwoPossibleRoutes 117 | = "first" :> Get '[PlainText] ReturnType 118 | :<|> "second" :> Get '[PlainText] ReturnType 119 | 120 | twoPossibleRoutes :: Server TwoPossibleRoutes 121 | twoPossibleRoutes = first :<|> second 122 | where 123 | first :: Handler ReturnType 124 | first = pure "first" 125 | 126 | second :: Handler ReturnType 127 | second = pure "second" 128 | 129 | 130 | -- Route acts like a tree: same parent piece with different children pieces 131 | -- /hierarchy/route1 132 | -- /hierarchy/route2 133 | -- /hierarchy/subhierarchy/routeA 134 | -- /hierarchy/subhierarchy/routeA 135 | type HierarchialRoute 136 | = "hierarchy" :> 137 | ( "route1" :> Get '[PlainText] String 138 | :<|> "route2" :> Get '[PlainText] String 139 | :<|> "subhierarchy" :> 140 | ( "routeA" :> Get '[PlainText] String 141 | :<|> "routeB" :> Get '[PlainText] String 142 | ) 143 | ) 144 | 145 | hierarchialRoute :: Server HierarchialRoute 146 | hierarchialRoute = route1 :<|> route2 :<|> routeA :<|> routeB 147 | where 148 | route1 :: Handler String 149 | route1 = pure "route 1" 150 | 151 | route2 :: Handler String 152 | route2 = pure "route 2" 153 | 154 | routeA :: Handler String 155 | routeA = pure "route A" 156 | 157 | routeB :: Handler String 158 | routeB = pure "route B" 159 | 160 | -- ### Syntax for Extracting Information from the Path 161 | 162 | -- /user/:userId (e.g. "/user/4" ) 163 | type UserIdCaptureRoute = "user" :> Capture "id" Int :> Get '[PlainText] String 164 | 165 | userIdCaptureRoute :: Server UserIdCaptureRoute 166 | userIdCaptureRoute = idRoute 167 | where 168 | idRoute :: Int -> Handler String 169 | idRoute a = pure (show a) 170 | 171 | 172 | -- /flagQuery -- "heavy" is false because route doesn't include it 173 | -- /flagQuery?heavy -- "heavy" is true because route includes it 174 | type SingleFlagQuery 175 | = "flagQuery" :> QueryFlag "heavy" :> Get '[PlainText] String 176 | 177 | singleFlagQuery :: Server SingleFlagQuery 178 | singleFlagQuery = handleRoute 179 | where 180 | handleRoute :: Bool -> Handler String 181 | handleRoute b = pure (show b) 182 | 183 | 184 | -- Various combinations 185 | -- /flagQuery?first=&second=&third= 186 | -- /flagQuery?first=&third= 187 | -- /flagQuery?second=&first=&third= 188 | type MultiFlagQuery 189 | = "flagQuery" 190 | :> QueryFlag "first" 191 | :> QueryFlag "second" 192 | :> QueryFlag "third" 193 | :> Get '[PlainText] String 194 | 195 | multiFlagQuery :: Server MultiFlagQuery 196 | multiFlagQuery = handleFlag 197 | where 198 | handleFlag :: Bool -> Bool -> Bool -> Handler String 199 | handleFlag first second third = 200 | pure $ show first <> " " <> show second <> " " <> show third 201 | 202 | type QueryParameterValue = String 203 | 204 | -- /singleKeyValueQuery?key=value1 205 | type SingleKeyValueQuery 206 | = "singleKeyValueQuery" 207 | :> QueryParam "key" QueryParameterValue :> Get '[PlainText] String 208 | 209 | singleKeyValueQuery :: Server SingleKeyValueQuery 210 | singleKeyValueQuery = handleRoute 211 | where 212 | handleRoute :: Maybe QueryParameterValue -> Handler String 213 | handleRoute keyValue = pure ("key value was: " <> show keyValue) 214 | 215 | type QueryParameterValue1 = String 216 | type QueryParameterValue2 = String 217 | type QueryParameterValue3 = String 218 | 219 | -- /multiKeyValueQuery?key1=value1&key2=value2&key3=value3 220 | type MultiKeyValueQuery 221 | = "multiKeyValueQuery" 222 | :> QueryParam "key1" QueryParameterValue1 223 | :> QueryParam "key2" QueryParameterValue2 224 | :> QueryParam "key3" QueryParameterValue3 225 | :> Get '[PlainText] String 226 | 227 | multiKeyValueQuery :: Server MultiKeyValueQuery 228 | multiKeyValueQuery = handleRoute 229 | where 230 | handleRoute 231 | :: Maybe QueryParameterValue1 232 | -> Maybe QueryParameterValue2 233 | -> Maybe QueryParameterValue3 234 | -> Handler String 235 | handleRoute value1 value2 value3 = pure (show value1 <> show value2 <> show value3) 236 | 237 | data ManyParameterValues 238 | = MpvValueA 239 | | MpvValueB 240 | | MpvValueC 241 | | MpvValueD 242 | deriving Show 243 | 244 | -- /singleKeyWithMultiValue?key={mpvvaluea, mpvvalueb, mpvvaluec, mpvvalued} 245 | type SingleKeyWithMultiValueQuery 246 | = "singleKeyWithMultiValue" 247 | :> QueryParams "key" ManyParameterValues :> Get '[PlainText] String 248 | 249 | singleKeyWithMultiValueQuery :: Server SingleKeyWithMultiValueQuery 250 | singleKeyWithMultiValueQuery = handleRoute 251 | where 252 | handleRoute :: [ManyParameterValues] -> Handler String 253 | handleRoute values = pure ("string" <> foldl' (\acc next -> acc <> show next) "" values) 254 | 255 | -- ### Syntax for Extracting Information from the Request 256 | 257 | -- #### Request Headers 258 | 259 | type DecodedRequestHeaderValue = String 260 | 261 | type RouteWithRequestHeader 262 | = "requestHeaders" :> Header "User-Agent" DecodedRequestHeaderValue 263 | :> Get '[PlainText] String 264 | 265 | routeWithRequestHeader :: Server RouteWithRequestHeader 266 | routeWithRequestHeader = handleRoute 267 | where 268 | handleRoute :: Maybe DecodedRequestHeaderValue -> Handler String 269 | handleRoute decodedHeaderValue = pure (show decodedHeaderValue <> " some stuff") 270 | 271 | -- #### Request Body 272 | 273 | type DecodedRequestBodyValue = String 274 | 275 | type DecodedRequestBody 276 | = "requestBody" :> ReqBody '[PlainText] DecodedRequestBodyValue 277 | :> Get '[PlainText] String 278 | 279 | decodedRequestBody :: Server DecodedRequestBody 280 | decodedRequestBody = handleRoute 281 | where 282 | handleRoute :: DecodedRequestBodyValue -> Handler String 283 | handleRoute decodedBody = pure (decodedBody <> " is some body") 284 | 285 | -- ### Syntax for Specifying Things in Response 286 | 287 | -- #### Response Headers 288 | 289 | type ResponseHeaderValue = Int 290 | 291 | type SingleResponseHeader 292 | = "singleResponseHeader" 293 | :> Get '[PlainText] 294 | ( Headers '[ Header "My-Header" ResponseHeaderValue ] ReturnType ) 295 | 296 | singleResponseHeader :: Server SingleResponseHeader 297 | singleResponseHeader = handleRoute 298 | where 299 | handleRoute :: Handler (Headers '[ Header "My-Header" ResponseHeaderValue ] ReturnType) 300 | handleRoute = pure (addHeader 5 "response body") 301 | 302 | type MultiResponseHeader 303 | = "multiResponseHeader" 304 | :> Get '[PlainText] ( Headers '[ Header "First-Header" ResponseHeaderValue 305 | , Header "Header-Two" ResponseHeaderValue 306 | ] 307 | ReturnType 308 | ) 309 | multiResponseHeader :: Server MultiResponseHeader 310 | multiResponseHeader = handleRoute 311 | where 312 | handleRoute :: Handler ( Headers '[ Header "First-Header" ResponseHeaderValue 313 | , Header "Header-Two" ResponseHeaderValue 314 | ] 315 | ReturnType 316 | ) 317 | handleRoute = pure (addHeader 10 $ addHeader 20 "response body") 318 | 319 | -- ### Creating Prefixes to Routes 320 | 321 | type PrefixedRoute innerRoute = "prefix" :> innerRoute 322 | type RemainingRoute = "inner" :> Get '[PlainText] String 323 | 324 | -- /prefix/inner 325 | type FullRoute = PrefixedRoute RemainingRoute 326 | 327 | -- /prefix/prefix 328 | type NoMoreRoutes = PrefixedRoute (PrefixedRoute EmptyAPI) 329 | 330 | -- ### Catch-All Match 331 | 332 | type TryDifferentRouter = 333 | "tryMeFirst" :> Get '[PlainText] String 334 | :<|> "tryMeSecond" :> Get '[PlainText] String 335 | :<|> Raw -- indicates anything that hasn't matched thus far 336 | -- should be handled as a WAI Application 337 | -- (e.g. another web application, static file server, etc.) 338 | 339 | tryDifferentRouter :: Server TryDifferentRouter 340 | tryDifferentRouter = tryFirst :<|> trySecond :<|> serveStaticFilesViaDirectory 341 | where 342 | tryFirst :: Handler String 343 | tryFirst = pure "first" 344 | 345 | trySecond :: Handler String 346 | trySecond = pure "second" 347 | 348 | -- Note: this type signature isn't Handler Raw, lest we get a compiler error. 349 | serveStaticFilesViaDirectory :: Server Raw 350 | serveStaticFilesViaDirectory = serveDirectoryFileServer "/var/www/" 351 | 352 | -- ## Modeling Complex Routes 353 | 354 | {- 355 | GET /foo/bar?key=value 356 | POST /foo/bar -- request header has custome header 357 | PUT /foo/bar -- request body stores JSON; response returns JSON 358 | DELETE /foo/bar/:id 359 | GET /path/to/static/file.html 360 | -} 361 | 362 | -- Modular Approach 363 | type FooBar remaining = 364 | ("foo" :> "bar" :> remaining) 365 | :<|> Raw 366 | type FooBarRoutes 367 | = (QueryParam "key" String :> Get '[PlainText] String) 368 | :<|> (Header "My-Header" Int :> Post '[PlainText] String) 369 | :<|> (ReqBody '[JSON] Int :> Put '[JSON] Int) 370 | :<|> (Capture "id" Int :> Delete '[JSON] Int) 371 | 372 | type FooBarFullRoute = FooBar FooBarRoutes 373 | 374 | {- 375 | -- Same as above 376 | 377 | GET /foo/bar?key=value 378 | POST /foo/bar -- request header has custom header 379 | PUT /foo/bar -- request body stores JSON; response returns JSON 380 | DELETE /foo/bar/:id 381 | GET /path/to/static/file.html 382 | -} 383 | 384 | -- Full approach 385 | type FooBarFullApproach = 386 | ("foo" :> "bar" :> 387 | ( (QueryParam "key" String :> Get '[PlainText] String) 388 | :<|> (Header "My-Header" Int :> Post '[PlainText] String) 389 | :<|> (ReqBody '[JSON] Int :> Put '[JSON] Int) 390 | :<|> (Capture "id" Int :> Delete '[JSON] Int) 391 | ) 392 | ) :<|> Raw 393 | 394 | fooBarFullApproach :: Server FooBarFullApproach 395 | fooBarFullApproach = 396 | ( getStringWithKeyParam 397 | :<|> postStringHeaderInt 398 | :<|> putJsonIntReqbodyJsonInt 399 | :<|> deleteIntCaptureInt 400 | ) 401 | :<|> serveStaticFiles 402 | where 403 | getStringWithKeyParam :: Maybe String -> Handler String 404 | getStringWithKeyParam value = pure (" and other stuff") 405 | 406 | postStringHeaderInt :: Maybe Int -> Handler String 407 | postStringHeaderInt intHeader = pure (show intHeader <> " and some other stuff") 408 | 409 | putJsonIntReqbodyJsonInt :: Int -> Handler Int 410 | putJsonIntReqbodyJsonInt decodedIntValue = pure decodedIntValue 411 | 412 | deleteIntCaptureInt :: Int -> Handler Int 413 | deleteIntCaptureInt capturedIntValue = pure capturedIntValue 414 | 415 | serveStaticFiles :: Server Raw -- Server, not Handler, for Raw 416 | serveStaticFiles = serveDirectoryFileServer "/var/www/" 417 | 418 | -- ## Full Syntax 419 | 420 | -- GET http://example.com/staticPathPiece/:pieceName?key=4&heavy 421 | -- Request-Header: headerValue 422 | -- 423 | -- ========== Server ========== 424 | -- 425 | -- Content-Type: text/plain 426 | -- Response-Header: responseHeaderValue 427 | -- 428 | -- value of 'ReturnType' encoded as plain text 429 | type RouteOrderAndPossibleSyntax = 430 | "staticPathPiece" 431 | :> Capture "pieceName" Int 432 | :> QueryParam "key" Int 433 | :> QueryFlag "heavy" 434 | :> Header "Request-Header" DecodedRequestHeaderValue 435 | :> ReqBody '[PlainText] DecodedRequestBodyValue 436 | :> Get '[PlainText] 437 | (Headers '[ Header "Response-Header" ResponseHeaderValue 438 | ] 439 | ReturnType) 440 | 441 | routeOrderAndPossibleSyntax :: Server RouteOrderAndPossibleSyntax 442 | routeOrderAndPossibleSyntax = handleRoute 443 | where 444 | handleRoute 445 | :: Int -> Maybe Int -> Bool -> Maybe DecodedRequestHeaderValue -> DecodedRequestBodyValue 446 | -> Handler (Headers '[ Header "Response-Header" ResponseHeaderValue 447 | ] 448 | ReturnType) 449 | handleRoute capturedInt maybeParamValue heavyFlag reqHeader reqBody = 450 | pure (addHeader 5 "request body") 451 | 452 | -- ## Using Template Haskell to derive JSON instances for a data type 453 | 454 | data User = User 455 | { userId :: Int 456 | , userFirstName :: String 457 | , userLastName :: String 458 | } deriving (Eq, Show) 459 | 460 | $(deriveJSON defaultOptions ''User) 461 | 462 | -- ## Using a Different Monad 463 | 464 | type SimpleRoute = "simpleRoute" :> Get '[PlainText] String 465 | 466 | -- Concrete type :: ServerT SimpleRoute (ReaderT String Handler output) 467 | -- a.k.a. :: ServerT SimpleRoute (String -> IO (Either ServerError output)) 468 | simpleRouteHandler :: forall m. MonadReader String m => ServerT SimpleRoute m 469 | simpleRouteHandler = do 470 | configString <- ask 471 | pure ("constant value " <> configString) 472 | 473 | simpleRouteProxy :: Proxy SimpleRoute 474 | simpleRouteProxy = Proxy 475 | 476 | differentMonadServer :: Application 477 | differentMonadServer = 478 | serve simpleRouteProxy 479 | (hoistServer simpleRouteProxy (flip runReaderT "config") simpleRouteHandler) 480 | 481 | -- i.e. 482 | -- (hoistServer simpleRouteProxy 483 | -- (\routeHandlerLogic -> runReaderT routeHandlerLogic "config") 484 | -- simpleRouteHandler 485 | -- ) 486 | 487 | webAppUsingDifferentMonad :: IO () 488 | webAppUsingDifferentMonad = run 8080 differentMonadServer 489 | -------------------------------------------------------------------------------- /21-servant/stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-14.12 21 | 22 | # User packages to be built. 23 | # Various formats can be used as shown in the example below. 24 | # 25 | # packages: 26 | # - some-directory 27 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 28 | # subdirs: 29 | # - auto-update 30 | # - wai 31 | packages: 32 | - . 33 | # Dependency packages to be pulled from upstream that are not in the resolver. 34 | # These entries can reference officially published versions as well as 35 | # forks / in-progress versions pinned to a git hash. For example: 36 | # 37 | # extra-deps: 38 | # - acme-missiles-0.3 39 | # - git: https://github.com/commercialhaskell/stack.git 40 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 41 | # 42 | # extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | # flags: {} 46 | 47 | # Extra package databases containing global packages 48 | # extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=2.1" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # compiler-check: newer-minor 67 | -------------------------------------------------------------------------------- /21-servant/stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 545658 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/12.yaml 11 | sha256: 26b807457213126d26b595439d705dc824dbb7618b0de6b900adc2bf6a059406 12 | original: lts-14.12 13 | -------------------------------------------------------------------------------- /21-servant/test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE BlockArguments #-} 4 | module Main (main) where 5 | 6 | import Lib (app) 7 | import Test.Hspec 8 | import Test.Hspec.Wai 9 | import Test.Hspec.Wai.JSON 10 | 11 | main :: IO () 12 | main = hspec spec 13 | 14 | spec :: Spec 15 | spec = with (pure webApp) do 16 | describe "GET /" do 17 | it "responds with 200" do 18 | get "/" `shouldRespondWith` 200 19 | describe "GET /foo" do 20 | it "responds with 200" do 21 | get "/foo" `shouldRespondWith` 200 22 | -------------------------------------------------------------------------------- /91-Language-Extensions.md: -------------------------------------------------------------------------------- 1 | # Language Extensions 2 | 3 | See [a Guide to GHC's Language Extensions](https://limperg.de/ghc-extensions/). 4 | 5 | ## Other Resources 6 | 7 | - The [Haskell Extensions Cheatsheet](https://impurepics.com/posts/2019-08-01-haskell-extensions.html), which appears to only cover the main extensions used 8 | - The [GHC User Manual's Language Extensions Section](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html) 9 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Purescript-to-Haskell 2 | 3 | ## Purpose of this Repository 4 | 5 | [Via the Feynman Technique](https://medium.com/taking-note/learning-from-the-feynman-technique-5373014ad230), this repository exists to help me understand how to use Haskell on the backend via Yesod or Servant with the goal of using PureScript on the front-end, and to help anyone else who is walking along a similar path. 6 | 7 | See the [Table of Contents file](https://github.com/JordanMartinez/purescript-to-haskell/blob/master/table-of-contents.md). 8 | 9 | ### Goals of this library 10 | 11 | - [Done] - Explain how to install Haskell and setup a developer environment 12 | - [Done] - Overview Haskell's REPL 13 | - [Done] - Show the similarities, differences, and gotchas between PureScript and Haskell Syntax 14 | - [Done] - Overview which language extensions one should enable to get a Haskell developer experience that is near enough to a PureScript experience and how that affects the syntax 15 | - [Done] - Cover the main differences between Stack and Cabal, the two main build tools for Haskell 16 | - Cover other gotchas PS developers may experience 17 | - Overview the dependencies used in Yesod and Servant (e.g. aeson, text, blaze, etc.) 18 | - Overview Yesod 19 | - Start with [Yesod for Haskellers](https://www.yesodweb.com/book/yesod-for-haskellers) to learn how it works without Template Haskell 20 | - Then read the rest of the book to understand how Template Haskell reduces the boilerplate one would normally write 21 | - Overview Servant 22 | - Don't use the [stable branch's tutorial](https://haskell-servant.readthedocs.io/en/stable/tutorial/) 23 | - Use the [master branch's tutorial](https://docs.servant.dev/en/master/tutorial/index.html) 24 | - Provide sample `.package.yml` file for Yesod and Servant 25 | 26 | ### Non-Goals of this library 27 | 28 | - Teach one how to use Haskell or the functional paradigm 29 | - Teach PureScript to a Haskell programmer 30 | - Rewrite what has already been written 31 | 32 | ## How I got Here 33 | 34 | I spent the previous year [learning PureScript using the Feynman Technique](https://github.com/jordanmartinez/purescript-jordans-reference), so that I could build websites and applications that 35 | 1. are much safer than what JavaScript and TypeScript can offer 36 | 2. can utilize more powerful language features and abstractions than what Elm can offer 37 | 3. and are easier to setup than GHCJS or alternatives 38 | 39 | While I could build a front-end using libraries like [purescript-halogen](https://github.com/slamdata/purescript-halogen)/[purescript-ocelot](https://github.com/citizennet/purescript-ocelot) or [purescript-react-basic](https://github.com/lumihq/purescript-react-basic), building a web server that interacts with such a front-end is a different matter. 40 | 41 | PureScript can be used to produce a Node.js web server. However, the two main contenders, [purescript-httpure](https://github.com/cprussin/purescript-httpure) and [hyper](https://github.com/purescript-hyper/hyper), are still immature in a few areas. Moreover, both of their contributors are busy focused on other things. So, it seems unlikely that these libraries will provide the "out-of-box ready" environment I want. 42 | 43 | However, JavaScript is not the only language to which PureScript can compile. It can also compile to languages such as C++, Go, and Kotlin (a recent backend that is still being developed). Unfortunately, none of these PureScript backends have a mature ecosystem for building a webserver. Moreover, supporting non-JavaScript backends in the PureScript compiler is still something that needs work. I believe this situation will improve in the next few years, but I unfortunately can't wait that long. 44 | 45 | After asking on the `#purescript` FP Slack channel, I was encouraged to consider using Haskell to build a web server using either the Yesod or Servant frameworks. Since Haskell is similar to PureScript in a number of ways, I believed this approach would achieve my goal in an easier manner than attempting to build a mature web server in PureScript via one of its backends. 46 | -------------------------------------------------------------------------------- /assets/Haskell-Numeric-Type-Class-Hierarchy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Num 22 | 23 | 24 | 25 | 26 | 27 | 28 | Ord 29 | 30 | 31 | 32 | 33 | 34 | 35 | Real 36 | 37 | 38 | 39 | 40 | 41 | 42 | Enum 43 | 44 | 45 | 46 | 47 | 48 | 49 | Integral 50 | 51 | 52 | 53 | 54 | 55 | 56 | Fractional 57 | 58 | 59 | 60 | 61 | 62 | 63 | Floating 64 | 65 | 66 | 67 | 68 | 69 | 70 | RealFrac 71 | 72 | 73 | 74 | 75 | 76 | 77 | RealFloat 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | --------------------------------------------------------------------------------