Skip to content

Instantly share code, notes, and snippets.

@drvink
Last active May 21, 2022 20:52
Show Gist options
  • Save drvink/3a2770610e014deac306 to your computer and use it in GitHub Desktop.
Save drvink/3a2770610e014deac306 to your computer and use it in GitHub Desktop.
on nix: a language crash course for ml and haskell programmers
# locale bug workaround
[ -e "$HOME/.nix-profile/lib/locale/locale-archive" ] \
&& export LOCALE_ARCHIVE="$HOME/.nix-profile/lib/locale/locale-archive"
# needed for GHC, Cabal, etc. to find stuff properly
# requires nix's patched ghc-paths package
if [ -e "$HOME/.nix-profile/bin/ghc" ]; then
export NIX_GHC="$HOME/.nix-profile/bin/ghc"
export NIX_GHCPKG="$HOME/.nix-profile/bin/ghc-pkg"
export NIX_GHC_DOCDIR="$HOME/.nix-profile/share/doc/ghc/html"
export NIX_GHC_LIBDIR="$HOME/.nix-profile/lib/ghc-$($NIX_GHC --numeric-version)"
fi
if [ -d "$HOME/.nix-profile" ]; then
export C_INCLUDE_PATH="$HOME/.nix-profile/include"
export LIBRARY_PATH="$HOME/.nix-profile/lib"
fi
# Read this file first!
# This is the file we will be using as our entry point (i.e.
# "nix-env -f custom-packages.nix -i".
# Introduction
# ============
# nix's syntax is not very friendly; while somewhat ML-like, it diverges in
# unfortunate ways that confuse both ML and curly brace language users. The nix
# manual is good enough at explaining how to do things like writing a simple
# "package", but fails to explain the syntax and semantics of the language,
# which leads to confusion: what does this magic stuff at the top of the file
# actually mean, and how does evaluation flow through a typical program? The
# goal of this document is to explain the operational semantics enough so that
# an ML or Haskell user will easily understand the code, allowing them to focus
# on the concepts, which we leave to the manual. The file also demonstrates a
# conceptually challenging override of the standard derivation-building function
# for Haskell packages; its understanding is left as an exercise for the reader.
# Nearly every common language construct is covered, but tersely. The following
# article goes into even further detail and is highly recommended reading:
#
# https://medium.com/@MrJamesFisher/nix-by-example-a0063a1a4c55
# We will use # comments for our "literate nix", and C-style comments to
# annotate the purpose of the code. Without further ado, we will begin
# describing the language elements as we come across them.
# Functions and sets
# ==================
# A comma-delimited brace list followed by a colon is basically a function
# accepting a single record as its argument. The record will be destructured
# into name bindings a la pattern matching. Note that when constructing a
# record, the values are *semicolon*-delimited, and you must label each one as
# you would with a record. Like a record, the items can be in any order.
# Recursive records can be constructed by placing "rec" before the opening
# brace. nix calls records "sets", though they are much closer to immutable
# records than they are sets. As for mutability, there is none: nix has no
# equivalent to "mutable" or ref/IORef anywhere in the language.
# If not provided by the caller, a function can provide a default value for a
# set member by using the "name ? val" syntax as shown below. Since this is our
# entry point, we must declare defaults for all arguments.
{ pkgs ? (import <nixpkgs> {})
, system ? builtins.currentSystem }:
# We are now in the body of the function, but first, we will explain...
# Imports and paths
# =================
# "import" loads a nix expression from a file and returns its value. <name>
# uses the nix search path (the environment variable NIX_PATH) to provide a path
# corresponding to the key "name". Paths can also be directly specified; the
# following are all considered paths:
#
# ~/sup
# /homie
# ./hober
# http://nixos.org
#
# Note that paths are written EXACTLY as shown above--they are not quoted. Also
# note that despite the appearance of interchangability, paths and strings are
# distinct types with differing semantics. For example, if a path corresponding
# to a directory is passed to import, import will implicitly try to load the
# file "default.nix" from that directory. This expansion does not occur if a
# string is provided instead. Paths are not limited to use with just import; in
# fact, they have special interaction with regard to the nix datastore (the
# details of which we leave to the manual for explanation).
# In this case, the file that was imported is returning a function. We
# immediately call it with an argument of an empty set, which is used similarly
# to the unit type in ML.
# The builtins module is always present without needing any sort of import.
# "let" bindings
# ==============
# "let" is equivalent to let in Haskell. nix is lazy, so be careful to not
# introduce cycles. A let must be accompanied by a scoping "in"--there are no
# module-level bindings in nix. All of the following are valid:
#
# let add = { x, y }: x + y; in ...
#
# let x =
# let y = 1 + 2;
# z = 99; in
# y + z;
# in ...
# @ can be used to bind a name to a set passed to a function:
#
# let args@{ a, b }: ...
# This can be used to permit additional, unspecified members to be in the set
# (the ... below is literally an ellipsis):
#
# let args@{ a, b, ... }:
# Functions can also be curried and partially applied:
#
# let x =
# let add = a: b: a + b;
# add1 = add 1; in
# add1 2; # x == 3
# in ...
# Scoping with "with"
# ===================
# "with expr" introduces the contents of expr (which is generally a set) into a
# lexical scope. It could apply to everything following it at the module level,
# like "open" in OCaml:
#
# { a, b, c }:
# with b; # *contents* of b are opened into everything below
# Or it can be used like OCaml's "let open Some_module in":
#
# let x =
# with a_set_containing_qqq; #
# let y = 1 + 2;
# z = 99; in
# y + z + qqq;
# in ...
# Trial and error is encouraged to get a feel for how it works. Since "import"
# is an expression, you can also write things like "with import ./a_path {}".
# More examples:
#
# let f = x: y: (with a_set; x + y); in ...
# let f = with a_set; { x = 1 }; in ...
# let f = x: y: with a_set; x + y; in ...
let
self = with pkgs; {
# Inherit (no relation to object-oriented stuff)
# ==============================================
# Inside a set, inherit is equivalent to "label = label"; e.g. if "label" is
# in our lexical scope and has a value of 1, then "label" in the set we're
# constructing will be 1.
inherit
cabal2nix
glibcLocales /* work around https://github.com/NixOS/nix/issues/599 */
nix-prefetch-scripts;
mdl-haskell-env =
with pkgs.haskellPackages;
let
# Lists are space-delimited. ++ concatenates lists; nix has no cons
# operator. + concatenates strings or paths. Note that lists are not
# lazy!
pkgs = [
attoparsec hashtables haskell-src-exts ListLike mono-traversable
optparse-applicative parsec prelude-extras process QuickCheck
regex-base regex-compat-tdfa regex-posix regex-tdfa split text vector
];
tools = [
alex cabal-install cpphs ghc-mod ghci-ng happy haskell-docs hscolour
]; in
ghcWithPackages (hp: pkgs ++ tools);
}; in
# All nix programs ("expressions") must eventually return either a "derivation"
# (a special type which describes something to be installed) or a set of
# derivations. Refer to the manual for an explanation of the derivation type.
self
{ pkgs }:
{
callPackage = args: pkgs.lib.callPackageWith (pkgs // args);
}
# This file is actually named "~/.nixpkgs/config.nix". It is imported by
# nixpkgs/pkgs/top-level/all-packages.nix.
{ pkgs }:
# The function will return a set.
{
# A set element is of the form "name = value;". nix does not permit eliding
# semicolons anywhere one is required, similar to the syntax of BIND's
# named.conf.
allowUnfree = true;
# While there is nothing inherently special about a set member named
# "haskellPackageOverrides", ~/.nixpkgs/config.nix is the canonical place to
# put a number of different things; read all-packages.nix to see how it works.
#
# nix often uses a pattern where a function takes two sets as an argument and
# returns another set, where the intent is that the function's caller will
# merge the returned set with the set that was passed with the name "super".
# The name "self" is used to indicate the post-override set. Note that these
# names are simply convention; they are not special. Careless use of self can
# lead to infinite recursion.
haskellPackageOverrides = self: super: with pkgs.haskell.lib; {
# // gives the union of a set, with the RHS shadowing any names present in
# the LHS.
mkDerivation = args: super.mkDerivation (args // {
doCheck = false;
enableLibraryProfiling = true;
hyperlinkSource = true;
/*
* XXX HACK: The ghc-mod override in
* pkgs/development/haskell-modules/configuration-common.nix drags in a
* ton of extra packages by declaring emacs as an executable tool
* dependency; we work around it by avoiding compilation of the elisp
* files, but it would be preferable if we could compile them iff emacs is
* present on the machine. We have to do the override here instead of in
* the ghc-mod block because the problematic overrides are applied *after*
* the ones in the ghc-mod block.
*/
executableToolDepends =
# "set.attr or val" returns attr if present, otherwise the RHS of "or".
# "or val" is optional; an exception will be raised if the attribute
# does not exist, and nix will terminate if the exception is not caught.
let xs = args.executableToolDepends or []; in
if args.pname != "ghc-mod" then xs else
# "builtins" is always present.
builtins.filter (x: x != pkgs.emacs) xs;
postInstall =
let pi = args.postInstall or ""; in
# '' introduces a multi-line string. nix-level variables can be
# interpolated with ${}.
if args.pname != "ghc-mod" then pi else ''
local lispdir=( "$out/share/"*"-${self.ghc.name}/${args.pname}-${args.version}/elisp" )
# make -C $lispdir
mkdir -p $out/share/emacs/site-lisp
#ln -s "$lispdir/"*.el{,c} $out/share/emacs/site-lisp/
ln -s "$lispdir/"*.el $out/share/emacs/site-lisp/
'';
});
ghc-mod = overrideCabal super.ghc-mod (drv: {
src = pkgs.fetchFromGitHub {
owner = "kazu-yamamoto";
repo = "ghc-mod";
rev = "8a2e490592d24c91198170b7ad047f0a24806f6f";
# These weird-looking hashes are base32-encoded with nix-hash.
sha256 = "1mznkfrlf0kqir5aai6gs77n7dp7kcvccmplicf56812fjfyk1y6";
};
});
ghci-ng = overrideCabal super.ghci-ng (drv: {
src = pkgs.fetchFromGitHub {
owner = "chrisdone";
repo = "ghci-ng";
rev = "738f66f3d1f1a3b7ba574fb9c83da793179a42c3";
sha256 = "0flawgqdn01axkzac3zkzzqgbjm7z7b4y0xwcvr360x1fbnmmp52";
};
executableHaskellDepends =
let deps = with super; [ containers syb time transformers ];
in drv.executableHaskellDepends ++ deps;
version = "0.0.0";
homepage = "https://github.com/chrisdone/ghci-ng";
description = "Next generation GHCi";
});
haskell-docs = overrideCabal super.haskell-docs (drv: {
src = pkgs.fetchFromGitHub {
owner = "chrisdone";
repo = "haskell-docs";
rev = "431e08e422c12f684712d4d8abd67ce0d73b9505";
sha256 = "0s3zzn9p418n4a2kgrwc18mcmravwlag5c1djdi19lpj4k1qxk8j";
};
patches = [ ~/nix/patches/haskell-docs/0001-packagename-type-ambiguity.patch ];
});
};
packageOverrides = super: let self = super.pkgs; in {
bitlbee = pkgs.lib.overrideDerivation super.bitlbee (drv: {
src = pkgs.fetchFromGitHub {
owner = "bitlbee";
repo = "bitlbee";
rev = "b6a3fbf3b94d1e7e7aee82375661fc0934fec48b";
sha256 = "0zrs5ar1y8jdj3mjjz9gdznz28cpfis1hjy9b2yx9nqvm1im7mvi";
};
});
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment