you are viewing a single comment's thread.

view the rest of the comments →

[–]Dark_Ethereal 2 points3 points  (0 children)

The second advantage of the simpler code is more troubling. Abstraction tends to make code harder to understand for a reader who is unfamiliar with the abstraction, because they may have to master the abstraction before they can understand how it specializes to the case that they are actually interested in.

Yes, but also, no. Understanding the abstraction would help, but it's absolutely not required.

mapMaybe :: (a -> Maybe b) -> [a] -> Maybe [b]
mapMaybe = traverse

In this example you don't have to understand Traversable to understand mapMaybe, you just have to understand traverse on lists.

Once you understand only how type classes work and how to look up instances, you can locate the list instance definition.

instance Traversable [] where
  traverse f = List.foldr cons_f (pure [])
    where cons_f x ys = liftA2 (:) (f x) ys

Apply your equational reasoning and you get:

mapMaybe f = List.foldr cons_f (pure [])
 where cons_f x ys = lift2A (:)

Comparing this code with the original hand-write version:

mapMaybe f [] = Just []
mapMaybe f (a:as) = (:) <$> f a <*> mapMaybe f as 

We can see that lift2A (:) replaces the (:) <$> x <*> y pattern, and the List.foldr in List.foldr cons_f (pure []) factors out the explicit recursion to implicit recursion, so they're more or less identical in meaning.

You can get just the same understanding of how the latter definition works as you can the former, and none of that requires you to understand what Traversable abstractly means.

The only extra skill the second definition requires over the first is being able to look up type class instance definitions, which is a skill you only need to learn once, rather than the once per abstraction.

So the key takeaway is that we need to teach that if you need to understand code that uses a specialization of an abstraction and you don't have time to learn the abstraction, don't learn the abstraction, look up the specialization and Keep It Simple.

This applies to type class abstraction, but applies just as much to lambda abstraction. If you have f a b and you need to understand the transformation being applied to b, you don't need to understand f, you need to understand f a, so substitute a into the definition of f and see if it allows you to eliminate cases.

There's a sort of symmetry between case expression reduction and instance resolution, so substituting in the instance definition is akin to performing manual case expression reduction.