Skip to content

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
With this last change, we can no-longer keep our implementation, as this would only work for a List. This is a great pattern though - we might want to keep it.

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