diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,131 @@
+# Builds and tests this Haskell project on "GitHub Actions"
+#
+# 2021  Rudy Matela
+#
+# some docs: https://github.com/haskell/actions/tree/main/setup
+name: build
+on: [push]
+jobs:
+  build-and-test:
+    runs-on: ubuntu-latest
+    steps:
+
+      - name: Cache ~/.cabal/packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.cabal/packages
+          key:          v1-${{ runner.os }}-cabal-packages-${{ hashFiles('hello.cabal') }}
+          restore-keys: v1-${{ runner.os }}-cabal-packages-
+
+      - name: Cache ~/.cabal and ~/.ghc
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cabal
+            !~/.cabal/packages
+            ~/.ghc
+          key:          v1-${{ runner.os }}-cabal-ghc-latest-${{ hashFiles('hello.cabal') }}
+          restore-keys: v1-${{ runner.os }}-cabal-ghc-latest-
+
+      - run: du -hd3 ~/.cabal ~/.ghc || true
+
+      - run: haddock --version || sudo apt-get install ghc-haddock
+      - run: ghc   --version
+      - run: cabal --version
+
+      - name: Check out repository
+        uses: actions/checkout@v2
+
+      - run: cabal update
+      - run: make install-dependencies
+
+      - run: make
+      - run: make test
+      - run: make haddock
+      - run: make test-sdist
+      - run: make test-via-cabal
+
+
+  test-with-ghc:
+    strategy:
+      matrix:
+        ghc:
+          - '9.0'
+          - '8.10'
+          - '8.8'
+          - '8.6'
+          - '8.4'
+          - '8.2'
+          - '8.0'
+          - '7.10'
+          - '7.8'
+    runs-on: ubuntu-latest
+    needs: build-and-test
+    container: haskell:${{ matrix.ghc }}
+    steps:
+      - name: Cache ~/.cabal/packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.cabal/packages
+          key:          v1-${{ runner.os }}-cabal-packages-${{ hashFiles('hello.cabal') }}
+          restore-keys: v1-${{ runner.os }}-cabal-packages-
+
+      - name: Cache ~/.cabal and ~/.ghc
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cabal
+            !~/.cabal/packages
+            ~/.ghc
+          key:          v1-${{ runner.os }}-cabal-ghc-${{ matrix.ghc }}-${{ hashFiles('hello.cabal') }}
+          restore-keys: v1-${{ runner.os }}-cabal-ghc-${{ matrix.ghc }}-
+
+      - run: du -hd3 ~/.cabal ~/.ghc || true
+
+      - run: make --version || rm /etc/apt/sources.list.d/*.list # faster update
+      - run: make --version || apt-get update
+      - run: make --version || apt-get install make
+
+      - run: ghc   --version
+      - run: cabal --version
+
+      - name: Check out repository
+        uses: actions/checkout@v2
+
+      - run: cabal update
+      - run: make install-dependencies
+
+      - run: make
+      - run: make test
+      - run: make haddock
+      - run: make test-sdist
+      - run: make test-via-cabal
+
+  test-with-stack:
+    runs-on: ubuntu-latest
+    needs: build-and-test
+    steps:
+      - name: Setup Haskell's GHC and Cabal as required by current Stackage LTS
+        uses: haskell/actions/setup@v1
+        with: # lts-17.9
+          ghc-version: '8.10.4'
+          cabal-version: '3.2'
+
+      - uses: actions/cache@v2
+        with:
+          path: ~/.stack
+          key:          v1-${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }}
+          restore-keys: v1-${{ runner.os }}-stack-
+
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - run: make test-via-stack
+
+  test-with-hugs:
+    runs-on: ubuntu-latest
+    needs: build-and-test
+    steps:
+      - run: sudo apt-get install hugs
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - run: make hugs-test
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -32,11 +32,11 @@
   $(EGS)
 GHCIMPORTDIRS = src:test
 GHCFLAGS = -O2 $(shell grep -q "Arch Linux" /etc/lsb-release && echo -dynamic)
-HADDOCKFLAGS = --no-print-missing-docs
 HUGSIMPORTDIRS = .:./src:./test:./etc/hugs-backports:/usr/lib/hugs/packages/*
 HUGSFLAGS = -98 -h32M
 RUNPARAMETERS =
 LIB_DEPS = base template-haskell
+INSTALL_DEPS = leancheck
 
 all: mk/toplibs
 
@@ -73,10 +73,12 @@
 	./test/sdist
 
 test-via-cabal:
-	cabal test
+	cabal configure --enable-tests --enable-benchmarks --ghc-options="$(GHCFLAGS) -O0"
+	cabal build
+	cabal test main
 
 test-via-stack:
-	stack test
+	stack test express:test:main --ghc-options="$(GHCFLAGS) -O0" --system-ghc --no-install-ghc --no-terminal
 
 diff-test: $(patsubst %,%.diff-test,$(BENCHS))
 
@@ -89,6 +91,9 @@
 	./$< >           test/model/$<.out
 
 test-via-everything: test test-via-cabal test-via-stack
+
+hugs-test:
+	echo 'TBA'
 
 legacy-test: # needs ghc-8.8 .. ghc-7.8 installed as such
 	make clean  &&  make test -j GHC=ghc-8.8
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -366,8 +366,8 @@
 
 [express-logo]: https://github.com/rudymatela/express/raw/master/doc/express.svg?sanitize=true
 
-[build-status]: https://travis-ci.org/rudymatela/express.svg?branch=master
-[build-log]:    https://travis-ci.org/rudymatela/express
+[build-log]:    https://github.com/rudymatela/express/actions/workflows/build.yml
+[build-status]: https://github.com/rudymatela/express/actions/workflows/build.yml/badge.svg
 [hackage-version]: https://img.shields.io/hackage/v/express.svg
 [express-on-hackage]: https://hackage.haskell.org/package/express
 [stackage-lts-badge]:          https://stackage.org/package/express/badge/lts
diff --git a/express.cabal b/express.cabal
--- a/express.cabal
+++ b/express.cabal
@@ -1,6 +1,6 @@
 -- Cabal file for express
 name: express
-version: 0.1.6
+version: 0.1.8
 synopsis: Dynamically-typed expressions involving applications and variables.
 description:
   Express is a library for manipulating dynamically typed Haskell expressions.
@@ -26,6 +26,7 @@
                , doc/express.svg
 extra-source-files: .gitignore
                   , .travis.yml
+                  , .github/workflows/build.yml
                   , Makefile
                   , mk/depend.mk
                   , mk/haskell.mk
@@ -58,7 +59,7 @@
 source-repository this
   type:           git
   location:       https://github.com/rudymatela/express
-  tag:            v0.1.6
+  tag:            v0.1.8
 
 library
   exposed-modules:     Data.Express
diff --git a/mk/haskell.mk b/mk/haskell.mk
--- a/mk/haskell.mk
+++ b/mk/haskell.mk
@@ -21,6 +21,7 @@
 GHC ?= ghc
 GHCCMD = $(GHC) -i$(GHCIMPORTDIRS) $(GHCFLAGS)
 HADDOCK ?= haddock
+CABAL_INSTALL = $(shell cabal --version | grep -q "version [0-2]\." && echo 'cabal install' || echo 'cabal v1-install')
 
 # Hugs Parameters
 HUGSIMPORTDIRS ?= "/usr/lib/hugs/packages/*"
@@ -46,19 +47,13 @@
 
 LIB_DEPS ?= base
 ALL_DEPS ?= $(LIB_DEPS)
+INSTALL_DEPS ?=
 
 PKGNAME = $(shell cat *.cabal | grep "^name:"    | sed -e "s/name: *//")
-HADDOCK_VERSION = $(shell $(HADDOCK) --version | grep version | sed -e 's/.*version //;s/,.*//')
-HADDOCK_MAJOR = $(shell echo $(HADDOCK_VERSION) | sed -e 's/\..*//')
-HADDOCK_MINOR = $(shell echo $(HADDOCK_VERSION) | sed -e 's/[0-9]*\.\([0-9]*\).[0-9]*/\1/')
-HADDOCK_PKG_NAME = $(shell [ $(HADDOCK_MAJOR) -gt 2 ] \
-                        || [ $(HADDOCK_MAJOR) -eq 2 -a $(HADDOCK_MINOR) -ge 20 ] \
-                        && echo "--package-name=$(PKGNAME)")
-HADDOCK_HLNK_SRC = $(shell [ $(HADDOCK_MAJOR) -gt 2 ] \
-                        || [ $(HADDOCK_MAJOR) -eq 2 -a $(HADDOCK_MINOR) -ge 17 ] \
-                        && echo "--hyperlinked-source")
 
+HADDOCK_HAS = haddock --help | grep -q --
 
+
 # Implicit rules
 %.hi %.o: %.hs
 	$(GHCCMD) $< && touch $@
@@ -94,6 +89,9 @@
 depend:
 	find $(ALL_HSS) | ./mk/ghcdeps -i$(GHCIMPORTDIRS) $(GHCFLAGS) > $(DEPMK)
 
+install-dependencies:
+	$(CABAL_INSTALL) $(INSTALL_DEPS)
+
 # haddock rules
 haddock: doc/index.html
 
@@ -110,8 +108,9 @@
 	./mk/haddock-i $(LIB_DEPS) | xargs \
 	$(HADDOCK) --html -odoc $(LIB_HSS) \
 	  --title=$(PKGNAME) \
-	  $(HADDOCK_PKG_NAME) \
-	  $(HADDOCK_HLNK_SRC) \
+	  $(shell $(HADDOCK_HAS) --package-name          && echo "--package-name=$(PKGNAME)" ) \
+	  $(shell $(HADDOCK_HAS) --hyperlinked-source    && echo "--hyperlinked-source"      ) \
+	  $(shell $(HADDOCK_HAS) --no-print-missing-docs && echo --no-print-missing-docs     ) \
 	  $(HADDOCKFLAGS)
 
 clean-cabal:
diff --git a/src/Data/Express/Canon.hs b/src/Data/Express/Canon.hs
--- a/src/Data/Express/Canon.hs
+++ b/src/Data/Express/Canon.hs
@@ -171,23 +171,24 @@
 -- > [ord 'b' :: Int]
 --
 -- When applying this to expressions already containing variables
--- new variables are introduced so name clashes are avoided:
+-- clashes are avoided and these variables are not touched:
 --
 -- > > canonicalVariations $ i_ -+- ii -+- jj -+- i_
--- > [ x + y + z + x' :: Int
--- > , x + y + z + x :: Int ]
+-- > [ x + i + j + y :: Int
+-- > , x + i + j + y :: Int ]
 --
 -- > > canonicalVariations $ ii -+- jj
--- > [x + y :: Int]
+-- > [i + j :: Int]
 --
 -- > > canonicalVariations $ xx -+- i_ -+- i_ -+- length' (c_ -:- unit c_) -+- yy
--- > [ (((x + y) + z) + length (c:d:"")) + x' :: Int
--- > , (((x + y) + z) + length (c:c:"")) + x' :: Int
--- > , (((x + y) + y) + length (c:d:"")) + z :: Int
--- > , (((x + y) + y) + length (c:c:"")) + z :: Int
+-- > [ (((x + z) + x') + length (c:d:"")) + y :: Int
+-- > , (((x + z) + x') + length (c:c:"")) + y :: Int
+-- > , (((x + z) + z) + length (c:d:"")) + y :: Int
+-- > , (((x + z) + z) + length (c:c:"")) + y :: Int
 -- > ]
 canonicalVariations :: Expr -> [Expr]
-canonicalVariations = map canonicalize . fastCanonicalVariations
+canonicalVariations e  =  map (canonicalizeKeeping (nonHoleVars e))
+                       $  fastCanonicalVariations e
 
 -- |
 -- Returns the most general canonical variation of an 'Expr'
@@ -223,7 +224,8 @@
 -- This function is the same as taking the 'head' of 'canonicalVariations'
 -- but a bit faster.
 mostGeneralCanonicalVariation :: Expr -> Expr
-mostGeneralCanonicalVariation  =  canonicalize . fastMostGeneralVariation
+mostGeneralCanonicalVariation e  =  canonicalizeKeeping (nonHoleVars e)
+                                 $  fastMostGeneralVariation e
 
 -- |
 -- Returns the most specific canonical variation of an 'Expr'
@@ -259,7 +261,8 @@
 -- This function is the same as taking the 'last' of 'canonicalVariations'
 -- but a bit faster.
 mostSpecificCanonicalVariation :: Expr -> Expr
-mostSpecificCanonicalVariation  =  canonicalize . fastMostSpecificVariation
+mostSpecificCanonicalVariation e  =  canonicalizeKeeping (nonHoleVars e)
+                                  $  fastMostSpecificVariation e
 
 -- |
 -- A faster version of 'canonicalVariations' that
@@ -327,6 +330,31 @@
 
 -- |
 -- Variable names existing in a given Expr.
+--
 -- This function is not exported.
 varnames :: Expr -> [String]
 varnames e  =  [n | Value ('_':n) _ <- vars e]
+
+-- |
+-- Variables that are not holes.
+--
+-- This function is not exported.
+nonHoleVars :: Expr -> [Expr]
+nonHoleVars  =  filter (not . isHole) . nubVars
+
+-- | Canonicalizes an 'Expr' while keeping the given variables untouched.
+--
+-- > > canonicalizeKeeping [zz] (zz -+- ii -+- jj)
+-- > z + x + y :: Int
+--
+-- > > canonicalizeKeeping [ii,jj] (zz -+- ii -+- jj)
+-- > x + i + j :: Int
+--
+-- This function is not exported.
+canonicalizeKeeping :: [Expr] -> Expr -> Expr
+canonicalizeKeeping vs e  =  canonicalizeWith namesFor e
+  where
+  nm (Value ('_':n) _)  =  n
+  namesFor v | v `elem` vs  =  nm v : err
+             | otherwise    =  names' v \\ map nm vs
+  err  =  error "Data.Express.canonicalizeKeeping: the impossible happened.  This is definitely a bug."
diff --git a/src/Data/Express/Fixtures.hs b/src/Data/Express/Fixtures.hs
--- a/src/Data/Express/Fixtures.hs
+++ b/src/Data/Express/Fixtures.hs
@@ -115,6 +115,7 @@
   -- ** Integers
   , i_, xx, yy, zz, xx'
   , ii, jj, kk, ii'
+  , ll, mm, nn
   , zero, one, two, three, minusOne, minusTwo
   , idE, negateE, absE
   , idInt
@@ -164,6 +165,9 @@
   , elem'
   , sort'
   , insert'
+  , bs_, pps, qqs
+  , and', or'
+  , sum', product'
 
   -- ** Maybes
   , nothing
@@ -359,18 +363,56 @@
 xx' :: Expr
 xx'  =  var "x'" int
 
+
+-- | A variable @i@ of 'Int' type.
+--
+-- > > ii
+-- > i :: Int
 ii :: Expr
 ii  =  var "i" int
 
+-- | A variable @j@ of 'Int' type.
+--
+-- > > jj
+-- > j :: Int
 jj :: Expr
 jj  =  var "j" int
 
+-- | A variable @k@ of 'Int' type.
+--
+-- > > kk
+-- > k :: Int
 kk :: Expr
 kk  =  var "k" int
 
+-- | A variable @i'@ of 'Int' type.
+--
+-- > > ii'
+-- > i' :: Int
 ii' :: Expr
 ii'  =  var "i'" int
 
+-- | A variable @l@ of 'Int' type.
+--
+-- > > ll
+-- > l :: Int
+ll :: Expr
+ll  =  var "l" int
+
+-- | A variable @m@ of 'Int' type.
+--
+-- > > mm
+-- > m :: Int
+mm :: Expr
+mm  =  var "m" int
+
+-- | A variable @n@ of 'Int' type.
+--
+-- > > nn
+-- > n :: Int
+nn :: Expr
+nn  =  var "n" int
+
 -- | The value @0@ bound to the 'Int' type encoded as an 'Expr'.
 --
 -- > > zero
@@ -1098,6 +1140,75 @@
 sixtuple e1 e2 e3 e4 e5 e6 = cccccE :$ e1 :$ e2 :$ e3 :$ e4 :$ e5 :$ e6
   where
   cccccE = value ",,,,," ((,,,,,) :: Int -> Int -> Int -> Int -> Int -> Int -> (Int,Int,Int,Int,Int,Int))
+
+-- | A typed hole of @[Bool]@ type encoded as an 'Expr'.
+--
+-- > > bs_
+-- > _ :: [Bool]
+bs_ :: Expr
+bs_  =  hole [bool]
+
+-- | 'Expr' representing a variable @p' :: `[Bool]`@.
+--
+-- > > pps
+-- > ps :: [Bool]
+pps :: Expr
+pps  =  var "ps" [bool]
+
+-- | A typed hole of '[Bool]' type
+--
+-- > > qqs
+-- > qs :: [Bool]
+qqs :: Expr
+qqs  =  var "qs" [bool]
+
+-- | 'and' lifted over the 'Expr' type.
+--
+-- > > and' pps
+-- > and ps :: Bool
+--
+-- > > evl (and' $ expr [False,True]) :: Bool
+-- > False
+and' :: Expr -> Expr
+and' e  =  andE :$ e
+  where
+  andE  =  value "and" (and :: [Bool] -> Bool)
+
+-- | 'or' lifted over the 'Expr' type.
+--
+-- > > or' pps
+-- > or ps :: Bool
+--
+-- > > evl (or' $ expr [False,True]) :: Bool
+-- > True
+or' :: Expr -> Expr
+or' e  =  orE :$ e
+  where
+  orE  =  value "or" (or :: [Bool] -> Bool)
+
+-- | 'sum' of 'Int' elements lifted over the 'Expr' type.
+--
+-- > > sum' xxs
+-- > sum xs :: Int
+--
+-- > > evl (sum' $ expr [1,2,3::Int]) :: Int
+-- > 6
+sum' :: Expr -> Expr
+sum' e  =  sumE :$ e
+  where
+  sumE  =  value "sum" (sum :: [Int] -> Int)
+
+-- | 'product' of 'Int' elements lifted over the 'Expr' type.
+--
+-- > > product' xxs
+-- > product xs :: Int
+--
+-- > > evl (product' $ expr [1,2,3::Int]) :: Int
+-- > 6
+product' :: Expr -> Expr
+product' e  =  productE :$ e
+  where
+  productE  =  value "product" (product :: [Int] -> Int)
 
 headOr :: a -> [a] -> a
 headOr x []     =  x
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-15.5 # or ghc-8.8.3
+resolver: lts-17.9 # or ghc-8.10.4
 
 packages:
 - .
diff --git a/test/canon.hs b/test/canon.hs
--- a/test/canon.hs
+++ b/test/canon.hs
@@ -88,8 +88,11 @@
        , xx -+- xx -+- xx -+- xx
        ]
   , canonicalVariations (i_ -+- ii -+- jj -+- i_)
-    == [ xx -+- yy -+- zz -+- xx'
-       , xx -+- yy -+- zz -+- xx ]
+    == [ xx -+- ii -+- jj -+- yy
+       , xx -+- ii -+- jj -+- xx ]
+  , canonicalVariations (i_ -+- xx -+- yy -+- i_)
+    == [ zz -+- xx -+- yy -+- xx'
+       , zz -+- xx -+- yy -+- zz ]
 
 
   , holds n $ \e -> let xs = map (length . nubVars) $ canonicalVariations e
