Functions as instances of typeclasses?
{-# LANGUAGE LambdaCase #-}
I have a bunch of functions which encode failure in various ways. For example:
f :: A -> Bool
returns False
on failure g :: B -> Maybe B'
returns Nothing
on failure h :: C -> Either Error C'
returns Left ...
on failure I want to chain these operations in the same way as the Maybe
monad, so the chaining function needs to know whether each function failed before proceeding to the next one. For this I wrote this class:
class Fail a where
isFail :: a -> Bool
instance Fail () where
isFail () = False
instance Fail Bool where -- a
isFail = not
instance Fail (Maybe a) where -- b
isFail = not . isJust
instance Fail (Either a b) where -- c
isFail (Left _) = True
isFail _ = False
However, it's possible that functions that don't conform exist:
f' :: A -> Bool
returns True
on failure g' :: B -> Maybe Error
returns Just Error
on failure ( Nothing
on success) h' :: C -> Either C' Error
returns Right ...
on failure These could be remedied by simply wrapping them with functions that transform them, for example:
f'' = not . f'
f'' = not . f'
. g'' = (case Nothing -> Right (); Just e -> Left e) . g'
h'' = (case Left c -> Right c; Right e -> Left e) . h'
However, the user of the chaining function expects to be able to combine f
, g
, h
, f'
, g'
, and h'
and have them just work. He would not know that the return type of a function needs to be transformed unless he looks at the semantics of each function he's combining, and check if they match up with whatever Fail
instances he has in scope. This is tedious and too subtle for the average user to even notice, especially with type inference bypassing the user having to choose the right instances.
These functions weren't created with knowledge of how they'd be used. So I could make a type data Result ab = Fail a | Success b
data Result ab = Fail a | Success b
and make wrappers around each function. For example:
fR = (case True -> Sucess (); False -> Fail ()) . f
f'R = (case False -> Sucess (); True -> Fail ()) . f'
gR = (case Just a -> Sucess a; Nothing -> Fail ()) . g
g'R = (case Nothing -> Sucess (); Just e -> Fail e) . g'
hR = (case Left e -> Fail e; Right a -> Sucess a) . h
h'R = (case Right e -> Fail e; Left a -> Sucess a) . h'
However, this feels dirty. What we're doing is just certifying / explaining how each of f
, g
, h
, f'
, g'
, and h'
are used in the context of the combining function. Is there are more direct way of doing this? What I want exactly is a way to say which instance of the Fail
typeclass should be used for each function, ie, (using the names given to the typeclass instances above), f
→ a
, g
→ b
, h
→ c
, and f'
→ a'
, g'
→ b'
, h'
→ c'
for the "invalid" functions, where a'
, b'
, and c'
are defined as the following instances (which overlap the previous ones, so you'd need to be able to pick them by name somehow):
instance Fail Bool where -- a'
isFail = id
instance Fail (Maybe a) where -- b'
isFail = isJust
instance Fail (Either a b) where -- c'
isFail (Right _) = True
isFail _ = False
It doesn't necessarily have to by done via typeclasses though. Maybe there's some way to do this other than with typeclasses?
Don't do this. Haskell's static type system and referential transparency give you a tremendously useful guarantee: you can be properly sure that some particular value means the same thing1, regardless of how it was produced. There's neither mutability to interfer with this, nor dynamic-style “runtime reinterpretation” of expressions, as you'd need for the task you seem to envision.
If those functions you have there don't adhere to such a specification accordingly, well, then this is bad. Better get rid of them (at least, hide them and only export a re-defined version with unified behaviour). Or tell the users they'll have to live with looking up the specification of each. But don't try to hack some way around this particular symptom of broken definitions.
An easy change you could apply to just “flag” the functions where failure means the opposite as it otherwise does is to have them return such a wrapped result:
newtype Anti a = Anti { profail :: a }
instance (Anti a) => Fail (Anti a) where
isFail (Anti a) = not $ isFail a
1Mind: “same thing” in a possibly very abstract sense. There's no need for Left
to be universally a “fail constructor”, it's sufficient that it's clear that it's the variant constructor associated to the first type argument, which is not what the functor/monad instance operates on – from that it follows automatically that this will “mean” failure in a monadic application.
Ie, when you've chosen the right types, stuff should be unambigious pretty much automatically; obviously the opposite is true when you're just tossing around booleans, so perhaps you should get rid of those entirely...
下一篇: 函数作为类型类的实例?