Associate a non-type-class function with a type in Haskell

Viewed 73

This is a follow-up question to Associate a function with a type in Haskell.

Again, suppose you have a serializer/deserializer type class

class SerDes a where
    ser :: a -> ByteString
    des :: ByteString -> a

and you want to provide some sanity check or test case that varies with type a,

check :: ByteString -> Bool

Of course des is used deep in the guts of check but the type a can't be deduced. To do something useful with a it probably needs to be member of some type class, let's say it's Show a.

As usual, Proxy can do the job:

data Proxy a = Proxy    -- or import Data.Proxy

check :: Proxy a -> ByteString -> Bool
check (Proxy :: Proxy MyType) input = ...      
check (Proxy :: Proxy MyOtherType) input = ...  

(With the extension TypeApplications this can be made more succinct: check (Proxy @MyType) ....)

But can Proxy be avoided? (Provided you can't move check into the SerDes type class.)

2 Answers

I must first mention that you have a problem in the design of your typeclass. The signature of your des function says that for each input ByteString there exists a valid output a. This is likely not true.

For example, imagine having an instance of SerDes for Int. By your definition, you'll have a valid Int representation even for 6GB ByteString of random data. Sounds wrong.

For this reason, you need to specify the possibility of deserialization failures in the signature of des. A typical approach would be to wrap a in Maybe or Either YourDetailedRepresentationOfFailure. E.g.,

class SerDes a where
  ser :: a -> ByteString
  des :: ByteString -> Either Text a

In fact this is the approach essentially taken by all Haskell serialization and parsing libraries. They may introduce some abstractions there, but in essence they all find ways to represent deserialization failures.

Now to your actual question. Typeclass instances are identified by the type they are for, so you must provide the specific type for a somehow. Proxy is one option for that. Another one is to directly refer to that type without using it in your function and passing in the undefined value for it. Third (and cleanest one, IMO) is to wrap the result in Tagged.

The undefined option

{-# LANGUAGE ScopedTypeVariables, TypeApplications #-}

check :: forall a. SerDes a => a -> ByteString -> Bool
check _ bytes =
  isRight (des @a bytes)

Please notice that forall and ScopedTypeVariables extension are required to be able to refer to the type parameter in the function def.

You'll then call this function like this:

check (undefined :: a) bytes

or this:

check @a undefined bytes

The Tagged option

check :: SerDes a => ByteString -> Tagged a Bool
check bytes =
  fmap isRight (Tagged (des bytes))

You'll then call this function like this:

unTagged (check @a bytes)

Final note

In reality you'll likely never need the check function, since des already carries all the information needed and more. It's easier and more understandable to just have isRight (des @a bytes) in place where you need check. The fact that you have to go thru complications for defining check is actually a signal of a design mistake. In practical Haskell code you'll rarely meet such complications.

Yes, you can specify the type a via TypeApplications in the exact same way as in the answer you have linked:

{-# LANGUAGE ScopedTypeVariables, AllowAmbiguousTypes, TypeApplications #-}

check :: forall a. SerDes a => ByteString -> Bool
check bytes = ... des @a bytes ...

Note that you need ScopedTypeVariables and forall a. in order to create a scope for the type variable a so that you can refer to it when calling des. Without an explicit forall, the type variable's scope would be limited to just the type signature and you couldn't mention it in the body.

To call the check function, use type application as before:

check @Int bytes
Related