nix-book/source/new-flake/haskell-flake/main.adoc0
Amy de Buitléir 3220cdc8dc temp
2025-09-15 14:48:15 +01:00

276 lines
8.1 KiB
Text

= Haskell
Start with an empty directory and create a git repository.
....
$ mkdir hello-haskell
$ cd hello-haskell
$ git init
....
== A simple Haskell program
Next, we'll create a simple Haskell program.
////
$ cp ../Main.hs .
////
[source,haskell,linenums]
.Main.hs
....
$# cat Main.hs
....
== Running the program manually (optional)
[NOTE]
====
In this section you will learn how to do some development tasks manually, the "hard" way.
This can help you understand the distinction between Nix's role and
the Haskell build system you've chosen.
Also, if you have a build problem but you're not sure if the fault is in
your flake definition or some other configuration file,
these commands can help you narrow it down.
But you may wish to skip to the <<_the_cabal_file,next section>>
and come back here later.
====
Before we package the program, let's verify that it runs. We're going to
need Haskell. By now you've probably figured out that we can write a
`flake.nix` and define a development shell that includes Haskell. We'll
do that shortly, but first I want to show you a handy shortcut. We can
launch a _temporary_ shell with any Nix packages we want. This is
convenient when you just want to try out some new software and you're
not sure if you'll use it again. It's also convenient when you're not
ready to write `flake.nix` (perhaps you're not sure what tools and
packages you need), and you want to experiment a bit first.
The command to enter a temporary shell is
`nix shell -p __installables__`
Where __installables__ are flakes and other types of packages that you need.
(You can learn more about these in the
https://nix.dev/manual/nix/stable/command-ref/new-cli/nix#installables[Nix manual].)
=== Some unsuitable shells
[NOTE]
====
In this section, we will try commands that fail in subtle ways.
Examining these failures will give you a much better understanding of Haskell development with Nix,
and help you avoid (or at least diagnose) similar problems in future.
If you're impatient, you can skip to the next section to see the right way to do it.
You can come back to this section later to learn more.
====
Let's enter a shell with the Glasgow Haskell Compiler ("ghc") and try to run the program.
....
$# echo '$ nix shell nixpkgs#ghc'
$# nix shell nixpkgs#ghc --command sh
$ runghc Main.hs # Fails
....
The error message tells us that we need the module `Network.HostName`.
That module is provided by the Haskell package called `hostname`.
Let's exit that shell and try again, this time adding the `hostname` package.
....
$# echo '$ exit'
$# echo '$ nix shell nixpkgs#ghc nixpkgs#hostname'
$# nix shell nixpkgs#ghc nixpkgs#hostname --command sh
$ runghc Main.hs # Fails
....
That reason that failed is that we asked for the wrong package.
The Nix package `hostname` isn't the Haskell package we wanted,
it's a different package entirely (an alias for `hostname-net-tools`.)
The package we want is in the _package set_ called `haskellPackages`, so we can refer to it as `haskellPackages.hostname`.
Let's try that again, with the correct package.
....
$# echo '$ exit'
$# echo '$ nix shell nixpkgs#ghc nixpkgs#haskellPackages.hostname'
$# nix shell nixpkgs#ghc nixpkgs#haskellPackages.hostname --command sh
$ runghc Main.hs # Fails
....
Now what's wrong?
The syntax we used in the `nix shell` command above is fine,
but it doesn't make the package _available to GHC_!
=== A suitable shell for a quick test
Now we will create a shell that can run the program.
When you need support for both a language and some of
its packages, it's best to use one of the Nix functions that are
specific to the programming language and build system.
For Haskell, we can use the `ghcWithPackages` function.
The command below is rather complex,
and a complete explanation would be rather lengthy.
[#haskell-nix-shell]
....
$# echo '$ nix shell --impure --expr '"'"'with import <nixpkgs> {}; haskellPackages.ghcWithPackages (p: [ p.hostname ])'"'"
$# nix shell --impure --expr 'with import <nixpkgs> {}; haskellPackages.ghcWithPackages (p: [ p.hostname ])' --command sh
$ runghc Main.hs
....
Success! Now we know the program works.
== The cabal file
It's time to write a Cabal file for this program.
This is just an ordinary Cabal file; we don't need to do anything special for Nix.
////
$ cp ../hello-flake-haskell.cabal .
////
[source,cabal,linenums]
.hello-flake-haskell.cabal
....
$# cat hello-flake-haskell.cabal
....
== Building the program manually (optional)
[NOTE]
====
In this section you will learn how to do some development tasks manually, the "hard" way.
This can help you understand the distinction between Nix's role and
the Haskell build system you've chosen.
Also, if you have a build problem but you're not sure if the fault is in
your flake definition or some other configuration file,
these commands can help you narrow it down.
But you may wish to skip to the <<haskell-flake,next section>>
and come back here later.
====
We won't write `flake.nix` just yet.
First we'll try building the package manually.
(If you didn't run the `nix shell` command from <<haskell-nix-shell,earlier>>, do so now.)
....
$ cabal build
....
The `cabal` command is provided by the `cabal-install` package.
The error happens because we don't have `cabal-install` available
in the temporary shell.
We can correct that.
....
$# echo '$ nix shell --impure --expr '"'"'with import <nixpkgs> {}; haskellPackages.ghcWithPackages (p: [ p.hostname p.cabal-install ])'"'"
$# nix shell --impure --expr 'with import <nixpkgs> {}; haskellPackages.ghcWithPackages (p: [ p.hostname p.cabal-install ])' --command sh
....
Note that we're now inside a temporary shell inside the previous
temporary shell! To get back to the original shell, we have to `exit`
twice.
Alternatively, we could have done `exit` followed by the
second `nix-shell` command.
....
$# echo '$ cabal build'
$# echo ". . ."
$# cabal build |& tail -n 4
....
After a lot of output messages, the build succeeds.
[#haskell-flake]
== The Nix flake
Now we should write `flake.nix`.
We already know how to write most of the flake from the examples we did earlier.
The two parts that will be different are the development shell and the package builder.
However, there's a _much_ simpler way, using `haskell-flake`.
The following command will create `flake.nix` based on their template.
....
$ nix flake init -t github:srid/haskell-flake
....
Examining the flake,
you'll notice that it is well-commented.
The only thing we need to change for now is the name in `packages.default`;
I've highlighted the change below.
[source,nix,linenums,highlight=55]
.flake.nix
....
$# sed -i 's/example/hello-flake-haskell/' flake.nix
$# cat flake.nix
....
We also need a `LICENSE` file.
For now, this can be empty.
....
$ touch LICENSE
....
== Building the program
Let's try out the new flake.
Nix flakes only "`see`" files that are
part of the repository. We need to add all of the important files to the
repo before building or running the flake.
....
$ git add flake.nix hello-flake-haskell.cabal LICENSE Main.hs
$ nix build
....
We'll deal with those warnings later.
The important thing for now is that the build succeeded.
== Running the program
Now we can run the program.
....
$ nix run
....
By the way, we didn't need to do `nix build` earlier.
The `nix run` command will first build the program for us if needed.
We'd like to share this package with others, but first we should do some
cleanup.
It's time to deal with those warnings.
When the package was built, Nix created a `flake.lock` file.
We need to add this to the repo, and commit all important files.
....
$ git add flake.lock
$ git commit -a -m 'initial commit'
....
You can test that your package is properly configured by going to
another directory and running it from there.
....
$ cd ..
$ nix run ./hello-haskell
....
If you move the project to a public repo, anyone can run it. Recall from
the beginning of the tutorial that you were able to run `hello-flake`
directly from my repo with the following command.
....
nix run "git+https://codeberg.org/mhwombat/hello-flake"
....
Modify the URL accordingly and invite someone else to run your new
Haskell flake.
////
Good adoc0 scripts clean up after themselves.
$ rm -rf hello-haskell # clean up
////