├── DevelopmentEnvironment.md └── README.md /DevelopmentEnvironment.md: -------------------------------------------------------------------------------- 1 | # Setting up a solid Haskell development environment on Windows (with GHCJS) 2 | 3 | For me, learning Haskell as a language was relatively easy. My struggles began when I started getting serious with Haskell and doing some actual work in it. 4 | 5 | I didn't struggle with the language or understanding some advanced concepts I struggled with tooling. Tooling in and of it self is fine and well thought out in my opinion but what was missing (until now) is a comprehensive tutorial / documentation on how to set up your development environment properly. 6 | 7 | Coincidentally, this is what I think is missing from most other programming language courses. Absence of such tutorials might be fine if you are slowly growing into a programming language community and picking things up as you go, but if you are experienced programmer it is pain in the ass to slowly collect bits and pieces of information from thousand different places. 8 | 9 | So, with that in mind, I present you a comprehensive tutorial on how to set up a solid and flexible Haskell development environment on Windows which also supports compiling with GHCJS. 10 | 11 | ## Installing Haskell 12 | 13 | Personally I like to manually manage my compilers, but in this case we'll first cover a simple installation procedure with automated installer and then I'll show you a more flexible method. 14 | 15 | ### Method 01: Minimal Installer 16 | 17 | [![01_Haskell_Minimal_Installer](https://img.youtube.com/vi/tcyaDUuQaCw/0.jpg)](https://www.youtube.com/watch?v=tcyaDUuQaCw) 18 | 19 | I recommend using the minimal installer. Full installer comes with some included libraries but you are most likely going to need some other versions of those libraries for your future projects so it really doesn't matter if they are initially included or not. We'll download them later anyway. 20 | 21 | You can download the minimal installer from the following link: 22 | 23 | [https://www.haskell.org/platform/windows.html](https://www.haskell.org/platform/windows.html) 24 | 25 | After you are done downloading it, run it (obviously). I suggest you go with all the defaults except when you come to the "Choose Component" section of the installer. There you should uncheck "Stack". 26 | 27 | Stack is a package manager / build tool and a really cool piece of software which eased pains of development with Haskell for quite some time now, but with recent advances in Cabal ("official" build tool) development I think it's time to start using Cabal again. 28 | 29 | You can also install Stack manually from the following [link](https://get.haskellstack.org/stable/windows-x86_64-installer.exe) in case it turns out you really need it. 30 | 31 | ### Method 02: Manual Installation 32 | 33 | [![02_Haskell_Manual_Installation](https://img.youtube.com/vi/30KfCFmvk9U/0.jpg)](https://www.youtube.com/watch?v=30KfCFmvk9U) 34 | 35 | Manual installation is my preferred way of doing things. You can download GHC (Glasgow Haskell Compiler) and Cabal from the following links: 36 | 37 | + [GHC download page](https://www.haskell.org/ghc/download.html) 38 | + [Cabal download page](https://www.haskell.org/cabal/download.html) 39 | 40 | On those two pages you will find stable releases and also a list of older releases. 41 | 42 | At the time of writing this guide, latest stable version of GHC is `8.6.5`. However, since I like to keep my GHCJS version in sync with my GHC version and the latest GHCJS version to date is `8.4` I will download `GHC 8.4.4` instead (you can find it under "Older releases" section). This can easily be changed later. 43 | 44 | Also, current version of Cabal (`cabal-install` actually) is `2.4` but I suggest you upgrade to `3.x` once it is released since it should include a bunch of very nice improvements. 45 | 46 | Obviously, I shouldn't have to mention that you want to download GHC and Cabal version for your operating system and architecture. 47 | 48 | Since I'm using Windows and have a 64 bit operating system I have downloaded this two files: 49 | 50 | + ghc-8.4.4-x86_64-unknown-mingw32.tar.xz 51 | + cabal-install-2.4.1.0-x86_64-unknown-mingw32.zip 52 | 53 | In my user directory I've created the `Haskell` directory which will contain files from these two archives. You are free to create this directory anywhere you like. 54 | 55 | In this directory I've created `GHC` and `Cabal` directory with `Versions` folder into which I've extracted contents of two previously downloaded archives. 56 | 57 | Here's the final directory structure: 58 | 59 | ``` 60 | C:\Users\lh\Haskell 61 | ├── Cabal 62 | │   └── Versions 63 | │   └── Cabal-2.4.1.0 64 | └── GHC 65 | └── Versions 66 | └── ghc-8.4.4 67 | ``` 68 | 69 | This layout is useful because it allows me to have multiple GHC and Cabal versions and switch them up when I feel like it. 70 | 71 | To make switching easier I've created symbolic links to the current version of Cabal and GHC with a static name `Link` in each directory. This way I don't have to modify my `Path` variable every time I decide to change my GHC / Cabal version. I can just relink new version to the `Link` link. 72 | 73 | Here's how to do it from PowerShell: 74 | 75 | ``` 76 | PS C:\Users\lh\Haskell> cmd /c mklink /D .\GHC\Link .\GHC\Versions\ghc-8.4.4\ 77 | PS C:\Users\lh\Haskell> cmd /c mklink /D .\Cabal\Link .\Cabal\Versions\Cabal-2.4.1.0\ 78 | ``` 79 | 80 | Since PowerShell doesn't contain `mklink` for some reason we need to execute it through `cmd` (the old prompt). This is why `mklink` is prepended with `cmd /c`. 81 | 82 | After this you should have the following directory structure: 83 | 84 | ``` 85 | C:\Users\lh\Haskell 86 | ├── Cabal 87 | │ ├── Link -> ./Versions/Cabal-2.4.1.0/ 88 | │ └── Versions 89 | │ └── Cabal-2.4.1.0 90 | └── GHC 91 | ├── Link -> ./Versions/ghc-8.4.4/ 92 | └── Versions 93 | └── ghc-8.4.4 94 | ``` 95 | 96 | Next step is to add following two paths to your account `Path` variable (you can add it to the system path but this is more of a "personal" setting so I think this belongs to the user path variable): 97 | 98 | ``` 99 | C:\Users\lh\Haskell\Cabal\Link 100 | C:\Users\lh\Haskell\GHC\Link\bin <-- notice the "bin" in GHC Link path 101 | ``` 102 | 103 | To add these two paths to your path variable you can press start and start typing "path" in the search bar. You should see the "Edit the system environment variables" item. 104 | 105 | To check if everything works as it should open your console (close the previous ones because they still have the old environment variables cached) and type the following: 106 | 107 | ``` 108 | PS C:\Users\lh> ghc --version 109 | The Glorious Glasgow Haskell Compilation System, version 8.4.4 110 | 111 | PS C:\Users\lh> cabal --version 112 | cabal-install version 2.4.1.0 113 | compiled using version 2.4.1.0 of the Cabal library 114 | ``` 115 | 116 | You should get the version information of GHC and Cabal. 117 | 118 | #### Useful Resources 119 | 120 | + [GHC user's guide](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/) 121 | + [Cabal user's guide](https://www.haskell.org/cabal/users-guide/) 122 | + [Stack user's guide](https://docs.haskellstack.org/en/stable/README/) 123 | 124 | ## Installing GIT (the right way) 125 | 126 | [![03_Git_Installer](https://img.youtube.com/vi/eQsVp-TTCZI/0.jpg)](https://www.youtube.com/watch?v=eQsVp-TTCZI) 127 | 128 | GIT comes with MSYS2 package. We don't nasty third party UNIX utilities form GIT installation polluting our pristine Windows environment. We'll do that our self later. 129 | 130 | The reason why is because we'll later use MSYS2 as a package manager, and since GIT is installed in `Program files` directory we'd have to work from administrator GIT bash console to download any library or tool, and they would be downloaded into `Program files` which is not desirable. 131 | 132 | 1. When installing GIT in the "Adjusting your PATH environment" installer step you should choose "Git from the command line and also from 3rd-party software" option. 133 | 2. On the "Configuring the terminal emulator to use with Git Bash" select "Use windows default console window" option. 134 | 3. And just to be on the safe side, on the "Configuring extra options" step check "Enable symbolic links" as well. 135 | 136 | ## Installing MSYS2 137 | 138 | [![04_MSYS2_Installer](https://img.youtube.com/vi/OjJp9tf2aoI/0.jpg)](https://www.youtube.com/watch?v=OjJp9tf2aoI) 139 | 140 | Installing MSYS2 is pretty straight forward if you have read the [instructions on their page](https://www.msys2.org/). Just for the completeness sake I've also recorded a video of the installation procedure. 141 | 142 | ## Configuring MSYS2 And Compiling Cairo library 143 | 144 | [![05_MSYS2_Configuration_Cairo_Installation](https://img.youtube.com/vi/RfvS7hJYqiU/0.jpg)](https://www.youtube.com/watch?v=RfvS7hJYqiU) 145 | 146 | If you can compile [cairo](https://hackage.haskell.org/package/cairo) on Windows then you can do anything! 147 | 148 | Cairo is the first Haskell library I've encountered that was complicated to get working on Windows, but in reality it's really easy. You just need to **know what you're doing** ;). 149 | 150 | Cairo package provides Haskell bindings to [cairo graphics library](https://www.cairographics.org/) which was written in C... or C++ or something. 151 | 152 | The point is, it's a pain in the ass to get and install because it is available as a stand alone, but it isn't, it's actually packaged in GTK but you have to have the right GTK and add it to the path... you get the picture. 153 | 154 | Luckily we can use [Arch Linux's](https://www.archlinux.org/) package manager [pacman](https://wiki.archlinux.org/index.php/Pacman) within the MSYS2 UNIX like environment to download Windows cairo binaries. 155 | 156 | As you will soon see, this Frankenstein monster works beautifully once you patch all the pieces together. 157 | 158 | Let's first try to install `cairo` through `cabal` to see what happens. I'll be using `new-` style commands but if you are using `cabal >= 3.0` you can drop the `new-` prefix. 159 | 160 | **Also, don't forget to run `cabal new-update` to update your local package database.** 161 | 162 | We can "install" the `cairo` library by running the following command: 163 | 164 | ``` 165 | cabal new-install cairo --lib 166 | ``` 167 | 168 | This should give us a warning that some packages failed to install and that we should rerun the previous command with `-j1` flag to see the error. 169 | 170 | ``` 171 | cabal new-install cairo --lib -j1 172 | ``` 173 | 174 | After running this command we notice that the problem is in the missing `pkg-config` utility. This little command utility is used by build systems and such to tell them where certain libraries are located on the computer. 175 | 176 | Since Haskell `cairo` library is looking for C++ `cairo` library it can't automagically find it without the help of `pkg-config`. 177 | 178 | We can get the `pkg-config` from our newly installed MSYS2 console. Press `start` and search for "msys". You will notice that we get several results: 179 | 180 | 1. MSYS2 MSYS 181 | 2. MSYS2 MinGW 32-bit 182 | 3. MSYS2 MinGW 64-bit 183 | 184 | We can use option `1` for now, just keep in mind that 32 bit version will have 32 bit libraries in it's environment (once we download them) and 64 bit version will obviously have 64 bit versions of libraries. 185 | 186 | Great, so now you are hopefully in the MSYS console environment. 187 | 188 | We can search for installable packages with the following command: 189 | 190 | ``` 191 | pacman -Ss name-of-the-package or keywords 192 | ``` 193 | 194 | Let's search for `pkg-config`: 195 | 196 | ``` 197 | lh@FloatingIsland MSYS ~ 198 | $ pacman -Ss pkg-config 199 | mingw32/mingw-w64-i686-pkg-config 0.29.2-1 (mingw-w64-i686-toolchain) 200 | A system for managing library compile/link flags (mingw-w64) 201 | mingw32/mingw-w64-i686-python2-pkgconfig 1.4.0-1 202 | A Python interface to the pkg-config command line tool 203 | mingw32/mingw-w64-i686-python3-pkgconfig 1.4.0-1 204 | A Python interface to the pkg-config command line tool 205 | mingw32/mingw-w64-i686-ruby-pkg-config 1.3.2-1 206 | Implementation of pkg-config in ruby (mingw-w64) 207 | mingw64/mingw-w64-x86_64-pkg-config 0.29.2-1 (mingw-w64-x86_64-toolchain) 208 | A system for managing library compile/link flags (mingw-w64) 209 | mingw64/mingw-w64-x86_64-python2-pkgconfig 1.4.0-1 210 | A Python interface to the pkg-config command line tool 211 | mingw64/mingw-w64-x86_64-python3-pkgconfig 1.4.0-1 212 | A Python interface to the pkg-config command line tool 213 | mingw64/mingw-w64-x86_64-ruby-pkg-config 1.3.2-1 214 | Implementation of pkg-config in ruby (mingw-w64) 215 | msys/pkg-config 0.29.2-1 (base-devel) 216 | A system for managing library compile/link flags 217 | ``` 218 | 219 | As you can see we get several options. Notice that we can install both 32 and 64 bit versions of the packages. They are prefixed with `mingw32` and `mingw64` respectively. 220 | 221 | Because I'm on a 64 bit system I'm going to install 64 bit packages, but note that sometimes, depending on what you are doing and what your dependencies are, you will have to install 32 bit version of some package on your 64 bit system. It's nothing unusual. 222 | 223 | We can notice that we have some packages with `python` or `ruby` in them. I'm guessing those are `python` and `ruby` bindings / wrappers for `pkg-config` but we are looking for the raw `pkg-config` which is present in the following line: 224 | 225 | ``` 226 | mingw64/mingw-w64-x86_64-pkg-config 0.29.2-1 (mingw-w64-x86_64-toolchain) 227 | A system for managing library compile/link flags (mingw-w64) 228 | ``` 229 | 230 | So, `mingw-w64-x86_64-pkg-config` is the name of the package we want to install but before telling `pacman` to install it for us notice that we have `mingw-w64-x86_64-toolchain` in the parentheses besides the version number. 231 | 232 | This tells us that `pkg-config` is a member of `mingw-w64-x86_64-toolchain` group of packages. This `toolchain` group obviously contains various tools and utilities for compiling so it wouldn't hurt to install the whole group along with the `pkg-config`. We can do this as though the group was a single package and just execute the following command: 233 | 234 | ``` 235 | pacman -S mingw-w64-x86_64-toolchain 236 | ``` 237 | 238 | You will be asked if you want to install the whole group or just parts of it so just press enter a few times until things start installing. 239 | 240 | After the installation if we try to use [which](https://www.linux.org/docs/man1/which.html) command to see the location of `pkg-config` on our disk we will get the message that there is no such executable. 241 | 242 | This is because we are within `MSYS` environment but the package we have installed belongs to `MinGW64`. Close your current `MSYS` console and open `MSYS2 MinGW 64-bit`. 243 | 244 | If we run `which pkg-config` in `MINGW64` console we should see that it is located in `/mingw64/bin` folder. 245 | 246 | Question is, where is `/mingw64` folder? Or even better, where is the `/` (root) folder? The root folder of our little UNIX like system is located in the directory where you've installed the msys2 package. In my case that is `C:\msys64`. 247 | 248 | If we follow this logic then our `pkg-config` is located in `C:\msys64\mingw64\bin` along with all the other 64 bit libraries and programs that we have installed through MSYS console. 249 | 250 | Right now `pkg-config` is only available through MSYS console, but I prefer to use PowerShell and have a "native" feel to my system. Because of that it is now time to pollute our system by adding `C:\msys64\mingw64\bin` to our system path. 251 | 252 | In case you need to install some 32 bit packages don't forget to add `C:\msys64\mingw32\bin` to your system path as well. 253 | 254 | We also need to add `bin` path of `MSYS` utilities as well, and to make sure everything plays nicely together order those paths like this: 255 | 256 | 1. C:\msys64\mingw64\bin 257 | 2. C:\msys64\mingw32\bin 258 | 3. C:\msys64\usr\bin 259 | 260 | I've put 64 bit folder first, because I'm on a 64 bit system and I want 64 bit version of libraries to take precedence. This can sometimes not work very well so you'll have to massage your path and installed packages but from my experience it happens very rarely and it is a bridge you should cross when you come to it. 261 | 262 | **When you are wondering why something is missing or not working, it is quite useful to use `which` to check where things are coming from.** 263 | 264 | Once you've added those paths, don't forget to close your PowerShell console and fire it up again to reflect changes in your new path. 265 | 266 | Now we should have some progress with our Haskell `cairo` installation. If we run `cabal new-install cairo` we will be greeted with a new message. This time the message is not about `pkg-config` but from `pkg-config` telling us that he can't find `cairo` (this time a C library not a Haskell one) on our machine. 267 | 268 | This is because we haven't installed it yet. To do so, we need to go back to our MSYS console and use `pacman`. 269 | 270 | If we search for `cairo` we again get a number of results, among which are some python and ruby bindings to `cairo` as well as `cairo` library it self. To install it run the following command: 271 | 272 | ``` 273 | pacman -S mingw-w64-x86_64-cairo 274 | ``` 275 | 276 | After that we can close our consoles, fire up PowerShell once again and finally install and compile Haskell `cairo` library by running: 277 | 278 | ``` 279 | cabal new-install cairo --lib 280 | ``` 281 | 282 | ## Windows Subsystem For Linux 283 | 284 | Awesome feature. It's like reverse [WINE](https://www.winehq.org/) and it allows us to run Linux software on Windows machine. Note that MSYS is only UNIX **like** environment and we have been using native Windows versions of `cairo` and `pkg-config` which will not be the case with [Windows Subsystem For Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (aka. WSL). 285 | 286 | WSL will allow us much simpler workflow with [GHCJS](https://github.com/ghcjs/ghcjs) without the need for messy compilation of GHCJS through Stack or tools like [Nix](https://nixos.org/nix/) package manager (although you can use Nix as well and I plan to cover it in future guides). 287 | 288 | ### Enabling WSL 289 | 290 | [![06_WSL_Enable](https://img.youtube.com/vi/Kd_CTkziBc4/0.jpg)](https://www.youtube.com/watch?v=Kd_CTkziBc4) 291 | 292 | To enable WSL you can either open a console as an administrator and then run following command in PowerShell: 293 | 294 | ``` 295 | Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 296 | ``` 297 | 298 | Or press the `start` button and search for "Turn Windows features on or off" and enabling "Windows Subsystem for Linux" which is what I did. 299 | 300 | After enabling it you should restart the computer. 301 | 302 | ### WSL Ubuntu Installation 303 | 304 | [![07_WSL_Installation_Procedure](https://img.youtube.com/vi/goEYFy3NbMg/0.jpg)](https://www.youtube.com/watch?v=goEYFy3NbMg) 305 | 306 | You can install [Ubuntu](https://www.ubuntu.com/) for WSL from Windows Store, but I prefer to install it manually. 307 | 308 | You can download the installation package with the following command: 309 | 310 | ``` 311 | PS C:\Users\lh\Desktop> Invoke-WebRequest -Uri https://aka.ms/wsl-ubuntu-1804 -OutFile Ubuntu.appx -UseBasicParsing 312 | ``` 313 | 314 | This will download latest (at the time) 18.04 version of Ubuntu to your desktop (or where ever). Once the file has been downloaded you can install it by running: 315 | 316 | ``` 317 | PS C:\Users\lh\Desktop> Add-AppxPackage .\Ubuntu.appx 318 | ``` 319 | 320 | After that is done, press the `start` button and search for "Ubuntu" and hit enter. 321 | 322 | You should get the Ubuntu's [bash](https://www.gnu.org/software/bash/) console / shell / what ever and the initial setup will start automatically. 323 | 324 | You will have to enter username and password for this new "virtual" linux system. 325 | 326 | **Make sure you remember your password**. 327 | 328 | After that, you will have Linux powers on your Windows machine and you can even enter that `bash` environment from PowerShell simply by executing `bash` command. 329 | 330 | Before continuing you should probably run 331 | 332 | ``` 333 | sudo apt update 334 | sudo apt upgrade 335 | ``` 336 | 337 | to update your new Linux system. 338 | 339 | ### WSL GHC and Cabal Installation 340 | 341 | [![08_WSL_GHC_Cabal_Installation](https://img.youtube.com/vi/v7GLCTGGczc/0.jpg)](https://www.youtube.com/watch?v=v7GLCTGGczc) 342 | 343 | Ok, so the whole point of installing WSL was to get GHCJS the easy way. You can thank [Herbert V. Riedel](https://github.com/hvr) for this since he has provided us with several nice repositories with pre-compiled GHCJS / GHC versions optimized for WSL. 344 | 345 | + [GHC PPA](https://launchpad.net/~hvr/+archive/ubuntu/ghc/) 346 | + [GHC for WSL PPA](https://launchpad.net/~hvr/+archive/ubuntu/ghc-wsl) 347 | + [GHCJS PPA](https://launchpad.net/~hvr/+archive/ubuntu/ghcjs) 348 | 349 | You can add this PPA's by executing following commands (obviously you should only add either GHC PPA or GHC for WSL PPA, not both): 350 | 351 | ``` 352 | sudo add-apt-repository ppa:hvr/ghc-wsl 353 | sudo add-apt-repository ppa:hvr/ghcjs 354 | sudo apt-get update 355 | ``` 356 | 357 | After adding PPA's and updating package database we can start by installing GHC and Cabal. Since GHCJS and GHC major versions need to be in sync (I think) and the latest GHCJS version at the moment is `8.4` I will install `GHC 8.4.4`. 358 | 359 | Also, even though HVR has `cabal-install-3.0` available in the PPA it is still not officially released so I'm going to install version `2.4` instead. 360 | 361 | ``` 362 | lh@FloatingIsland:/mnt/c/Users/lh$ sudo apt install ghc-8.4.4 363 | lh@FloatingIsland:/mnt/c/Users/lh$ sudo apt install cabal-install-2.4 364 | ``` 365 | 366 | With this done, GHC and Cabal still won't be in our path. Instead their binaries are available in the `/opt/ghc` and `/opt/cabal` folders and we need to add them to the path. 367 | 368 | Layout of those two directories is similar to my "manual" windows setup and we have option to install multiple GHC and Cabal versions and switch the active one by running following commands: 369 | 370 | ``` 371 | sudo update-alternatives --config opt-ghc 372 | sudo update-alternatives --config opt-cabal 373 | ``` 374 | 375 | Moving on, we need to add `/opt/ghc/bin` and `/opt/cabal/bin` directories to our path. To do so I've opened my `~/.profile` file with [VIM](https://www.vim.org/) by running `vim ~/.profile` and added following code at the end of this file: 376 | 377 | ```bash 378 | # set PATH so it includes GHC from /opt folder 379 | if [ -d "/opt/ghc/bin" ] ; then 380 | PATH="/opt/ghc/bin:$PATH" 381 | fi 382 | ``` 383 | 384 | After that you can run `. ~/.profile` to update your environment without exiting and reloading `bash`. 385 | 386 | You can check if you have GHC and Cabal by executing these commands: 387 | 388 | ``` 389 | lh@FloatingIsland:/opt/ghc$ ghc --version 390 | The Glorious Glasgow Haskell Compilation System, version 8.4.4 391 | 392 | lh@FloatingIsland:/opt/ghc$ cabal --version 393 | cabal-install version 2.4.1.0 394 | compiled using version 2.4.1.0 of the Cabal library 395 | ``` 396 | 397 | ### WSL GHCJS Installation 398 | 399 | [![09_WSL_GHCJS_Installation](https://img.youtube.com/vi/X4QQAIbaji8/0.jpg)](https://www.youtube.com/watch?v=X4QQAIbaji8) 400 | 401 | Next we have to install GHCJS, which is also very straight forward. You can do it by running: 402 | 403 | ``` 404 | lh@FloatingIsland:/opt/ghc$ sudo apt install ghcjs-8.4 405 | ``` 406 | 407 | It will be installed into the `/opt/ghcjs/8.4/bin` directory so we need to add this to our path as well. Again, after opening `~/.profile` I've added the following code to the end of the file. 408 | 409 | ```bash 410 | # set PATH so it includes GHCJS from /opt folder 411 | if [ -d "/opt/ghcjs/8.4/bin" ] ; then 412 | PATH="/opt/ghcjs/8.4/bin:$PATH" 413 | fi 414 | ``` 415 | 416 | And after sourcing my profile with `. ~/.profile` I can check if GHCJS was set up correctly by executing `ghcjs --version`. 417 | 418 | ### WSL Building Miso ToDo MVC Single Page Web Application 419 | 420 | [![10_WSL_GHCJS_Building_Miso_ToDoMVC](https://img.youtube.com/vi/YIsZMLiSzlY/0.jpg)](https://www.youtube.com/watch?v=YIsZMLiSzlY) 421 | 422 | Cool. Now that we have everything set up it's time to test our GHCJS compiling abilities :D 423 | 424 | To do so I'll use Haskell [Miso](https://haskell-miso.org/) library written by [David Johnson](https://github.com/dmjio) and try to build [example ToDo MVC](https://todo-mvc.haskell-miso.org/) application. 425 | 426 | Source can be found on [Miso's github repository](https://github.com/dmjio/miso/blob/master/examples/todo-mvc/Main.hs). I've downloaded the source by hand into `ToDoMVC` folder on my desktop and ran `cabal init` command inside of that folder to initialize the project. 427 | 428 | After deleting all the cr** that is not needed and adding `cabal.project` file (new cabal feature) for this simple test `ToDoMVC` content looks like this: 429 | 430 | ``` 431 | ToDoMVC/ 432 | ├── Main.hs 433 | ├── ToDoMVC.cabal 434 | └── cabal.project 435 | ``` 436 | **cabal.project** 437 | 438 | ```cabal 439 | packages : ToDoMVC.cabal 440 | 441 | constraints : base ^>= 4.11 442 | , miso ^>= 0.21 443 | , aeson ^>= 1.4 444 | , containers ^>= 0.6 445 | , jsaddle-warp ^>= 0.9 446 | ``` 447 | 448 | **ToDoMVC.cabal** 449 | 450 | ```cabal 451 | cabal-version : 2.4 452 | name : ToDoMVC 453 | version : 0.1.0.0 454 | 455 | executable ToDoMVC 456 | main-is : Main.hs 457 | build-depends : base 458 | , miso 459 | , aeson 460 | , containers 461 | , jsaddle-warp 462 | default-language : Haskell2010 463 | ``` 464 | 465 | **Main.hs** 466 | 467 | ```haskell 468 | {-# LANGUAGE TypeOperators #-} 469 | {-# LANGUAGE OverloadedStrings #-} 470 | {-# LANGUAGE FlexibleInstances #-} 471 | {-# LANGUAGE TypeFamilies #-} 472 | {-# LANGUAGE DataKinds #-} 473 | {-# LANGUAGE DeriveGeneric #-} 474 | {-# LANGUAGE ScopedTypeVariables #-} 475 | {-# LANGUAGE RecordWildCards #-} 476 | {-# LANGUAGE LambdaCase #-} 477 | {-# LANGUAGE MultiParamTypeClasses #-} 478 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 479 | {-# LANGUAGE ExtendedDefaultRules #-} 480 | module Main where 481 | 482 | import Data.Aeson hiding (Object) 483 | import Data.Bool 484 | import qualified Data.Map as M 485 | import Data.Monoid 486 | import GHC.Generics 487 | import Miso 488 | import Miso.String (MisoString) 489 | import qualified Miso.String as S 490 | 491 | import Control.Monad.IO.Class 492 | import Language.Javascript.JSaddle.Warp as JSaddle 493 | 494 | default (MisoString) 495 | 496 | data Model = Model 497 | { entries :: [Entry] 498 | , field :: MisoString 499 | , uid :: Int 500 | , visibility :: MisoString 501 | , step :: Bool 502 | } deriving (Show, Generic, Eq) 503 | 504 | data Entry = Entry 505 | { description :: MisoString 506 | , completed :: Bool 507 | , editing :: Bool 508 | , eid :: Int 509 | , focussed :: Bool 510 | } deriving (Show, Generic, Eq) 511 | 512 | instance ToJSON Entry 513 | instance ToJSON Model 514 | 515 | instance FromJSON Entry 516 | instance FromJSON Model 517 | 518 | emptyModel :: Model 519 | emptyModel = Model 520 | { entries = [] 521 | , visibility = "All" 522 | , field = mempty 523 | , uid = 0 524 | , step = False 525 | } 526 | 527 | newEntry :: MisoString -> Int -> Entry 528 | newEntry desc eid = Entry 529 | { description = desc 530 | , completed = False 531 | , editing = False 532 | , eid = eid 533 | , focussed = False 534 | } 535 | 536 | data Msg 537 | = NoOp 538 | | CurrentTime Int 539 | | UpdateField MisoString 540 | | EditingEntry Int Bool 541 | | UpdateEntry Int MisoString 542 | | Add 543 | | Delete Int 544 | | DeleteComplete 545 | | Check Int Bool 546 | | CheckAll Bool 547 | | ChangeVisibility MisoString 548 | deriving Show 549 | 550 | main :: IO () 551 | main = 552 | JSaddle.run 8080 $ startApp App { initialAction = NoOp, ..} 553 | where 554 | model = emptyModel 555 | update = updateModel 556 | view = viewModel 557 | events = defaultEvents 558 | mountPoint = Nothing 559 | subs = [] 560 | 561 | updateModel :: Msg -> Model -> Effect Msg Model 562 | updateModel NoOp m = noEff m 563 | updateModel (CurrentTime n) m = 564 | m <# do liftIO (print n) >> pure NoOp 565 | updateModel Add model@Model{..} = 566 | noEff model { 567 | uid = uid + 1 568 | , field = mempty 569 | , entries = entries <> [ newEntry field uid | not $ S.null field ] 570 | } 571 | updateModel (UpdateField str) model = noEff model { field = str } 572 | updateModel (EditingEntry id' isEditing) model@Model{..} = 573 | model { entries = newEntries } <# do 574 | focus $ S.pack $ "todo-" ++ show id' 575 | pure NoOp 576 | where 577 | newEntries = filterMap entries (\t -> eid t == id') $ 578 | \t -> t { editing = isEditing, focussed = isEditing } 579 | 580 | updateModel (UpdateEntry id' task) model@Model{..} = 581 | noEff model { entries = newEntries } 582 | where 583 | newEntries = 584 | filterMap entries ((==id') . eid) $ \t -> 585 | t { description = task } 586 | 587 | updateModel (Delete id') model@Model{..} = 588 | noEff model { entries = filter (\t -> eid t /= id') entries } 589 | 590 | updateModel DeleteComplete model@Model{..} = 591 | noEff model { entries = filter (not . completed) entries } 592 | 593 | updateModel (Check id' isCompleted) model@Model{..} = 594 | model { entries = newEntries } <# eff 595 | where 596 | eff = 597 | liftIO (putStrLn "clicked check") >> 598 | pure NoOp 599 | 600 | newEntries = 601 | filterMap entries (\t -> eid t == id') $ \t -> 602 | t { completed = isCompleted } 603 | 604 | updateModel (CheckAll isCompleted) model@Model{..} = 605 | noEff model { entries = newEntries } 606 | where 607 | newEntries = 608 | filterMap entries (const True) $ 609 | \t -> t { completed = isCompleted } 610 | 611 | updateModel (ChangeVisibility v) model = 612 | noEff model { visibility = v } 613 | 614 | filterMap :: [a] -> (a -> Bool) -> (a -> a) -> [a] 615 | filterMap xs predicate f = go' xs 616 | where 617 | go' [] = [] 618 | go' (y:ys) 619 | | predicate y = f y : go' ys 620 | | otherwise = y : go' ys 621 | 622 | viewModel :: Model -> View Msg 623 | viewModel m@Model{..} = 624 | div_ 625 | [ class_ "todomvc-wrapper" 626 | , style_ $ M.singleton "visibility" "hidden" 627 | ] 628 | [ section_ 629 | [ class_ "todoapp" ] 630 | [ viewInput m field 631 | , viewEntries visibility entries 632 | , viewControls m visibility entries 633 | ] 634 | , infoFooter 635 | , link_ 636 | [ rel_ "stylesheet" 637 | , href_ "https://d33wubrfki0l68.cloudfront.net/css/d0175a264698385259b5f1638f2a39134ee445a0/style.css" 638 | ] 639 | ] 640 | 641 | viewEntries :: MisoString -> [ Entry ] -> View Msg 642 | viewEntries visibility entries = 643 | section_ 644 | [ class_ "main" 645 | , style_ $ M.singleton "visibility" cssVisibility 646 | ] 647 | [ input_ 648 | [ class_ "toggle-all" 649 | , type_ "checkbox" 650 | , name_ "toggle" 651 | , checked_ allCompleted 652 | , onClick $ CheckAll (not allCompleted) 653 | ] 654 | , label_ 655 | [ for_ "toggle-all" ] 656 | [ text $ S.pack "Mark all as complete" ] 657 | , ul_ [ class_ "todo-list" ] $ 658 | flip map (filter isVisible entries) $ \t -> 659 | viewKeyedEntry t 660 | ] 661 | where 662 | cssVisibility = bool "visible" "hidden" (null entries) 663 | allCompleted = all (==True) $ completed <$> entries 664 | isVisible Entry {..} = 665 | case visibility of 666 | "Completed" -> completed 667 | "Active" -> not completed 668 | _ -> True 669 | 670 | viewKeyedEntry :: Entry -> View Msg 671 | viewKeyedEntry = viewEntry 672 | 673 | viewEntry :: Entry -> View Msg 674 | viewEntry Entry {..} = liKeyed_ (toKey eid) 675 | [ class_ $ S.intercalate " " $ 676 | [ "completed" | completed ] <> [ "editing" | editing ] 677 | ] 678 | [ div_ 679 | [ class_ "view" ] 680 | [ input_ 681 | [ class_ "toggle" 682 | , type_ "checkbox" 683 | , checked_ completed 684 | , onClick $ Check eid (not completed) 685 | ] 686 | , label_ 687 | [ onDoubleClick $ EditingEntry eid True ] 688 | [ text description ] 689 | , button_ 690 | [ class_ "destroy" 691 | , onClick $ Delete eid 692 | ] [] 693 | ] 694 | , input_ 695 | [ class_ "edit" 696 | , value_ description 697 | , name_ "title" 698 | , id_ $ "todo-" <> S.ms eid 699 | , onInput $ UpdateEntry eid 700 | , onBlur $ EditingEntry eid False 701 | , onEnter $ EditingEntry eid False 702 | ] 703 | ] 704 | 705 | viewControls :: Model -> MisoString -> [ Entry ] -> View Msg 706 | viewControls model visibility entries = 707 | footer_ [ class_ "footer" 708 | , hidden_ (null entries) 709 | ] 710 | [ viewControlsCount entriesLeft 711 | , viewControlsFilters visibility 712 | , viewControlsClear model entriesCompleted 713 | ] 714 | where 715 | entriesCompleted = length . filter completed $ entries 716 | entriesLeft = length entries - entriesCompleted 717 | 718 | viewControlsCount :: Int -> View Msg 719 | viewControlsCount entriesLeft = 720 | span_ [ class_ "todo-count" ] 721 | [ strong_ [] [ text $ S.ms entriesLeft ] 722 | , text (item_ <> " left") 723 | ] 724 | where 725 | item_ = S.pack $ bool " items" " item" (entriesLeft == 1) 726 | 727 | viewControlsFilters :: MisoString -> View Msg 728 | viewControlsFilters visibility = 729 | ul_ 730 | [ class_ "filters" ] 731 | [ visibilitySwap "#/" "All" visibility 732 | , text " " 733 | , visibilitySwap "#/active" "Active" visibility 734 | , text " " 735 | , visibilitySwap "#/completed" "Completed" visibility 736 | ] 737 | 738 | visibilitySwap :: MisoString -> MisoString -> MisoString -> View Msg 739 | visibilitySwap uri visibility actualVisibility = 740 | li_ [ ] 741 | [ a_ [ href_ uri 742 | , class_ $ S.concat [ "selected" | visibility == actualVisibility ] 743 | , onClick (ChangeVisibility visibility) 744 | ] [ text visibility ] 745 | ] 746 | 747 | viewControlsClear :: Model -> Int -> View Msg 748 | viewControlsClear _ entriesCompleted = 749 | button_ 750 | [ class_ "clear-completed" 751 | , prop "hidden" (entriesCompleted == 0) 752 | , onClick DeleteComplete 753 | ] 754 | [ text $ "Clear completed (" <> S.ms entriesCompleted <> ")" ] 755 | 756 | viewInput :: Model -> MisoString -> View Msg 757 | viewInput _ task = 758 | header_ [ class_ "header" ] 759 | [ h1_ [] [ text "todos" ] 760 | , input_ 761 | [ class_ "new-todo" 762 | , placeholder_ "What needs to be done?" 763 | , autofocus_ True 764 | , value_ task 765 | , name_ "newTodo" 766 | , onInput UpdateField 767 | , onEnter Add 768 | ] 769 | ] 770 | 771 | onEnter :: Msg -> Attribute Msg 772 | onEnter action = 773 | onKeyDown $ bool NoOp action . (== KeyCode 13) 774 | 775 | infoFooter :: View Msg 776 | infoFooter = 777 | footer_ [ class_ "info" ] 778 | [ p_ [] [ text "Double-click to edit a todo" ] 779 | , p_ [] 780 | [ text "Written by " 781 | , a_ [ href_ "https://github.com/dmjio" ] [ text "David Johnson" ] 782 | ] 783 | , p_ [] 784 | [ text "Part of " 785 | , a_ [ href_ "http://todomvc.com" ] [ text "TodoMVC" ] 786 | ] 787 | ] 788 | ``` 789 | 790 | After all of this was set up we can finally get to work and try to build it. Inside the `ToDoMVC` directory open a `bash` shell and run: 791 | 792 | ``` 793 | cabal new-build ToDoMVC --ghcjs 794 | ``` 795 | 796 | Of course, don't forget to update the `cabal` package database by running `cabal new-update` beforehand. 797 | 798 | This process will take a while the first time because `cabal` has to download a lot of packages and compile them. On consecutive runs and for future projects this process will be much much faster and smoother. 799 | 800 | 801 | After `cabal` has finished building `ToDoMVC` we can find the results deeply nested within the newly created `new-dist` folder. Last line of `cabal` output should tell you the exact path. 802 | 803 | Inside of that folder you will find all the JavaScript files as well as `index.html` file which you can open to test your application. 804 | 805 | ## Final Thoughts 806 | 807 | So, this was a somewhat lenghty procedure but the thing is that for any serious project you need to get acquainted with this tools. It's not different for any other language. 808 | 809 | In some cases all of this things come nicely bundled and hidden from you (e.g. Visual Studio) but when you stumble upon some complicated problem regarding your tool chain there is no escape from actually learning about the tools you are using. 810 | 811 | --- 812 | 813 | It can be a pain to switch back and forth from `bash` to `PowerShell` so I recommend creating a `Makefile` for your project to ease your pain. 814 | 815 | I intend to make a more comprehensive tutorial in the future covering some nitty gritty details which aren't usually covered but until then here's an example `Makefile` from one of my projects to give you the general idea on how to expand on this guide. 816 | 817 | ```makefile 818 | .PHONY: js import import-bin server server-bin client client-bin client-bin-copy devdb testdb 819 | 820 | windows := $(filter Windows_NT,$(OS)) 821 | 822 | ################################################################################ 823 | 824 | static = ./Static 825 | schema = ./Shared/sql/schema 826 | 827 | jscdir = $(static)/jsc 828 | imgdir = $(static)/img 829 | cssdir = $(static)/css 830 | 831 | jsaddle = --constraint=\"miso +jsaddle\" 832 | project = --project-file=CookBook.project 833 | restart = --restart=CookBook.cabal --restart=CookBook.project 834 | 835 | ################################################################################ 836 | 837 | bishbash = $(if $(windows),bash -l -c "$(1)",$(1)) 838 | mkclient = cabal new-build exe:Client --ghcjs $(project) 839 | mkjscdir = mkdir -p $(jscdir) 840 | gtbindir = $(call bishbash,cabal-plan list-bin exe:Client) 841 | 842 | ################################################################################ 843 | 844 | js: client-bin client-bin-copy 845 | 846 | import: 847 | ghcid -c "cabal new-repl exe:Import $(project)" $(restart) 848 | 849 | import-bin: 850 | cabal new-build exe:Import $(project) 851 | 852 | server: 853 | ghcid -c "cabal new-repl exe:Server $(project)" $(restart) 854 | 855 | server-bin: 856 | cabal new-build exe:Server $(project) 857 | 858 | client: 859 | ghcid -c "cabal new-repl exe:Client $(project) $(jsaddle)" $(restart) 860 | 861 | client-bin: 862 | $(call bishbash,$(mkclient)) 863 | 864 | client-bin-copy: 865 | $(call bishbash,$(mkjscdir)) 866 | $(call bishbash,cp $(shell $(gtbindir)).jsexe/rts.js $(jscdir)) 867 | $(call bishbash,cp $(shell $(gtbindir)).jsexe/lib.js $(jscdir)) 868 | $(call bishbash,cp $(shell $(gtbindir)).jsexe/out.js $(jscdir)) 869 | $(call bishbash,cp $(shell $(gtbindir)).jsexe/runmain.js $(jscdir)/run.js) 870 | 871 | ################################################################################ 872 | 873 | devdb: 874 | psql -q -f $(schema)/prepare_dev.sql 875 | psql -q -f $(schema)/schema_dev.sql 876 | psql -q -f $(schema)/permissions_dev.sql 877 | 878 | testdb: 879 | psql -q -f $(schema)/prepare_test.sql 880 | psql -q -f $(schema)/schema_test.sql 881 | psql -q -f $(schema)/permissions_test.sql 882 | ``` 883 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haskell-Guide --------------------------------------------------------------------------------