├── .dir-locals.el ├── .gitignore ├── README.org ├── complete.org ├── demo-emacs-haskell.cabal ├── demo.el ├── demo.org ├── emacs.d ├── custom.el └── init.el ├── package.yaml ├── src ├── Demo │ └── List.hs └── Main.hs ├── stack.yaml └── test ├── Demo └── ListSpec.hs ├── DocTest.hs └── Spec.hs /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((emacs-lisp-mode 2 | (flycheck-disabled-checkers emacs-lisp-checkdoc))) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.stack-work* 2 | TAGS 3 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Using Emacs with Haskell Projects 2 | 3 | * About 4 | 5 | A tutorial about using Emacs to develop Haskell projects. Only basic Haskell 6 | and Emacs knowledge is required. 7 | 8 | The tutorial attempts to cover several of the Emacs packages that one often 9 | uses when writing Haskell: 10 | 11 | - /company/ 12 | - /flycheck/ 13 | - /haskell-mode/ 14 | - /haskell-snippets/ 15 | - /helm/ 16 | - /hlint-refactor/ 17 | - /intero/ 18 | - /projectile/ 19 | - /use-package/ 20 | - /which-key/ 21 | - /yasnippet/ 22 | 23 | If you are new to Emacs, go through its tutorial first (press =control+h= 24 | followed by =t=). 25 | 26 | This was originally presented for the "PDX Emacs Hackers" group. Some of the 27 | people attending were not Haskellers, hence the sparse section on basic 28 | Haskell. 29 | 30 | * Setup 31 | 32 | *** Haskell Tools 33 | 34 | 1. Install [[https://docs.haskellstack.org/en/stable/install_and_upgrade/][Stack]] 35 | 36 | 2. Install sandboxed GHC and dependencies 37 | 38 | #+BEGIN_SRC sh 39 | stack setup 40 | stack build --dependencies-only --test 41 | #+END_SRC 42 | 43 | 3. Install tools 44 | 45 | #+BEGIN_SRC sh 46 | stack build intero 47 | stack install apply-refact codex hasktags hlint 48 | codex set format emacs 49 | #+END_SRC 50 | 51 | This might take a while. 52 | 53 | *** Emacs 54 | 55 | 1. Move any preexisting Emacs configurations 56 | 57 | #+BEGIN_SRC sh 58 | for f in ~/.emacs*; do 59 | if [ -e "$f" ]; then 60 | echo "Moving config to $f.orig" 61 | mv "$f" "$f.orig" 62 | fi 63 | done 64 | #+END_SRC 65 | 66 | 2. Use this project's configuration 67 | 68 | #+BEGIN_SRC sh 69 | cp -r emacs.d ~/.emacs.d 70 | #+END_SRC 71 | 72 | 3. (Re)start Emacs 73 | 74 | It will take up to a couple of minutes for Emacs to download and compile 75 | all dependencies. 76 | 77 | 4. Follow the [[file:demo.org][tutorial]] 78 | -------------------------------------------------------------------------------- /complete.org: -------------------------------------------------------------------------------- 1 | * =src/Main.hs= 2 | #+BEGIN_SRC haskell 3 | module Main where 4 | 5 | import System.Environment (getArgs) 6 | import System.Exit 7 | 8 | import Demo.List 9 | 10 | main :: IO () 11 | main = do 12 | args <- getArgs 13 | case args of 14 | "car":xs -> 15 | case car (fromList xs) of 16 | Nothing -> putStrLn "" 17 | Just a -> putStrLn a 18 | _ -> do 19 | putStrLn "Usage: demo-list [text]..." 20 | exitFailure 21 | #+END_SRC 22 | 23 | * =src/Demo/List.hs= 24 | #+BEGIN_SRC haskell 25 | {-# LANGUAGE DeriveFoldable #-} 26 | module Demo.List where 27 | 28 | data List a = Cons a (List a) | Nil 29 | deriving (Eq, Foldable, Show) 30 | 31 | -- | Returns the first element, if non-empty. 32 | -- 33 | -- >>> car Nil 34 | -- Nothing 35 | -- 36 | -- >>> car (Cons 'a' Nil) 37 | -- Just 'a' 38 | car :: List a -> Maybe a 39 | car xs = case xs of 40 | Nil -> Nothing 41 | Cons x _ -> Just x 42 | 43 | cdr :: List a -> List a 44 | cdr Nil = Nil 45 | cdr (Cons _ xs) = xs 46 | 47 | fromList = 48 | foldr Cons Nil 49 | #+END_SRC 50 | 51 | * =test/Demo/ListSpec.hs= 52 | #+BEGIN_SRC haskell 53 | {-# LANGUAGE TypeApplications #-} 54 | 55 | module Demo.ListSpec where 56 | 57 | import Test.Hspec 58 | 59 | import Demo.List 60 | 61 | main = hspec spec 62 | 63 | spec = parallel $ do 64 | describe "List" $ do 65 | describe "cdr" $ do 66 | it "returns the tail" $ do 67 | let xs = Cons 'b' (Cons 'c' Nil) 68 | cdr (Cons 'a' xs) `shouldBe` xs 69 | 70 | it "returns Nil for empty lists" $ 71 | cdr Nil `shouldBe` Nil @Char 72 | #+END_SRC 73 | -------------------------------------------------------------------------------- /demo-emacs-haskell.cabal: -------------------------------------------------------------------------------- 1 | -- This file has been generated from package.yaml by hpack version 0.17.0. 2 | -- 3 | -- see: https://github.com/sol/hpack 4 | 5 | name: demo-emacs-haskell 6 | version: 0.0.0 7 | homepage: https://github.com/cydparser/demo-emacs-haskell#readme 8 | bug-reports: https://github.com/cydparser/demo-emacs-haskell/issues 9 | build-type: Simple 10 | cabal-version: >= 1.10 11 | 12 | source-repository head 13 | type: git 14 | location: https://github.com/cydparser/demo-emacs-haskell 15 | 16 | executable demo-list 17 | main-is: Main.hs 18 | hs-source-dirs: 19 | src 20 | default-extensions: NoMonomorphismRestriction 21 | ghc-options: -Wall -Wno-missing-signatures -Wno-type-defaults -Wno-unused-do-bind -threaded -rtsopts -with-rtsopts=-N 22 | build-depends: 23 | base >= 4.7 && < 5 24 | other-modules: 25 | Demo.List 26 | default-language: Haskell2010 27 | 28 | test-suite doctest 29 | type: exitcode-stdio-1.0 30 | main-is: DocTest.hs 31 | hs-source-dirs: 32 | test 33 | default-extensions: NoMonomorphismRestriction 34 | ghc-options: -Wall -Wno-missing-signatures -Wno-type-defaults -Wno-unused-do-bind -threaded -rtsopts -with-rtsopts=-N 35 | build-depends: 36 | base >= 4.7 && < 5 37 | , QuickCheck 38 | , doctest 39 | other-modules: 40 | Demo.ListSpec 41 | Spec 42 | default-language: Haskell2010 43 | 44 | test-suite spec 45 | type: exitcode-stdio-1.0 46 | main-is: Spec.hs 47 | hs-source-dirs: 48 | src 49 | test 50 | default-extensions: NoMonomorphismRestriction 51 | ghc-options: -Wall -Wno-missing-signatures -Wno-type-defaults -Wno-unused-do-bind -threaded -rtsopts -with-rtsopts=-N 52 | build-depends: 53 | base >= 4.7 && < 5 54 | , QuickCheck 55 | , hspec 56 | , hspec-discover 57 | other-modules: 58 | Demo.List 59 | Main 60 | Demo.ListSpec 61 | DocTest 62 | default-language: Haskell2010 63 | -------------------------------------------------------------------------------- /demo.el: -------------------------------------------------------------------------------- 1 | (require 'demo-it) 2 | 3 | (demo-it-create 4 | :advance-mode 5 | :full-screen 6 | :insert-medium 7 | (demo-it-presentation "demo.org")) 8 | -------------------------------------------------------------------------------- /demo.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Using Emacs with Haskell Projects 2 | 3 | * Introduction 4 | 5 | ** Meta 6 | 7 | Available on GitHub: https://github.com/cydparser/demo-emacs-haskell 8 | 9 | #+BEGIN_SRC text 10 | ------- ------- 11 | --/ \-- --/ \-- 12 | -/ -/ \- 13 | / / \ \ 14 | | | | | 15 | / / \ \ 16 | | <3 Haskell | Me | <3 Emacs | 17 | \ \ / / 18 | | | | | 19 | \ \ / / 20 | -\ -\ /- 21 | --\ /-- --\ /-- 22 | ------- ------- 23 | #+END_SRC 24 | 25 | ** [[https://github.com/jwiegley/use-package][use-package]] 26 | 27 | The [[./emacs.d/init.el][init]] file included with this tutorial uses /use-package/ to install and 28 | configure packages. 29 | 30 | + View custom keybindings: =M-x describe-personal-keybindings= 31 | 32 | ** [[https://github.com/justbur/emacs-which-key][which-key]] 33 | 34 | The package /which-key/ displays available keybindings automatically in the 35 | minibuffer. Typing the start of a command and waiting a second will result in 36 | a pop-up listing commands that begin with the same prefix. 37 | 38 | *Do*: view a /which-key/ pop-up. 39 | 40 | 1. Type =C-c p= and wait one second for the pop-up 41 | 42 | 2. Press =C-g= to close the buffer and cancel the command 43 | 44 | * Haskell 45 | 46 | Haskell is a purely functional, lazy, statically typed language. 47 | 48 | + Skip to [[The Project]] if you are familiar with Haskell 49 | 50 | ** Similarities to Emacs Lisp 51 | 52 | + Expressions rather than statements 53 | 54 | #+BEGIN_SRC haskell 55 | -- Only definitions are allowed at the top-level. 56 | x = if expr 57 | then "something" 58 | else "other thing" 59 | #+END_SRC 60 | 61 | + First-class Functions and Lambdas 62 | 63 | Functions, lambdas, and curried functions are all called in the same way -- 64 | there is not an equivalent of #', ~apply~, ~apply-partially~, and ~funcall~. 65 | 66 | #+BEGIN_SRC haskell 67 | -- A function definition. 68 | plus1 n = n + 1 69 | 70 | -- + lambda 71 | -- v 72 | div2 = \n -> n / 2 73 | 74 | thenDiv2 f = -- Functions can be passed as arguments 75 | f . div2 -- and returned. 76 | 77 | -- `.` is for function composition. It is an ordinary function. 78 | 79 | main = print (thenDiv2 plus1 10) 80 | -- 6 81 | #+END_SRC 82 | 83 | + Let Expressions 84 | 85 | #+BEGIN_SRC haskell 86 | func x = 87 | let y = x + 1 88 | f a = a * 10 -- Local function 89 | in f y 90 | 91 | -- Alternatively: 92 | func' x = 93 | f y 94 | where 95 | y = x + 1 96 | f a = a * 10 97 | #+END_SRC 98 | 99 | + REPL (GHCi) 100 | 101 | ** Statically Typed with Type Inference 102 | 103 | Writing type signatures is usually optional. 104 | 105 | #+BEGIN_SRC haskell 106 | -- + "has type" 107 | -- | + a list of type `a` values 108 | -- | | + the type constructor of functions (domain -> range) 109 | -- | | | + concrete type 110 | -- v v v v 111 | length' :: [a] -> Int 112 | length' xs = length xs 113 | 114 | -- + constraint: `a` has an instance of Num 115 | -- | 116 | -- v--------v 117 | plus :: (Num a) => a -> a -> a 118 | plus x y = x + y 119 | #+END_SRC 120 | 121 | ** Purely Functional 122 | 123 | + Functions behave like mathematical functions (input -> output) 124 | 125 | A function has at least one argument and must return a value. 126 | 127 | + Referential transparency 128 | 129 | An expression can be replaced by the value without changing the 130 | computation. 131 | 132 | ** Lazy (by default) 133 | 134 | + Call by need 135 | 136 | If-then-else can be implemented without special operations. 137 | 138 | #+BEGIN_SRC haskell 139 | ifThenElse :: Bool -> a -> a -> a 140 | ifThenElse cond expr0 expr1 = 141 | case cond of 142 | True -> expr0 143 | False -> expr1 144 | #+END_SRC 145 | 146 | + Infinite lists are useful 147 | 148 | #+BEGIN_SRC haskell 149 | threeEven = 150 | take 3 (map (* 2) [0..]) 151 | -- [0,2,4] 152 | #+END_SRC 153 | 154 | ** Immutable (by default) 155 | 156 | + Values are bound to names rather than assigned to variables 157 | 158 | + Only one function to test for equality: ~==~ 159 | 160 | No need to worry about whether the pointers reference the same location. 161 | 162 | ** No Nil or Null 163 | 164 | + The possibility for the absence of a value is expressed in the type 165 | 166 | #+BEGIN_SRC haskell 167 | -- data Maybe a = Just a | Nothing 168 | 169 | noInt :: Maybe Int 170 | noInt = Nothing 171 | #+END_SRC 172 | 173 | ** IO Actions 174 | 175 | + Using IO changes the type of a function 176 | 177 | #+BEGIN_SRC haskell 178 | f :: Int -> IO Int 179 | f i = do 180 | print i 181 | pure (i + 1) 182 | #+END_SRC 183 | 184 | An ~IO a~ is a recipe for an action that can be passed around and composed 185 | without performing the action. Only actions that are assigned to ~main~ can 186 | eventually be performed. 187 | 188 | * The Project 189 | 190 | We will build a simple executable that manipulates lists. 191 | 192 | #+BEGIN_SRC sh 193 | stack exec -- demo-list cdr 1 2 3 194 | # 2 3 195 | #+END_SRC 196 | 197 | ** [[http://projectile.readthedocs.io/en/latest/][Projectile]] 198 | 199 | Provides project navigation, building, testing, etc. 200 | 201 | + Keybindings (small subset) 202 | 203 | - =C-c p != =projectile-run-shell-command-in-root= 204 | - =C-c p R= =projectile-regenerate-tags= 205 | - =C-c p S= =projectile-save-project-buffers= 206 | - =C-c p b= =projectile-switch-to-buffer= 207 | - =C-c p c= =projectile-compile-project= 208 | - =C-c p f= =projectile-find-file= 209 | - =C-c p j= =projectile-find-tag= 210 | - =C-c p k= =projectile-kill-buffers= 211 | - =C-c p t= =projectile-toggle-between-implementation-and-test= 212 | 213 | Press =C-c p= and wait a second to see the full list. It is worth spending 214 | time playing with the many commands. 215 | 216 | + Commands with prefix =C-c p 4= perform some action in a different pane 217 | without, splitting the current pane if necessary 218 | 219 | - =C-c p 4 f= =projectile-find-file-other-window= 220 | - =C-c p 4 t= =projectile-find-implementation-or-test-other-window= 221 | 222 | *Do*: open /List.hs/ in a different frame. 223 | 224 | 1. Type =C-c p 4 f= 225 | 226 | A different pane will display all files in this project. 227 | 228 | 2. Start typing =List.hs= 229 | 230 | 3. Press =RET= when src/demo/List.hs is highlighted 231 | 232 | ** [[https://emacs-helm.github.io/helm/][Helm]] 233 | 234 | The list of project files in the previous section was a /helm/ buffer. Helm 235 | allows one to use fuzzy searching among other (perhaps too many) things. 236 | 237 | + Uses normal Emacs navigation bindings (=C-n=, =C-p=, =M->= etc.) 238 | 239 | + Press =RET= to choose the highlighted selection 240 | 241 | + Standard Emacs keybindings overridden for this tutorial 242 | 243 | - =C-x C-f= ~helm-find-files~ 244 | - =C-h a= ~helm-apropos~ 245 | - =C-x b= ~helm-mini~ 246 | - =M-s o= ~helm-occur~ 247 | - =M-x= ~helm-M-x~ 248 | 249 | + Use =C-j= when you would instinctively use =tab= 250 | 251 | *Do*: play with /helm/. 252 | 253 | 1. Press =M-x= 254 | 255 | 2. Type arbitrary keys and watch as the list updates 256 | 257 | 3. Press =C-g= when done 258 | 259 | ** [[https://commercialhaskell.github.io/intero/][Intero]] 260 | 261 | + Back-End process that enables: 262 | 263 | - On the fly type checking 264 | - Code completion 265 | - Jumping to definitions 266 | - Displaying type of thing at point 267 | - And more 268 | 269 | + Installed once per Haskell project (not globally) 270 | 271 | Opening /List.hs/ will either result in: 272 | 273 | #+BEGIN_SRC text 274 | Booting up intero ... 275 | #+END_SRC 276 | 277 | Or, if Intero was not previously installed: 278 | 279 | #+BEGIN_SRC text 280 | Installing intero-x.y.z automatically ... 281 | #+END_SRC 282 | 283 | It might take a couple of minutes to build Intero. 284 | 285 | + View running Intero back-ends with =M-x intero-list-buffers= 286 | 287 | - =p= move up 288 | - =n= move down 289 | - =d= mark a buffer for deletion 290 | - =x= delete marked buffers 291 | 292 | + Keybindings 293 | 294 | - =C-c C-i= ~intero-info~ 295 | - =C-c C-l= ~intero-repl-load~ 296 | - =C-c C-r= ~intero-apply-suggestions~ 297 | - =C-c C-t= ~intero-type-at~ 298 | - =C-c C-z= ~intero-repl~ 299 | - =M-.= ~intero-goto-definition~ 300 | 301 | *Do*: view running process list. 302 | 303 | 1. Type =M-x intero-list-buffers= and press =RET= 304 | 305 | 2. Press =d= after selecting the only process 306 | 307 | 3. Press =x= to kill the process 308 | 309 | 4. Press =q= to hide the now empty list 310 | 311 | 5. Type =M-x intero-restart= to start a new process 312 | 313 | ** [[https://github.com/haskell/haskell-snippets#available-expansion-keys][haskell-snippets]] 314 | 315 | [[https://github.com/joaotavora/yasnippet][Yasnippet]] provides Emacs with templates. /haskell-snippets/ adds templates for 316 | /haskell-mode/. 317 | 318 | + Especially useful templates 319 | 320 | - =mod= adds a named (based on filepath) module declaration 321 | - =main= adds a ~Main~ module and ~main~ function 322 | - =lang= adds a LANGUAGE pragma 323 | - =opt= adds a OPTIONS_GHC pragma 324 | 325 | *Do*: name the module in /List.hs/ using a snippet. 326 | 327 | 1. Type =mod= and press =M-/= (~hippie-expand~) 328 | 329 | A /helm/ buffer will appear with a list of options. 330 | 331 | 2. Select "simple module" and press =RET= 332 | 333 | 3. Press =tab= to accept the default module name 334 | 335 | ** ~List a~ 336 | 337 | *Do:* add the following code to /List.hs/. 338 | 339 | #+BEGIN_SRC haskell 340 | data List a = Cons a (List a) | Nil 341 | deriving (Eq, Show) 342 | #+END_SRC 343 | 344 | ** Search by Type 345 | 346 | Functions can be searched for by name or by type. 347 | 348 | + =M-x hayoo= Search using [[http://hayoo.fh-wedel.de/][Hayoo!]] 349 | 350 | + =M-x hoogle= Search using [[https://www.haskell.org/hoogle/][Hoogle]] 351 | 352 | *Do*: find a function that takes a collection and indicates whether it is empty. 353 | 354 | 1. Type =M-x hayoo= and press =RET= 355 | 356 | 2. Type ~f a -> Bool~ and press =RET= 357 | 358 | Your browser (or Emacs' browser) should open with ~null~ from ~Data.Foldable~ 359 | at the top of the search results. 360 | 361 | ** [[http://www.flycheck.org/en/latest/][Flycheck]] 362 | 363 | Intero uses Flycheck to indicate warnings and errors. 364 | 365 | + Flycheck uses prefix =C-c != 366 | 367 | - =C-c ! l= ~flycheck-list-errors~ 368 | - =C-c ! p= ~flycheck-previous-error~ 369 | - =C-c ! n= ~flycheck-next-error~ 370 | 371 | + =C-c != is both awkward to say and to type 372 | 373 | We'll use these custom bindings instead: 374 | 375 | - =M-p= ~flycheck-previous-error~ 376 | - =M-n= ~flycheck-next-error~ 377 | 378 | *Do*: derive ~Foldable~. 379 | 380 | ~null~ is the function that we want, but it requires that ~List~ have an instance 381 | for ~Foldable~. We could write the ~Foldable~ instance ourselves, but the 382 | compiler can implement it for us. 383 | 384 | 1. Add ~Foldable~ to the ~deriving~ tuple 385 | 386 | #+BEGIN_SRC haskell 387 | deriving (Eq, Foldable, Show) 388 | #+END_SRC 389 | 390 | ~Foldable~ should be underlined with a red, squiggly line, indicating an 391 | error. 392 | 393 | 2. Press =M-p= to move to the error 394 | 395 | The compiler has told us that, "You need DeriveFoldable to derive an 396 | instance for this class." Intero is able to apply some of the compiler's 397 | suggestions, and will tell us to press =C-c C-r= if the error can be 398 | automatically fixed. 399 | 400 | 3. View list of suggestions: =C-c C-r= 401 | 402 | A new buffer will pop up asking us to choose which suggestions to 403 | apply. There is only one option in this case, so it is already 404 | checked. Pressing enter on an option will toggle whether it is checked. 405 | 406 | 4. Apply the suggestion: =C-c C-c= 407 | 408 | ** Viewing Types and Info 409 | 410 | Intero uses /eldoc/ to automatically show type signatures in the minibuffer. 411 | 412 | + The type can be manually shown if /eldoc/ is disabled: =C-c C-t= 413 | 414 | + The /info/ for infix operators includes associativity 415 | 416 | E.g. looking up the info on ~+~ 417 | 418 | #+BEGIN_SRC haskell 419 | infixl 6 + 420 | #+END_SRC 421 | 422 | *Do*: view the type of ~Cons~. 423 | 424 | 1. Move the cursor onto ~Cons~ 425 | 426 | Notice the type signature at the bottom-left of Emacs. 427 | 428 | + Type classes and functions have additional displayable information 429 | 430 | *Do*: view info about ~Foldable~. 431 | 432 | 1. Move the cursor onto ~Foldable~ and press =C-c C-i= 433 | 434 | ** Implement ~car~ 435 | 436 | *Do*: add the definition of ~car~. 437 | 438 | #+BEGIN_SRC haskell 439 | -- | Returns the first element, if non-empty. 440 | -- 441 | -- >>> car Nil 442 | -- Nothing 443 | -- 444 | -- >>> car (Cons 'a' Nil) 445 | -- Just 'a' 446 | car :: List a -> Maybe a 447 | car xs = case xs of 448 | Nil -> Nothing 449 | Cons x _ -> Just x 450 | #+END_SRC 451 | 452 | ** Running Tests 453 | 454 | The =-- >>>= bits are [[https://github.com/sol/doctest#doctest-test-interactive-haskell-examples][Doctest]] examples. Projectile provides =C-c p P= for running 455 | all tests by default. We can run just the tests in this file by modifying the 456 | command. First, we need to add a ~main~ method to =Main.hs= so that the project 457 | can compile. 458 | 459 | *Do*: use the =main= template. 460 | 461 | 1. Open /Main.hs/: =C-c p f Main.hs= 462 | 463 | 2. Type =main= and press =M-/= 464 | 465 | 3. Select "main module" and press =RET= 466 | 467 | 4. Save modified project files: =C-c p S= 468 | 469 | 5. Switch back to /List.hs/: =C-c p b List.hs= 470 | 471 | *Do*: run Doctest only on /List.hs/. 472 | 473 | 1. Type =C-c p P= 474 | 475 | 2. Append the following to the test command after a space: 476 | 477 | #+BEGIN_SRC text 478 | :doctest --test-arguments src/Demo/List.hs 479 | #+END_SRC 480 | 481 | A compilation buffer will show the test results. Projectile will remember 482 | the modified command for future runs. 483 | 484 | 3. Press =q= to close the results 485 | 486 | + See [[./test/DocTest.hs]] for how to enable testing a single file in other 487 | projects 488 | 489 | ** REPL 490 | 491 | Intero provides a basic REPL. 492 | 493 | + Keybindings 494 | 495 | - =C-c C-l= load the current buffer into the REPL 496 | - =C-c C-z= switch back and forth between REPL and the module 497 | - =M-n= next input 498 | - =M-p= previous input 499 | 500 | *Do*: load /List.hs/ into the REPL 501 | 502 | 1. =C-c C-l= to switch to the REPL 503 | 504 | 2. Type ~car (Cons 'a' (Cons 'b' Nil))~ and press =RET= 505 | 506 | You should see: 507 | 508 | #+BEGIN_SRC haskell 509 | Just 'a' 510 | #+END_SRC 511 | 512 | ** Implement ~cdr~ 513 | 514 | Sometimes it is useful to implement a function without specifying its type, 515 | and later to insert the inferred type. 516 | 517 | *Do*: insert the inferred type. 518 | 519 | 1. Implement =cdr= 520 | 521 | #+BEGIN_SRC haskell 522 | cdr Nil = Nil 523 | cdr (Cons _ xs) = xs 524 | #+END_SRC 525 | 526 | 2. Move onto the first occurrence of =cdr= 527 | 528 | 3. Insert the type signature: =C-u C-c C-t= 529 | 530 | The above command should have added a line containing: 531 | 532 | #+BEGIN_SRC haskell 533 | cdr :: List a -> List a 534 | #+END_SRC 535 | 536 | ** Jump to Test Files 537 | 538 | Projectile can jump to a file's corresponding test file, creating a new file 539 | if one does not exist. 540 | 541 | *Do*: jump to /ListSpec.hs/. 542 | 543 | 1. From /List.hs/: =C-c p t= 544 | 545 | The first ~import~ will be marked as an error. This is due to Intero only 546 | loading dependencies of the library initially. The tests are a separate 547 | Stack "target", with their own dependencies. 548 | 549 | 2. Add the =:spec= target: =M-x intero-targets= 550 | 551 | A buffer will appear with a list of three targets. 552 | 553 | 3. Move the cursor to the target ending with =:test:spec= and press =RET= 554 | 555 | 4. =C-c C-c= to apply the change 556 | 557 | The import error should go away. 558 | 559 | ** Implement and Run Specs 560 | 561 | *Do*: 562 | 563 | 1. Replace ~undefined~ with the following 564 | 565 | #+BEGIN_SRC haskell 566 | it "returns the tail" $ do 567 | let xs = Cons 'b' (Cons 'c' Nil) 568 | cdr (Cons 'a' xs) `shouldBe` xs 569 | 570 | it "returns Nil for empty lists" $ 571 | cdr Nil `shouldBe` Nil 572 | #+END_SRC 573 | 574 | The last use of ~cdr~ should be marked as an error due to an "ambiguous type 575 | variable." This is caused by ~shouldBe~'s constraint of ~(Show a, Eq a)~ and 576 | the ~Nil~'s inferred type of ~List a~ (no constraints). 577 | 578 | 2. At the top of the file write =lang= and press =M-/= 579 | 580 | 3. Type =typ app=, select =TypeApplications=, and press =RET= 581 | 582 | 4. Add ~@Char~ after the last ~Nil~ 583 | 584 | 5. Run the tests: =C-c p P= 585 | 586 | Change the test command to =stack build --test :spec= to run only Hspec 587 | tests. A compilation buffer will display the results. 588 | 589 | ** Imports 590 | 591 | + Keybindings 592 | 593 | - =M-g i= ~haskell-navigate-imports~ (custom) 594 | 595 | Also bound to =M-g M-i=. 596 | 597 | Pressing =C-u= before =M-g i= will return the cursor to where it was before 598 | jumping. 599 | 600 | Pressing =M-g i= multiple times will cycle through import sections if any 601 | are separated by blank lines. 602 | 603 | - C-c C-, =haskell-mode-format-imports= 604 | 605 | Both aligns and orders imports. 606 | 607 | *Do*: 608 | 609 | 1. Jump to /Main.hs/: =C-c p b main= and press =RET= 610 | 611 | 2. Add the executable target to intero: =M-x intero-targets= 612 | 613 | Mark the line ending with =:exe:demo-list= and press =C-c C-c.= 614 | 615 | 3. Replace ~main~ with the following 616 | 617 | #+BEGIN_SRC haskell 618 | import System.Environment (getArgs) 619 | 620 | import Demo.List 621 | 622 | main :: IO () 623 | main = do 624 | args <- getArgs 625 | case args of 626 | "car":xs -> 627 | case car (undefined xs) of 628 | Nothing -> putStrLn "" 629 | Just a -> putStrLn a 630 | _ -> do 631 | putStrLn "Usage: demo-list [text]..." 632 | #+END_SRC 633 | 634 | 4. Move to the import section, =M-g i=, and press =C-o= to insert a blank line 635 | 636 | If there were no previous imports, the cursor would have moved to ~main~. 637 | 638 | 5. Type ~import Sys~ 639 | 640 | A /company/ completion pop-up should have appeared with a list of modules 641 | that begin with =Sys=. 642 | 643 | ** [[http://company-mode.github.io/][Company]] 644 | 645 | Company is used for text completion. 646 | 647 | + Keybindings 648 | 649 | - =M-n= ~company-select-next~ 650 | - =M-p= ~company-select-previous~ 651 | - =C-s= ~company-search-candidates~ 652 | - =C-M-s= ~company-filter-candidates~ 653 | - =RET= ~company-complete-selection~ 654 | 655 | *Do*: 656 | 657 | 1. Press =M-n= to move the selection down to ~System.Exit~ and press =RET= 658 | 659 | 2. Format the imports: C-c C-, 660 | 661 | 3. Return to where the cursor was before modifying the imports: =C-u M-g i= 662 | 663 | 4. Add ~exitFailure~ after the usage message 664 | 665 | If you type slowly enough /company/ will provide a list of completions. 666 | 667 | 5. Replace ~undefined xs~ with ~fromList xs~ 668 | 669 | We still need to define this. 670 | 671 | ** Jump to Definitions 672 | 673 | Intero keeps track of where things were defined and can use this information 674 | to jump to the definition of the thing under the cursor. Intero will only 675 | jump to things defined within this project. 676 | 677 | + Keybindings 678 | 679 | - =M-.= jump to definition 680 | - M-, return to the previous location 681 | 682 | *Do*: jump to project definition. 683 | 684 | 1. Move the cursor onto ~car~ (the function, not the string) 685 | 686 | 2. Press =M-.= to jump to the definition 687 | 688 | 3. Return to where the cursor was before: M-, 689 | 690 | ** Jump to Dependency Definitions 691 | 692 | [[https://github.com/aloiscochard/codex][Codex]] can be used to create a TAGS file that contains the locations of third 693 | party library definitions. =M-.= has been bound to a [[file:emacs.d/init.el::defun init-intero-goto-definition][custom function]] that uses 694 | both Intero and the tags file. 695 | 696 | *Do*: jump to an external definition. 697 | 698 | 1. Generate tags: =C-c p R= 699 | 700 | The command must be run when in a Haskell buffer. 701 | 702 | This will create or update the [[file:TAGS][tags]] file. It must be manually regenerated 703 | after modifying the dependencies of a project. 704 | 705 | 2. Move the cursor onto ~getArgs~ and press =M-.= 706 | 707 | Choose =n= if prompted about adding a table. 708 | 709 | 3. Return to /Main.hs/: M-, 710 | 711 | 4. Try using =C-c p j= to jump to an arbitrary definition 712 | 713 | If the symbol is defined in multiple places a /xref/ buffer will list all 714 | results. 715 | 716 | + Keybindings for /xref/ list 717 | 718 | - =C-o= =xref-show-location-at-point= 719 | - =RET= =xref-goto-xref= 720 | - =n= =xref-next-line= 721 | - =p= =xref-prev-line= 722 | - =q= =quit-window= 723 | 724 | ** [[https://github.com/ndmitchell/hlint][HLint]] 725 | 726 | There is a /flycheck/ checker for HLint that provides code improvement 727 | suggestions. Some of the suggestions can be automatically applied using 728 | /hlint-refactor/. 729 | 730 | + Keybindings 731 | 732 | - C-c , b ~hlint-refactor-refactor-buffer~ 733 | - C-c , r ~hlint-refactor-refactor-at-point~ 734 | 735 | *Do*: implement ~fromList~ in /List.hs/. 736 | 737 | 1. Add the following definition 738 | 739 | #+BEGIN_SRC haskell 740 | fromList xs = 741 | foldr (Cons . id) Nil xs 742 | #+END_SRC 743 | 744 | 2. Press =M-p= to move the cursor onto ~Cons~ 745 | 746 | The yellow squiggly lines are suggestions from HLint. Suggestions can be 747 | disabled by adding a [[https://github.com/ndmitchell/hlint/blob/master/data/default.yaml][.hlint.yaml]] to a project. 748 | 749 | 3. Apply the suggestion to remove ~id~: C-c , r 750 | 751 | Sometimes applying a suggestion results in a new suggestion. 752 | 753 | 4. Try to apply the other suggestions 754 | 755 | Some suggestions cannot be applied automatically. 756 | 757 | ** Build the Project 758 | 759 | *Do*: 760 | 761 | 1. Compile the executable: =C-c p c= 762 | 763 | A default compile command will be shown. Add the following to the command 764 | to enable more optimizations and to turn warning into errors. 765 | 766 | #+BEGIN_SRC sh 767 | --ghc-options '-Werror -O2' 768 | #+END_SRC 769 | 770 | 2. Verify that the executable works: =C-c p != followed by: 771 | 772 | #+BEGIN_SRC sh 773 | stack exec -- demo-list car 1 2 3 774 | #+END_SRC 775 | 776 | You should see a "1" printed. 777 | 778 | ** Extra Credit 779 | 780 | + Add specs for ~car~ 781 | 782 | + Implement =demo-list cdr= 783 | -------------------------------------------------------------------------------- /emacs.d/custom.el: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydparser/demo-emacs-haskell/6706a29e94120548739b3b66c908541186330be3/emacs.d/custom.el -------------------------------------------------------------------------------- /emacs.d/init.el: -------------------------------------------------------------------------------- 1 | ;;; Initialize package 2 | 3 | (require 'package) 4 | 5 | (add-to-list 'package-archives 6 | '("melpa" . "http://melpa.org/packages/")) 7 | (setq package-archive-priorities '(("melpa" . 10)) 8 | package-enable-at-startup nil) 9 | (package-initialize) 10 | 11 | ;;; Initialize use-package 12 | 13 | (setq use-package-always-ensure t) 14 | 15 | (unless (package-installed-p 'use-package) 16 | (package-refresh-contents) 17 | (package-install 'use-package)) 18 | 19 | (eval-when-compile 20 | (require 'use-package)) 21 | 22 | (require 'bind-key) 23 | (require 'diminish) 24 | 25 | ;;; Utilities 26 | 27 | (defun init-kill-buffer-current () 28 | "Kill the current buffer." 29 | (interactive) 30 | (kill-buffer (current-buffer))) 31 | 32 | ;;; Global Configuration 33 | 34 | ;; Store customizations in a separate file. 35 | (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) 36 | (load custom-file) 37 | 38 | ;; Store auto-saves and backups in emacs.d/var. 39 | (let* ((vdir (expand-file-name "var" user-emacs-directory)) 40 | (adir (expand-file-name "autosaves/" vdir)) 41 | (ldir (expand-file-name "auto-save-list/" vdir)) 42 | (bdir (expand-file-name "backups/" vdir))) 43 | (make-directory adir t) 44 | (make-directory bdir t) 45 | (setq auto-save-file-name-transforms `((".*" ,(concat adir "\\1") t)) 46 | auto-save-list-file-prefix (concat ldir "/saves-") 47 | backup-directory-alist `((".*" . ,bdir)))) 48 | 49 | (when (member "Inconsolata" (font-family-list)) 50 | (set-frame-font "Inconsolata 15")) 51 | 52 | ;; Simplify prompts. 53 | (fset 'yes-or-no-p 'y-or-n-p) 54 | 55 | ;; Reduce noise. 56 | (setq confirm-nonexistent-file-or-buffer nil 57 | inhibit-splash-screen t 58 | inhibit-startup-echo-area-message t 59 | inhibit-startup-message t 60 | initial-scratch-message nil 61 | kill-buffer-query-functions (remq 'process-kill-buffer-query-function kill-buffer-query-functions) 62 | ring-bell-function 'ignore) 63 | 64 | ;; Prevent accidental closure. 65 | (setq confirm-kill-emacs 'y-or-n-p) 66 | 67 | ;; Display column number in modeline. 68 | (setq column-number-mode t) 69 | 70 | ;; Collect garbage less frequently. 71 | (setq gc-cons-threshold 104857600) 72 | 73 | ;; Delete the trailing newline. 74 | (setq kill-whole-line t) 75 | 76 | ;; Adjust indentation and line wrapping. 77 | (let ((spaces 2) 78 | (max-line-length 100)) 79 | (setq-default fill-column max-line-length 80 | indent-tabs-mode nil 81 | tab-width spaces 82 | tab-stop-list (number-sequence spaces max-line-length spaces))) 83 | 84 | ;; Open URLs within Emacs. 85 | (when (package-installed-p 'eww) 86 | (setq browse-url-browser-function 'eww-browse-url)) 87 | 88 | (bind-key "C-c C-SPC" #'delete-trailing-whitespace) 89 | (bind-key "C-x C-b" #'ibuffer) 90 | (bind-key "C-x C-k" #'init-kill-buffer-current) 91 | (bind-key "M-/" #'hippie-expand) 92 | (bind-key "M-o" #'other-window) 93 | 94 | (global-subword-mode 1) 95 | 96 | ;;; General Packages 97 | 98 | (use-package company 99 | :demand 100 | :diminish "" 101 | :init 102 | (progn 103 | (setq company-idle-delay 0.3) 104 | (global-company-mode))) 105 | 106 | (use-package exec-path-from-shell 107 | :defer t 108 | :if (memq window-system '(mac ns)) 109 | :init 110 | (progn 111 | (setq exec-path-from-shell-check-startup-files nil) 112 | (exec-path-from-shell-initialize))) 113 | 114 | (use-package helm 115 | :demand 116 | :diminish "" 117 | :bind (("C-M-y" . helm-show-kill-ring) 118 | ("C-h a" . helm-apropos) 119 | ("C-x C-f" . helm-find-files) 120 | ("C-x b" . helm-mini) 121 | ("M-s o" . helm-occur) 122 | ("M-x" . helm-M-x) 123 | :map helm-map 124 | ([tab] . helm-execute-persistent-action)) 125 | :init 126 | (progn 127 | (setq helm-M-x-fuzzy-match t 128 | helm-apropos-fuzzy-match t 129 | helm-buffers-fuzzy-matching t 130 | helm-ff-newfile-prompt-p nil 131 | helm-locate-fuzzy-match t 132 | helm-recentf-fuzzy-match t) 133 | (require 'helm-config) 134 | (helm-mode))) 135 | 136 | (use-package which-key 137 | :demand 138 | :pin melpa 139 | :init (which-key-mode)) 140 | 141 | (use-package yaml-mode 142 | :defer t) 143 | 144 | (use-package yasnippet 145 | :demand 146 | :diminish (yas-minor-mode . "") 147 | :init 148 | (progn 149 | (add-to-list 'hippie-expand-try-functions-list #'yas-hippie-try-expand) 150 | (yas-global-mode)) 151 | :config 152 | (progn 153 | (defun init-yas-uncapitalize (cap) 154 | (concat (downcase (substring cap 0 1)) 155 | (substring cap 1))) 156 | 157 | (unbind-key "TAB" yas-minor-mode-map) 158 | (unbind-key "" yas-minor-mode-map))) 159 | 160 | ;;; Demo Packages 161 | 162 | (use-package demo-it 163 | :defer t) 164 | 165 | (use-package expand-region 166 | :defer t 167 | :bind ("C-=" . er/expand-region)) 168 | 169 | (use-package fancy-narrow 170 | :defer t) 171 | 172 | (use-package org 173 | :defer t 174 | :init 175 | (progn 176 | (setq org-hide-emphasis-markers t 177 | org-log-done 'time 178 | org-src-fontify-natively t 179 | org-startup-truncated nil)) 180 | :config 181 | (progn 182 | (progn 183 | (org-babel-do-load-languages 184 | 'org-babel-load-languages 185 | '((emacs-lisp . t) 186 | (sh . t)))))) 187 | 188 | (use-package org-bullets 189 | :defer t 190 | :init 191 | (progn 192 | (add-hook 'org-mode-hook #'org-bullets-mode))) 193 | 194 | (use-package org-tree-slide 195 | :defer t) 196 | 197 | (use-package zenburn-theme 198 | :demand 199 | :init 200 | (progn 201 | ;; Increase contrast for presentation. 202 | (defvar zenburn-override-colors-alist 203 | '(("zenburn-bg-1" . "#101010") 204 | ("zenburn-bg-05" . "#202020") 205 | ("zenburn-bg" . "#2B2B2B") 206 | ("zenburn-bg+05" . "#383838") 207 | ("zenburn-bg+1" . "#3F3F3F") 208 | ("zenburn-bg+2" . "#494949") 209 | ("zenburn-bg+3" . "#4F4F4F"))) 210 | (load-theme 'zenburn 'no-confirm))) 211 | 212 | ;;; Development Packages 213 | 214 | (use-package compile 215 | :defer t 216 | :init 217 | (progn 218 | (setq compilation-scroll-output 'first-error) 219 | 220 | (defun init-compilation-colorize () 221 | "Colorize compilation." 222 | (let ((inhibit-read-only t)) 223 | (goto-char compilation-filter-start) 224 | (move-beginning-of-line nil) 225 | (ansi-color-apply-on-region (point) (point-max)))) 226 | 227 | (add-hook 'compilation-filter-hook #'init-compilation-colorize))) 228 | 229 | (use-package etags 230 | :bind (("M-." . init-goto-tag)) 231 | :init 232 | (progn 233 | (setq tags-revert-without-query t)) 234 | :config 235 | (progn 236 | (defun init-goto-tag () 237 | "Jump to the definition." 238 | (interactive) 239 | (find-tag (find-tag-default))))) 240 | 241 | (use-package helm-projectile 242 | :demand 243 | :init 244 | (progn 245 | (setq projectile-completion-system 'helm) 246 | (helm-projectile-on))) 247 | 248 | (use-package flycheck 249 | :demand 250 | :diminish "" 251 | :bind (:map flycheck-mode-map 252 | ("M-n" . flycheck-next-error) 253 | ("M-p" . flycheck-previous-error)) 254 | :init 255 | (progn 256 | (add-hook 'after-init-hook #'global-flycheck-mode)) 257 | :config 258 | (progn 259 | (defun init-flycheck-may-enable-mode (f) 260 | "Disallow flycheck in special buffers." 261 | (interactive) 262 | (and (not (string-prefix-p "*" (buffer-name))) 263 | (apply (list f)))) 264 | 265 | (advice-add 'flycheck-may-enable-mode :around 266 | #'init-flycheck-may-enable-mode))) 267 | 268 | (use-package magit 269 | :defer t 270 | :init 271 | (progn 272 | (setq magit-push-always-verify nil 273 | magit-revert-buffers t) 274 | (add-hook 'git-commit-mode-hook #'flyspell-mode))) 275 | 276 | (use-package paren 277 | :defer t 278 | :init 279 | (show-paren-mode)) 280 | 281 | (use-package projectile 282 | :demand 283 | :diminish "" 284 | :init 285 | (progn 286 | (setq projectile-create-missing-test-files t 287 | projectile-mode-line nil 288 | projectile-use-git-grep t) 289 | (projectile-mode))) 290 | 291 | ;;; Haskell Packages 292 | 293 | (use-package haskell-mode 294 | :defer t 295 | :bind (:map haskell-mode-map 296 | ("M-g i" . haskell-navigate-imports) 297 | ("M-g M-i" . haskell-navigate-imports)) 298 | :init 299 | (progn 300 | (setq haskell-compile-cabal-build-alt-command 301 | "cd %s && stack clean && stack build --ghc-options -ferror-spans" 302 | haskell-compile-cabal-build-command 303 | "cd %s && stack build --ghc-options -ferror-spans" 304 | haskell-compile-command 305 | "stack ghc -- -Wall -ferror-spans -fforce-recomp -c %s"))) 306 | 307 | (use-package haskell-snippets 308 | :defer t) 309 | 310 | (use-package hlint-refactor 311 | :defer t 312 | :diminish "" 313 | :init (add-hook 'haskell-mode-hook #'hlint-refactor-mode)) 314 | 315 | (use-package intero 316 | :defer t 317 | :diminish " λ" 318 | :bind (:map intero-mode-map 319 | ("M-." . init-intero-goto-definition)) 320 | :init 321 | (progn 322 | (defun init-intero () 323 | "Enable Intero unless visiting a cached dependency." 324 | (if (and buffer-file-name 325 | (string-match ".+/\\.\\(stack\\|stack-work\\)/.+" buffer-file-name)) 326 | (progn 327 | (eldoc-mode -1) 328 | (flycheck-mode -1)) 329 | (intero-mode) 330 | (set (make-local-variable 'projectile-tags-command) "codex update"))) 331 | 332 | (add-hook 'haskell-mode-hook #'init-intero)) 333 | :config 334 | (progn 335 | (defun init-intero-goto-definition () 336 | "Jump to the definition of the thing at point using Intero or etags." 337 | (interactive) 338 | (or (intero-goto-definition) 339 | (find-tag (find-tag-default)))) 340 | 341 | (flycheck-add-next-checker 'intero '(warning . haskell-hlint)))) 342 | 343 | ;;; Enable Disabled Features 344 | 345 | (put 'dired-find-alternate-file 'disabled nil) 346 | (put 'downcase-region 'disabled nil) 347 | (put 'narrow-to-region 'disabled nil) 348 | (put 'upcase-region 'disabled nil) 349 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: demo-emacs-haskell 2 | github: cydparser/demo-emacs-haskell 3 | 4 | dependencies: 5 | - base >= 4.7 && < 5 6 | 7 | default-extensions: 8 | - NoMonomorphismRestriction 9 | 10 | ghc-options: 11 | - -Wall 12 | - -Wno-missing-signatures 13 | - -Wno-type-defaults 14 | - -Wno-unused-do-bind 15 | - -threaded 16 | - -rtsopts 17 | - -with-rtsopts=-N 18 | 19 | executables: 20 | demo-list: 21 | main: Main.hs 22 | source-dirs: 23 | - src 24 | 25 | tests: 26 | doctest: 27 | main: DocTest.hs 28 | source-dirs: 29 | - test 30 | dependencies: 31 | - QuickCheck 32 | - doctest 33 | spec: 34 | main: Spec.hs 35 | source-dirs: 36 | - src 37 | - test 38 | dependencies: 39 | - QuickCheck 40 | - hspec 41 | - hspec-discover 42 | -------------------------------------------------------------------------------- /src/Demo/List.hs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydparser/demo-emacs-haskell/6706a29e94120548739b3b66c908541186330be3/src/Demo/List.hs -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydparser/demo-emacs-haskell/6706a29e94120548739b3b66c908541186330be3/src/Main.hs -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | packages: 3 | - '.' 4 | resolver: lts-8.12 5 | -------------------------------------------------------------------------------- /test/Demo/ListSpec.hs: -------------------------------------------------------------------------------- 1 | module Demo.ListSpec where 2 | 3 | import Test.Hspec 4 | 5 | import Demo.List 6 | 7 | {-# ANN module "HLint: ignore Redundant do" #-} 8 | 9 | main = hspec spec 10 | 11 | spec = parallel $ do 12 | describe "List" $ do 13 | describe "cdr" $ do 14 | undefined 15 | -------------------------------------------------------------------------------- /test/DocTest.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import System.Environment (getArgs) 4 | import Test.DocTest 5 | 6 | main :: IO () 7 | main = do 8 | args <- getArgs 9 | doctest $ case args of 10 | [] -> ["src"] 11 | fs -> fs 12 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | --------------------------------------------------------------------------------