24 days of Hackage, 2015: day 16: safe; what is safety anyway?
Dec 16, 2015 · 7 minute read · CommentsHaskellHackagesafesafetyexceptionsPureScriptElmpsychology
Table of contents for the whole series
A table of contents is at the top of the article for day 1.
Day 16
Today I’m doing something strange: up till now, I’ve discussed
libraries and tools I actually use. Today I’m discussing a library I
do not use, while thinking about why I not, and why you or I might
want to use it. This library is
safe
, which aims to
protect against the unfortunate “unsafety” of common functions in the
standard Prelude.
That’s a good thing, right? After all, on
day 7,
I promoted the use of the NonEmpty
list, and again used in
day 14. I
like safety, but what does “safety” mean anyway?
Safety is relative
A notion of safety is always relative to some criterion, some expectation, and even more generally, in the context of some way of life. And ways of life can be safeguarded through different mechanisms, from strict protection to “style guides” to implicit social contracts and ostracism.
In the context of the safe
package, the kind of safety we are
concerned about is not ever wanting to see a running program crash
with an exception from pure code like
*** Exception: Prelude.head: empty list
This happens if you call head
on an empty list:
-- | Crashes if nobody in line.
unsafeReportFirstInLine :: [Int] -> String
unsafeReportFirstInLine nums =
"next up: customer " ++ show (head nums)
> unsafeReportFirstInLine []
"next up: customer *** Exception: Prelude.head: empty list
The “safe” solution is don’t ever call head
on a list that might
be empty. There are different ways to achieve this.
Solving a human psychology problem?
One possible solution is to change the type of head
.
Providing functions such as head
in the Haskell Prelude is arguably
a historical legacy mistake from 1990, because it encourages
programmers (especially those starting out with Haskell, especially
when using instructional materials that use head
!) to call
head
. If you give people the easy ability to do something unsafe,
they will surely do it, and often.
Newer language communities attack the human psychology problem by not
providing an unsafe head
: for example, the standard PureScript
ecosystem provides a
safe Data.List.head
with type forall a. List a -> Maybe a
but also provides an entire
Data.List.Unsafe
module that includes
Data.List.Unsafe.head
with type forall a. List a -> a
.
And Elm
List
module
provides
List.head
of type List a -> Maybe a
.
Marking something as unsafe at least enables the writer and reader of code to make note that something might go wrong, so I think this is a good starting point for a solution. Furthermore, psychology research has shown clearly that defaults matter: if the goal is to promote safety, it is better to have the default be safe, and “opt out” explicitly to be unsafe, rather than have the default be unsafe, and “opt in” to be safe.
Unfortunately, for historical reasons, if you’re working with lists and some other data structures in Haskell, you are forced to opt in to safety, rather than opt out to unsafety.
The safe
package allows you to opt in to safety.
You get functions such as
Safe.headMay
with type [a] -> Maybe a
.
-- | Using 'Safe.headMay' and pattern matching on Maybe.
reportFirstInLine :: [Int] -> String
reportFirstInLine nums =
case Safe.headMay nums of
Just num -> "next up: customer " ++ show num
Nothing -> "there are no customers in line"
Pattern-matching directly on the data structure as an alternative
In practice, I don’t use functions like headMay
because I usually
just pattern-match on the list itself:
-- | Using pattern matching on list.
reportFirstInLine2 :: [Int] -> String
reportFirstInLine2 [] = "there are no customers in line"
reportFirstInLine2 (num:_) = "next up: customer " ++ show num
When I think about it, though, there is something not quite right
about this solution. The wildcard _
in the pattern gives away the
fact that we are getting back more information than we actually
need. In principle we should ask only for what we need, and
headMay
does precisely that. I’m basically violating conceptual
encapsulation by getting back more than I need (the tail) and ignoring
it.
So I believe I should really start using
headMay
in this kind of context, and thinking more deeply, I believe
that the single reason I haven’t is that the standard Prelude didn’t
provide it! It was easier to do the pattern-matching than to find
safe
and add it as a dependency.
How many of you think like me, and would happily use headMay
if it
were part of the standard Prelude, but because it is not, you use a
wildcarded pattern match on a list instead?
Revisiting the headMay
solution
Some of you might like using the maybe
function that has type b ->
(a -> b) -> Maybe a -> b
, to avoid pattern matching on Maybe
:
-- | No pattern matching.
reportFirstInLine3 :: [Int] -> String
reportFirstInLine3 =
maybe "there are no customers in line" reportOne . Safe.headMay
reportOne :: Int -> String
reportOne num = "next up: customer " ++ show num
There’s also a code golf version that I do not recommend:
-- | Code golf.
reportFirstInLine4 :: [Int] -> String
reportFirstInLine4 =
maybe "there are no customers in line"
(("next up: customer " ++) . show)
. Safe.headMay
Other safe
goodies
Each of the ...May
functions also has useful variants:
One allows specifying a default to return upon empty:
headDef :: a -> [a] -> a
Another is unsafe, but at least generates a better exception. This is useful if you know that a list is not empty, and do not want to handle the case in which it is empty, but just in case, generate an exception that if triggered, at least tells you where your “internal fatal error” came from.
headNote :: String -> [a] -> a
Exactness is a safety issue
The
Safe.Exact
module provides a lot of useful functions that have to do with
indexing into a list or the checking the sizes of lists. Here,
“safety” no longer refers to an exception. It refers to something more
insidious: code you write that typechecks and runs but does the
unintended thing. For example, it is very easy to use take
in a way
that you don’t intend because it silently allows you to “take” more
elements from a list than it contains, but just assumes you know what
you are doing and don’t “really” mean “take 1000 elements” but rather
“take 1000 elements or if there aren’t 1000, take all the elements”. I
have been bitten by take
before, where I passed in an absurd number
that I did not intend. So the Safe.Exact.takeExact...
family of
functions is quite useful. In the past, before I discovered safe
, I
ended up basically writing my own wrappers, and now I won’t do that
again.
Foldable
Finally the
Safe.Foldable
module is useful because Foldable
is full of unsafe operations.
When you know something about your data that the type doesn’t know
A final note on exactness of list operations as a safety issue: the principled solution to inexactness when it comes to indexing into or size of lists is to turn the potential bugs into type errors, using dependent types: a “list” type that is dependent on its size. A later Day of Hackage will mention solutions.
Conclusion
The safe
package is a nice utility library that wraps “unsafe”
Prelude operations. There are technical and psychological reasons I
haven’t used it, and I discussed them, but I will use it in the future
when it fits my needs.
All the code
All my code for my article series are at this GitHub repo.