Dear Haskell it's not you, it's your tooling.

· Read in about 9 min · (1751 Words)
Dear Haskell; yes you have some bad parts. String ahem. But for the most part I enjoy learning to write you. See, the problem is your friends; they are extremely agitating.

Stack does good work sometimes, but seems to prefer making very unreasonable choices:

Config hell

The default project template has three config files: config-hell.cabal package.yaml, stack.yaml. So if you want to add a dependency, how pray tell would you do it? Let’s look at the config files and see if there’s a clear way.

stack.yaml

Well the tool we used was stack so let’s start in stack.yaml. In stack.yaml the obvious choices are packages and extra-deps array.

The comments above packages read:

# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# - location:
#    git: https://github.com/commercialhaskell/stack.git
#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#  subdirs:
#  - auto-update
#  - wai

So packages are local directories and other addressable content. Not what we’re looking for.

What about extra-deps:

# Dependency packages to be pulled from upstream that are not in the resolver
# using the same syntax as the packages field.
# (e.g., acme-missiles-0.3)
# extra-deps: []

This looks more like what we’re looking for, but what the fuck is a resolver. At the top of stack.yaml it reads:

# Resolver to choose a 'specific' stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# resolver: lts-3.5
# resolver: nightly-2015-09-21
# resolver: ghc-7.10.2
# resolver: ghcjs-0.1.0_ghc-7.10.2
#
# The location of a snapshot can be provided as a file or url. Stack assumes
# a snapshot provided as a file might change, whereas a url resource does not.
#
# resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-11.9

Okay so a resolver version is like a Linux distribution release version.

We still haven’t figured out how to add a package as dependency. What’s left stack.yaml?

# Extra package databases containing global packages
# extra-package-dbs: []

Is this a like a ppa?\

<off_topic>

God I’m having flash backs to my Ubuntu days.
And Moses said thou shalt use Arch or a distribution no one has ever heard of, and the people were happy until the people realized they were doing a full clone of every *-git aur package.\

</off_topic>
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]

Non Haskell dependencies?

Well that didn’t work, maybe the next config file.

package.yaml

Success an array called dependencies it has config hell in it, but it works! I can add - lens and run stack ghci followed by import Control.Lens without an error.

    dependencies:
    - config-hell
    - lens

So what is config-hell.cabal?

As it turns out package.yaml is just an alternative format for config-hell.cabal provided by hpack.

Putting It all together

So, we have stack which is like a distribution of Haskell packages. If you want to use a package from a stack resolver/distribution then you put list it as a dependency in hpack’s package.yaml or cabal’s config-hell.cabal file, if your not using hpack. If you want a package that’s not in the resolver/distribution you list it in extra-deps in stack.yaml. Hey, that one made sense! You use the stack command to manage hpack which manages cabal to manage cabal directly. Some stack templates use hpack some don’t. Wait what’s a template?

Templates

because it’s not enough to have one over complicated build system.

Templates are just lightly configured projects. Running stack templates prints this beautifully documented list.

Template                    Description
chrisdone
foundation                - Project based on an alternative prelude with batteries and no dependencies.
franklinchen
ghcjs                     - Haskell to JavaScript compiler, based on GHC
ghcjs-old-base
hakyll-template           - a static website compiler library
haskeleton                - a project skeleton for Haskell packages
hspec                     - a testing framework for Haskell inspired by the Ruby library RSpec
new-template
protolude                 - Project using a custom Prelude based on the Protolude library
quickcheck-test-framework - a library for random testing of program properties
readme-lhs                - small scale, quick start, literate haskell projects
rio
rubik
scotty-hello-world
scotty-hspec-wai
servant                   - a set of packages for declaring web APIs at the type-level
servant-docker
simple
simple-hpack
simple-library
spock                     - a lightweight web framework
tasty-discover            - a project with tasty-discover with setup
tasty-travis
unicode-syntax-exe
unicode-syntax-lib
yesod-minimal
yesod-mongo
yesod-mysql
yesod-postgres
yesod-simple
yesod-sqlite

As far as I can tell this is the only documentation for each template.

Running stack new project-name template-name constructs a new project using the named template. Some templates use hpack, some don’t. While I understand where they were going with this, the lack of documentation, consistent configuration standards, and breath of choice make this a bit of a nightmare for people starting out. stack new test and stack new test simple are identical, while stack new test simple-hpack is mostly identical. Both simple and simple-hpack use hpack. The assumption that users will realize simple is the default template is a pretty good one, but why make people assume at all? Why not say the default is simple in the list of templates?

Enough with templates! hspec looked cool. Let’s try it out.

Inconsistent and suppressing errors

Okay let’s say I have a simple project (we’ll use the default templates code) and we want to try hspec tests on it. We will make it with stack new someFunc.

someFunc
├── app
│  └── Main.hs
├── ChangeLog.md
├── LICENSE
├── package.yaml
├── README.md
├── Setup.hs
├── someFunc.cabal
├── src
│  └── Lib.hs
├── stack.yaml
└── test
   └── Spec.hs

Our project someFunc has two code files: someFunc/app/Main.hs, and someFunc/src/Lib.hs.

Main.hs
module Main where
import Lib
main :: IO ()
main = someFunc
Lib.hs
module Lib
    ( someFunc
    ) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"

let’s take a look at the hspec template. stack new someFunc-hspec hspec creates:

someFunc-hspec
├── app
│  └── Main.hs
├── LICENSE
├── README.md
├── Setup.hs
├── someFunc-hspec.cabal
├── src
│  └── Data
│     └── String
│        └── Strip.hs
├── stack.yaml
└── test
   ├── Data
   │  └── String
   │     └── StripSpec.hs
   └── Spec.hs
Main.hs
module Main where
import Data.String.Strip
main :: IO ()
main = interact strip
Strip.hs
module Data.String.Strip (strip)  where
import Data.Char
strip :: String -> String
strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse
StripSpec.hs
module Data.String.StripSpec (main, spec) where
import Test.Hspec
import Test.QuickCheck
import Data.String.Strip
-- `main` is here so that this module can be run from GHCi on its own.  It is
-- not needed for automatic spec discovery.
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
  describe "strip" $ do
    it "removes leading and trailing whitespace" $ do
      strip "\t  foo bar\n" `shouldBe` "foo bar"
    it "is idempotent" $ property $
      \str -> strip str === strip (strip str)

stack test produces a successful test.

Cool, but I want to test someFunc, so we’ll copy the code files from someFunc into someFunc-hspec and delete the template’s code and tests for now.

cp someFunc/app/Main.hs someFunc-hspec/app/Main.hs
cp someFunc/src/Lib.hs someFunc-hspec/src

rm -rf someFunc-hspec/src/Data
rm -rf someFunc-hspec/test/Data

cd someFunc-hspec

To get

.
├── app
│  └── Main.hs
├── LICENSE
├── README.md
├── Setup.hs
├── someFunc-hspec.cabal
├── src
│  └── Lib.hs
├── stack.yaml
└── test
   └── Spec.hs

Now we’ll see if everything worked out with stack ghci.

someFunc-hspec-0.1.0.0: initial-build-steps (lib + exe)
The following GHC options are incompatible with GHCi and have not been passed to it: -threaded
Configuring GHCi with the following packages: someFunc-hspec
Using main module: 1. Package `someFunc-hspec' component exe:someFunc-hspec with main-is file: /home/host/haskell-hell/someFunc-hspec/app/Main.hs
GHCi, version 8.2.2: http://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Lib              ( /home/host/haskell-hell/someFunc-hspec/src/Lib.hs, interpreted )
[2 of 2] Compiling Main             ( /home/host/haskell-hell/someFunc-hspec/app/Main.hs, interpreted )
Ok, two modules loaded.
Loaded GHCi configuration from /tmp/haskell-stack-ghci/433033ec/ghci-script
*Main> main
someFunc
*Main>

Great, it all worked!

So, now we’ll build it with stack build

someFunc-hspec-0.1.0.0: build (lib + exe)
Preprocessing library for someFunc-hspec-0.1.0.0..
Cabal-simple_mPHDZzAJ_2.0.1.0_ghc-8.2.2: can't find source for
Data/String/Strip in src,
.stack-work/dist/x86_64-linux-tinfo6/Cabal-2.0.1.0/build/autogen,
.stack-work/dist/x86_64-linux-tinfo6/Cabal-2.0.1.0/build/global-autogen
--  While building custom Setup.hs for package someFunc-hspec-0.1.0.0
using:
      /home/host/.stack/setup-exe-cache/x86_64-linux-tinfo6/Cabal-simple_mPHDZzAJ_2.0.1.0_ghc-8.2.2 --builddir=.stack-work/dist/x86_64-linux-tinfo6/Cabal-2.0.1.0 build lib:someFunc-hspec exe:someFunc-hspec --ghc-options " -ddump-hi -ddump-to-file -fdiagnostics-color=always"
          Process exited with code: ExitFailure 1

The code works in ghci, but now with stack build.

rg  'Strip'
someFunc-hspec.cabal
18:  exposed-modules:     Data.String.Strip

So ghci doesn’t read the cabal config. If we replace Data.String.Strip with Lib. stack build is successful.

Let’s make a test for someFunc

First, we modify Lib.hs by making it return a string not IO. This is need necessary to test it as IO is very difficult to test.

Lib.hs
module Lib
    ( someFunc
    ) where

someFunc :: String
someFunc = "someFunc"

Then we add putStrLn to Main.hs

Main.hs
module Main where

import Lib

main :: IO ()
main = putStrLn someFunc

Now we have a passing test of someFunc!

A Comparison with rust

cargo only has two project templates binary, and library. cargo new some_func uses the binary template.

some_func
├── Cargo.toml
└── src
   └── main.rs

We will add a lib.rs file to src containing the function and it’s test.

lib.rs
/// some_func returns "some_func"
pub fn some_func() -> &'static str {
    "some_func"
}

#[test]
fn test_some_func() {
   assert_eq!(some_func(), "some_func");
}

Then we’ll modify main.rs.

main.rs
mod lib;
use lib::some_func;

fn main() {
    println!("{}", some_func());
}

Now cargo test passes!

You’ll notice there is only one config file.

Cargo.toml
[package]
name = "some_func"
version = "0.1.0"
authors = ["Avi Dessauer <avi.the.coder@gmail.com>"]

[dependencies]

When we run cargo test or cargo build it generates Cargo.lock, While this is not as theoretically sound as stack or the cabal solver, in practice, I have doubts that any amount of time I spend on manually configuring package versions will ever match the time it takes to learn cabal and stack.

Disclaimer

I am not intending to criticize the work done on cabal or stack. Both solve hard problems. I intend to highlight making stack and cabal the default choice for new Haskell programmers' front loads complexity. I would venture that the primary reason Haskell is such a niche language is because of the prioritization of theoretical soundness at the expense of ease of use. If any one, and this is a big “If”, actually wants Haskell to become more main stream, perhaps try making easier, slightly less-sound tools like cargo. I am not a big fan of the adage ‘worse is better’, but some times the easy way is better than theoretically sound.

Also apologies for my attempt at humor