Functions as type instances?

{-# LANGUAGE LambdaCase #-}

I have a bunch of functions that encode failure in various ways. For instance:

  • f :: A -> Bool returns False on failure
  • g :: B -> Maybe B' returns Nothing on error
  • h :: C -> Either Error C' returns Left ... on error

I want to relate these operations in the same way as the Maybe monad, so the chain function needs to know if each function worked before moving on to the next. 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 ab) where -- c isFail (Left _) = True isFail _ = False 

However, it is possible that functions that do not match 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 error

They could be fixed simply by 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 chain function expects to combine f , g , h , f' , g' and h' and make them work. He did not know that the return type of the function should be transformed if he does not look at the semantics of each function that he combines, and check if they correspond to the dark Fail instances that he has in scope. This is tedious and too subtle for the average user to even notice, especially with the type of output that bypasses the user who must select the correct instances.

These functions were not created with knowledge of how they will be used. Therefore, I could create a data Result ab = Fail a | Success b type of data Result ab = Fail a | Success b data Result ab = Fail a | Success b and wrap around each function. For instance:

  • 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 seems dirty. We simply confirm / explain how each of f , g , h , f' , g' and h' used in the context of the union function. Is there a more direct way to do this? I want to specify exactly which instance of the Fail class should be used for each function, i.e. (Using the names given for typeclass instances above), f β†’ a , g β†’ b , h β†’ c and f' β†’ a' , g' β†’ b' , h' β†’ c' for "invalid" functions, where a' , b' and c' defined as the following instances (which overlap the previous ones, so you will need to choose them somehow):

 instance Fail Bool where -- a' isFail = id instance Fail (Maybe a) where -- b' isFail = isJust instance Fail (Either ab) where -- c' isFail (Right _) = True isFail _ = False 

Of course, this does not have to be done through cool classes. Maybe there is a way to do this differently than using class tables?

+5
source share
1 answer

Do not do that. The Haskell static type system and referential transparency give you an extremely useful guarantee: you can be sure that a certain value means the same 1 no matter how it was created. There is no variability to interfere with this, nor a dynamic interpretation of the terms "runtime reinterpretation" of expressions, as you need for the task that you seem to represent.

If the features you have do not meet this specification accordingly, well, then this is bad. It’s better to get rid of them (at least hide them and export only an overridden version with unified behavior). Or tell users that they will have to live by looking at the specifications of each. But don't try to crack some way around this particular symptom of broken definitions.

An easy change that you can only apply to flag functions, where failure means the opposite, otherwise it means that they return such a wrapped result:

 newtype Anti a = Anti { profail :: a } instance (Anti a) => Fail (Anti a) where isFail (Anti a) = not $ isFail a 

1 Mind: "the same" in a possibly very abstract sense. There is no need for Left be a universal "crash constructor", it is enough to clear its variant constructor associated with an argument of the first type, which is not that the functor / monad on instance works - this automatically means that it will mean a monadic failure application.
Ie, when you choose the right types, things should be unambiguous almost automatically; obviously the opposite is true when you just throw around booleans , so maybe you should completely get rid of them ...

+10
source

Source: https://habr.com/ru/post/1214032/


All Articles