Looking for Assumptions in code¶
When writing functional code, a good general heuristic for making code more reusable is to remove assumptions. Examples adapted from ps unscripted on code reuse
Note
this section needs breaking down into smaller chunks, or to be simplified.
Example¶
sayHello :: List String -> List String
sayHello Nil = Nil
sayHello (name : names) =
("Hello, " <> name) : sayHello names
Contains several assumptions. The first and most obvious is that we wish to say hello. We can move towards
prefixAll :: String -> List String -> List String
prefixAll prefix Nil = Nil
prefixAll prefix (first : rest) =
(prefix <> first) : prefixAll prefix rest
sayHello = prefixAll "Hello, "
This code now contains the assumption that the caller wants to prefix every item in a list. Instead
transform :: (String -> String) -> List String -> List String
transform f Nil = Nil
transform f (first : rest) =
(f first) : transform f rest
prefixAll prefix = transform (prefix <> _)
suffixAll suffix = transform (_ <> suffix)
We're assuming that the transformation has to happen on a String over a list of strings - this can easily be changed in just the type signature. No code changes need to happen to fit this idea in.
transform :: forall a. (a -> a) -> List a -> List a
But now we're assuming the starting type has to be the same as the resulting type - one more type-level change
transform :: forall a b. (a -> b) -> List a -> List b
This is looking pretty good now - but we're assuming the caller wants to use a List
...
transform :: forall f a b. (a -> b) -> f a -> f b
type Transform f a b = (a -> b) -> f a -> f b
transformList :: forall a b. Transform List a b
-- or even, because most callers will want to supply `f` but not `a b`
type Transform f = forall a b. (a -> b) -> f a -> f b
transformList :: Transform List
This lets us build all-sorts. We can still be polymorphic with the f
formatAll :: forall f. Transform f -> f Number -> f String
formatAll t = t number.toString
This lets us provide transforming functions to make even less assumptions
sayHello :: forall f. Transform f -> f String -> f String
sayHello transform names = transform ("Hello, " <> _)
Most languages would stop here - but purescript has Typeclasses. Instead of pulling our pattern out as a Type and having to pass fulfilment functions around, we can declare our pattern as a Typeclass
class Transform f where
transform :: forall a b. (a -> b) -> f a -> f b
instance transformList :: Transform List where
transform f Nil = Nil
transform f (x : xs) =
(f x) : transform f xs
formatAll :: forall f. Transform f => f Number -> f String
formatAll = transform number.toString
Transform here is a renamed Functor