diff --git a/som.cabal b/som.cabal
--- a/som.cabal
+++ b/som.cabal
@@ -1,5 +1,5 @@
 name:           som
-version:        6.5.1
+version:        7.0.0
 synopsis:       Self-Organising Maps
 description:    A Kohonen Self-organising Map (SOM) maps input patterns 
                 onto a regular grid (usually two-dimensional) where each
@@ -28,7 +28,6 @@
 library
   hs-source-dirs:  src
   build-depends:   base ==4.*,
-                   base-unicode-symbols ==0.2.*,
                    binary ==0.7.*,
                    containers ==0.5.*,
                    grid ==7.*,
@@ -49,7 +48,6 @@
                    test-framework ==0.8.*,
                    som,
                    grid ==7.*,
-                   base-unicode-symbols ==0.2.*,
                    MonadRandom ==0.1.*,
                    random ==1.0.*
   hs-source-dirs:  test
diff --git a/src/Data/Datamining/Clustering/DSOM.hs b/src/Data/Datamining/Clustering/DSOM.hs
--- a/src/Data/Datamining/Clustering/DSOM.hs
+++ b/src/Data/Datamining/Clustering/DSOM.hs
@@ -15,6 +15,9 @@
 --
 -- * Rougier, N. & Boniface, Y. (2011). Dynamic self-organising map.
 --   Neurocomputing, 74 (11), 1840-1847. 
+--
+-- * Kohonen, T. (1982). Self-organized formation of topologically 
+--   correct feature maps. Biological Cybernetics, 43 (1), 59–69.
 ------------------------------------------------------------------------
 
 module Data.Datamining.Clustering.DSOM
diff --git a/src/Data/Datamining/Clustering/SOM.hs b/src/Data/Datamining/Clustering/SOM.hs
--- a/src/Data/Datamining/Clustering/SOM.hs
+++ b/src/Data/Datamining/Clustering/SOM.hs
@@ -34,18 +34,13 @@
 --
 -- * Kohonen, T. (1982). Self-organized formation of topologically 
 --   correct feature maps. Biological Cybernetics, 43 (1), 59–69.
---
--- * Rougier, N. & Boniface, Y. (2011). Dynamic self-organising map.
---   Neurocomputing, 74 (11), 1840-1847. 
 ------------------------------------------------------------------------
 
 module Data.Datamining.Clustering.SOM
   (
     -- * Construction
-    SOM,
-    defaultSOM,
-    customSOM,
-    decayingGaussian,
+    SOM(..),
+    DecayingGaussian(..),
     -- * Deconstruction
     toGridMap,
     -- * Advanced control
@@ -55,7 +50,7 @@
     setCounter
   ) where
 
-import Data.Datamining.Clustering.SOMInternal (SOM, defaultSOM,
-  customSOM, decayingGaussian, toGridMap, trainNeighbourhood,
-  incrementCounter, counter, setCounter)
+import Data.Datamining.Clustering.SOMInternal (SOM(..),
+  DecayingGaussian(..), toGridMap, trainNeighbourhood, incrementCounter,
+  counter, setCounter)
 
diff --git a/src/Data/Datamining/Clustering/SOMInternal.hs b/src/Data/Datamining/Clustering/SOMInternal.hs
--- a/src/Data/Datamining/Clustering/SOMInternal.hs
+++ b/src/Data/Datamining/Clustering/SOMInternal.hs
@@ -26,6 +26,57 @@
 import GHC.Generics (Generic)
 import Prelude hiding (lookup)
 
+class LearningFunction f where
+  type LearningRate f
+  rate :: f -> Int -> Int -> (LearningRate f)
+
+-- | A typical learning function for classifiers.
+--   @'DecayingGaussian' r0 rf w0 wf tf@ returns a bell curve-shaped
+--   function. At time zero, the maximum learning rate (applied to the
+--   BMU) is @r0@, and the neighbourhood width is @w0@. Over time the
+--   bell curve shrinks and the learning rate tapers off, until at time
+--   @tf@, the maximum learning rate (applied to the BMU) is @rf@,
+--   and the neighbourhood width is @wf@. Normally the parameters
+--   should be chosen such that:
+--
+--   * 0 < rf << r0 < 1
+--
+--   * 0 < wf << w0
+--
+--   * 0 < tf
+--
+--   where << means "is much smaller than" (not the Haskell @<<@
+--   operator!)
+data DecayingGaussian a = DecayingGaussian a a a a Int
+  deriving (Eq, Show, Generic)
+
+instance (Floating a, Fractional a, Num a) =>
+         LearningFunction (DecayingGaussian a) where
+  type LearningRate (DecayingGaussian a) = a
+  rate (DecayingGaussian r0 rf w0 wf tf) t d = r * exp (-(d'*d')/(w*w))
+    where a = t'/tf'
+          r = r0 * ((rf/r0)**a)
+          w = w0 * ((wf/w0)**a)
+          t' = fromIntegral t
+          tf' = fromIntegral tf
+          d' = fromIntegral d
+
+-- | A learning function that only updates the BMU and has a constant
+--   learning rate.
+data StepFunction a = StepFunction a deriving (Eq, Show, Generic)
+
+instance (Fractional a) => LearningFunction (StepFunction a) where
+  type LearningRate (StepFunction a) = a
+  rate (StepFunction r) _ d = if d == 0 then r else 0.0
+
+-- | A learning function that updates all nodes with the same, constant
+--   learning rate. This can be useful for testing.
+data ConstantFunction a = ConstantFunction a deriving (Eq, Show, Generic)
+
+instance (Fractional a) => LearningFunction (ConstantFunction a) where
+  type LearningRate (ConstantFunction a) = a
+  rate (ConstantFunction r) _ _ = r
+
 -- | A Self-Organising Map (SOM).
 --
 --   Although @SOM@ implements @GridMap@, most users will only need the
@@ -39,19 +90,19 @@
 --      just return an @error@). It would be problematic to implement
 --      them because the input SOM and the output SOM would have to have
 --      the same @Metric@ type.
-data SOM gm k p = SOM
+data SOM f gm k p = SOM
   {
     sGridMap :: gm p,
-    sLearningFunction :: Int -> Int -> Metric p,
+    sLearningFunction :: f,
     sCounter :: Int
-  } deriving Generic
+  } deriving (Eq, Show, Generic)
 
-instance (F.Foldable gm) => F.Foldable (SOM gm k) where
+instance (F.Foldable gm) => F.Foldable (SOM f gm k) where
   foldr f x g = F.foldr f x (sGridMap g)
 
-instance (G.Grid (gm p)) => G.Grid (SOM gm k p) where
-  type Index (SOM gm k p) = G.Index (gm p)
-  type Direction (SOM gm k p) = G.Direction (gm p)
+instance (G.Grid (gm p)) => G.Grid (SOM f gm k p) where
+  type Index (SOM f gm k p) = G.Index (gm p)
+  type Direction (SOM f gm k p) = G.Direction (gm p)
   indices = G.indices . sGridMap
   distance = G.distance . sGridMap
   neighbours = G.neighbours . sGridMap
@@ -62,8 +113,8 @@
   null = G.null . sGridMap
   nonNull = G.nonNull . sGridMap
 
-instance (F.Foldable gm, GM.GridMap gm p, G.Grid (GM.BaseGrid gm p)) => GM.GridMap (SOM gm k) p where
-  type BaseGrid (SOM gm k) p = GM.BaseGrid gm p
+instance (F.Foldable gm, GM.GridMap gm p, G.Grid (GM.BaseGrid gm p)) => GM.GridMap (SOM f gm k) p where
+  type BaseGrid (SOM f gm k) p = GM.BaseGrid gm p
   toGrid = GM.toGrid . sGridMap
   toMap = GM.toMap . sGridMap
   mapWithKey = error "Not implemented"
@@ -71,11 +122,13 @@
     where gm = sGridMap s
           gm' = GM.adjustWithKey f k gm
 
-currentLearningFunction :: SOM gm k p -> (Int -> Metric p)
-currentLearningFunction s = (sLearningFunction s) (sCounter s)
+currentLearningFunction
+  :: (LearningFunction f, LearningRate f ~ Metric p)
+    => SOM f gm k p -> (Int -> Metric p)
+currentLearningFunction s = (rate . sLearningFunction $ s) (sCounter s)
 
 -- | Extracts the grid and current models from the SOM.
-toGridMap :: GM.GridMap gm p => SOM gm k p -> gm p
+toGridMap :: GM.GridMap gm p => SOM f gm k p -> gm p
 toGridMap = sGridMap
 
 adjustNode
@@ -90,28 +143,30 @@
 --   the BMU and trains it and its neighbourhood.
 trainNeighbourhood
   :: (Pattern p, G.Grid (gm p), GM.GridMap gm p,
-      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p)) =>
-     SOM gm k p -> G.Index (gm p) -> p -> SOM gm k p
+      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p), LearningFunction f,
+      LearningRate f ~ Metric p) =>
+     SOM f gm k p -> G.Index (gm p) -> p -> SOM f gm k p
 trainNeighbourhood s bmu target = s { sGridMap=gm' }
   where gm = sGridMap s
         gm' = GM.mapWithKey (adjustNode gm f target bmu) gm
         f = currentLearningFunction s
 
-incrementCounter :: SOM gm k p -> SOM gm k p
+incrementCounter :: SOM f gm k p -> SOM f gm k p
 incrementCounter s = setCounter (sCounter s + 1) s
 
-counter :: SOM gm k p -> Int
+counter :: SOM f gm k p -> Int
 counter = sCounter
 
-setCounter :: Int -> SOM gm k p -> SOM gm k p
+setCounter :: Int -> SOM f gm k p -> SOM f gm k p
 setCounter k s = s { sCounter = k }
 
 justTrain
   :: (Ord (Metric p), Pattern p, G.Grid (gm p),
       GM.GridMap gm (Metric p), GM.GridMap gm p,
       G.Index (GM.BaseGrid gm (Metric p)) ~ G.Index (gm p),
-      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p)) =>
-     SOM gm k p -> p -> SOM gm k p
+      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p), LearningFunction f,
+      LearningRate f ~ Metric p) =>
+     SOM f gm k p -> p -> SOM f gm k p
 justTrain s p = trainNeighbourhood s bmu p
   where ds = GM.toList . GM.map (p `difference`) . sGridMap $ s
         bmu = fst . minimumBy (comparing snd) $ ds
@@ -119,8 +174,9 @@
 instance
   (GM.GridMap gm p, k ~ G.Index (GM.BaseGrid gm p), Pattern p,
   G.Grid (gm p), GM.GridMap gm (Metric p), k ~ G.Index (gm p),
-  k ~ G.Index (GM.BaseGrid gm (Metric p)), Ord (Metric p)) =>
-    Classifier (SOM gm) k p where
+  k ~ G.Index (GM.BaseGrid gm (Metric p)), Ord (Metric p),
+  LearningFunction f, LearningRate f ~ Metric p) =>
+    Classifier (SOM f gm) k p where
   toList = GM.toList . sGridMap
   numModels = G.tileCount . sGridMap
   models = GM.elems . sGridMap
@@ -132,92 +188,56 @@
           s' = trainNeighbourhood s bmu p
 
 
--- | Creates a classifier with a default (bell-shaped) learning
---   function. Usage is @'defaultSOM' gm r0 rf w0 wf tf@, where:
---
---   [@gm@] The geometry and initial models for this classifier.
---   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
---   @'HexHexGrid'@, and @ps@ is a set of random patterns.
---
---   [@r0@] See description in @'decayingGaussian2'@.
---
---   [@rf@] See description in @'decayingGaussian2'@.
---
---   [@w0@] See description in @'decayingGaussian2'@.
---
---   [@wf@] See description in @'decayingGaussian2'@.
---
---   [@tf@] See description in @'decayingGaussian2'@.
-defaultSOM
-  :: Floating (Metric p) => gm p -> Metric p -> Metric p -> Metric p ->
-     Metric p -> Int -> SOM gm k p
-defaultSOM gm r0 rf w0 wf tf =
-  SOM {
-        sGridMap=gm,
-        sLearningFunction=decayingGaussian2 r0 rf w0 wf tf,
-        sCounter=0
-      }
-
--- | Creates a classifier with a custom learning function.
---   Usage is @'customSOM' gm g@, where:
---
---   [@gm@] The geometry and initial models for this classifier.
---   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
---   @'HexHexGrid'@, and @ps@ is a set of random patterns.
---
---   [@f@] A function used to adjust the models in the classifier.
---   This function will be invoked with two parameters.
---   The first parameter will indicate how many patterns (or pattern
---   batches) have previously been presented to this classifier.
---   Typically this is used to make the learning rate decay over time.
---   The second parameter to the function is the grid distance from
---   the node being updated to the BMU (Best Matching Unit).
---   The output is the learning rate for that node (the amount by
---   which the node's model should be updated to match the target).
---   The learning rate should be between zero and one.
-customSOM :: gm p -> (Int -> Int -> Metric p) -> SOM gm k p
-customSOM gm f =
-  SOM {
-        sGridMap=gm,
-        sLearningFunction=f,
-        sCounter=0
-      }
+-- -- | Creates a classifier with a default (bell-shaped) learning
+-- --   function. Usage is @'defaultSOM' gm r0 rf w0 wf tf@, where:
+-- --
+-- --   [@gm@] The geometry and initial models for this classifier.
+-- --   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
+-- --   @'HexHexGrid'@, and @ps@ is a set of random patterns.
+-- --
+-- --   [@r0@] See description in @'decayingGaussian2'@.
+-- --
+-- --   [@rf@] See description in @'decayingGaussian2'@.
+-- --
+-- --   [@w0@] See description in @'decayingGaussian2'@.
+-- --
+-- --   [@wf@] See description in @'decayingGaussian2'@.
+-- --
+-- --   [@tf@] See description in @'decayingGaussian2'@.
+-- defaultSOM
+--   :: Floating (Metric p) => gm p -> Metric p -> Metric p -> Metric p ->
+--      Metric p -> Int -> SOM f gm k p
+-- defaultSOM gm r0 rf w0 wf tf =
+--   SOM {
+--         sGridMap=gm,
+--         sLearningFunction=DecayingGaussian r0 rf w0 wf tf,
+--         sCounter=0
+--       }
 
--- | Configures one possible learning function for classifiers.
---   @'decayingGaussian' r0 w0 tMax@ returns a bell curve-shaped
---   function. At time zero, the maximum learning rate (applied to the
---   BMU) is @r0@, and the neighbourhood width is @w0@. Over time the
---   neighbourhood width shrinks and the learning rate tapers off.
-decayingGaussian
-  :: Floating a => a -> a -> Int -> (Int -> Int -> a)
-decayingGaussian r w0 tMax t d = r * s * exp (-(d'*d')/(2*w0*w0*s*s))
-    where s = exp (-t'/tMax')
-          t' = fromIntegral t
-          tMax' = fromIntegral tMax
-          d' = fromIntegral d
+-- -- | Creates a classifier with a custom learning function.
+-- --   Usage is @'customSOM' gm g@, where:
+-- --
+-- --   [@gm@] The geometry and initial models for this classifier.
+-- --   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
+-- --   @'HexHexGrid'@, and @ps@ is a set of random patterns.
+-- --
+-- --   [@f@] A function used to adjust the models in the classifier.
+-- --   This function will be invoked with two parameters.
+-- --   The first parameter will indicate how many patterns (or pattern
+-- --   batches) have previously been presented to this classifier.
+-- --   Typically this is used to make the learning rate decay over time.
+-- --   The second parameter to the function is the grid distance from
+-- --   the node being updated to the BMU (Best Matching Unit).
+-- --   The output is the learning rate for that node (the amount by
+-- --   which the node's model should be updated to match the target).
+-- --   The learning rate should be between zero and one.
+-- customSOM
+--   :: (LearningFunction f, LearningRate f ~ Metric p)
+--     => gm p -> f -> SOM f gm k p
+-- customSOM gm f =
+--   SOM {
+--         sGridMap=gm,
+--         sLearningFunction=f,
+--         sCounter=0
+--       }
 
--- | Configures a typical learning function for classifiers.
---   @'decayingGaussian' r0 rf w0 wf tf@ returns a bell curve-shaped
---   function. At time zero, the maximum learning rate (applied to the
---   BMU) is @r0@, and the neighbourhood width is @w0@. Over time the
---   bell curve shrinks and the learning rate tapers off, until at time
---   @tf@, the maximum learning rate (applied to the BMU) is @rf@,
---   and the neighbourhood width is @wf@. Normally the parameters
---   should be chosen such that:
---
---   * 0 < rf << r0 < 1
---
---   * 0 < wf << w0
---
---   * 0 < tf
---
---   where << means "is much smaller than" (not the Haskell @<<@
---   operator!) 
-decayingGaussian2 :: Floating a => a -> a -> a -> a -> Int -> (Int -> Int -> a)
-decayingGaussian2 r0 rf w0 wf tf t d = r * exp (-(d'*d')/(w*w))
-    where a = t'/tf'
-          r = r0 * ((rf/r0)**a)
-          w = w0 * ((wf/w0)**a)
-          t' = fromIntegral t
-          tf' = fromIntegral tf
-          d' = fromIntegral d
