[Haskell-cafe] Lisp Style Restarts in Haskell

Mikael Brockman mbrock at goula.sh
Thu Feb 13 11:10:28 UTC 2014


"Henk-Jan van Tuyl" <hjgtuyl at chello.nl> writes:

> Or Control.Exception in the base package?

With Lisp-style restarts, exceptions (Common Lisp calls them
"conditions") don't necessarily "unwind the stack."  Instead, they
provide a set of alternatives for how to proceed.  Calls to the throwing
function can be wrapped in a handler that chooses, say, whether to skip
or abort.

To take a Haskell example, Data.Text.Encoding has an API for doing
Unicode decoding with "controllable error handling."  It's pretty
simple, and not very flexible.

> type OnDecodeError = String -> Maybe Word8 -> Maybe Char
>
> decodeUtf8With :: OnDecodeError -> ByteString -> Text

Considering some different possibilities for this API...  Something like
this (a kind of defunctionalized version) might be more familiar to a CL
programmer:

> data DecodeCondition = InvalidWord Word8 | UnexpectedEOF
> data DecodeRestart   = Ignore | UseChar Char
>
> decodeUtf8With :: (DecodeCondition -> DecodeRestart)
>                -> ByteString -> Text

We can use ImplicitParams to approximate the dynamic scope behavior, and
LambdaCase to write what CL calls the "restart-case":

> decodeUtf8 :: (?restart :: DecodeCondition -> DecodeRestart)
>            -> ByteString -> Text

Usage:

> myDecode s =
>   let ?restart = \case InvalidWord _ -> UseChar '*'
>                        UnexpectedEOF -> Ignore
>   in decodeUtf8 s

                                 * * *

One of the cool things about CL's condition system that this
implementation doesn't capture is the way the runtime environment can
provide interactive prompts for restarting uncaught conditions.  An
example session:

> CL-USER 6 > (restartable-gethash 'mango *fruits-and-vegetables*)
>
>  Error: RESTARTABLE-GETHASH error getting MANGO [...]
>    1 (continue) Return not having found the value.
>    2 Try getting the key from the hash again.
>    3 Use a new key.
>    4 Use a new hash.
>    5 (abort) Return to level 0.
>    6 Return to top loop level 0.
>
>  Type :b for backtrace, :c <option number> to proceed,
>    or :? for other options

To increase the flexibility of our purely functional restarts, we can
use monads:

> decodeUtf8With :: Monad m => (DecodeCondition -> m DecodeRestart)
>                -> ByteString -> m Text
>
> myDecode :: ByteString -> IO Text
> myDecode = decodeUtf8With (\c -> askUserAbout c >>= decide)

We can also use other monads:

> cautiousDecode :: ByteString -> Maybe Text
> cautiousDecode = decodeUtf8With (const Nothing)

This of course opens up a whole world of bizarre control flow
possibilities.

--
Mikael Brockman
@mbrock



More information about the Haskell-Cafe mailing list