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
this section needs breaking down into smaller chunks, or to be simplified.
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
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
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