Check whether a type is an instance of Show in Haskell?

前端 未结 3 1743
执笔经年
执笔经年 2020-11-30 10:48

Suppose I have a simple data type in Haskell for storing a value:

data V a = V a

I want to make V an instance of Show, regardless of a\'s t

3条回答
  •  慢半拍i
    慢半拍i (楼主)
    2020-11-30 11:20

    You can with this library: https://github.com/mikeizbicki/ifcxt. Being able to call show on a value that may or may not have a Show instance is one of the first examples it gives. This is how you could adapt that for V a:

    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE RankNTypes #-}
    {-# LANGUAGE TemplateHaskell #-}
    {-# LANGUAGE KindSignatures #-}
    {-# LANGUAGE ScopedTypeVariables #-}
    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE UndecidableInstances #-}
    
    import IfCxt
    import Data.Typeable
    
    mkIfCxtInstances ''Show
    
    data V a = V a
    
    instance forall a. IfCxt (Show a) => Show (V a) where
        show (V a) = ifCxt (Proxy::Proxy (Show a))
            (show a)
            "<>"
    

    This is the essence of this library:

    class IfCxt cxt where
        ifCxt :: proxy cxt -> (cxt => a) -> a -> a
    
    instance {-# OVERLAPPABLE #-} IfCxt cxt where ifCxt _ t f = f
    

    I don't fully understand it, but this is how I think it works:

    It doesn't violate the "open world" assumption any more than

    instance {-# OVERLAPPABLE #-} Show a where
        show _ = "<>"
    

    does. The approach is actually pretty similar to that: adding a default case to fall back on for all types that do not have an instance in scope. However, it adds some indirection to not make a mess of the existing instances (and to allow different functions to specify different defaults). IfCxt works as a a "meta-class", a class on constraints, that indicates whether those instances exist, with a default case that indicates "false.":

    instance {-# OVERLAPPABLE #-} IfCxt cxt where ifCxt _ t f = f
    

    It uses TemplateHaskell to generate a long list of instances for that class:

    instance {-# OVERLAPS #-} IfCxt (Show Int) where ifCxt _ t f = t
    instance {-# OVERLAPS #-} IfCxt (Show Char) where ifCxt _ t f = t
    

    which also implies that any instances that were not in scope when mkIfCxtInstances was called will be considered non-existing.

    The proxy cxt argument is used to pass a Constraint to the function, the (cxt => a) argument (I had no idea RankNTypes allowed that) is an argument that can use the constraint cxt, but as long as that argument is unused, the constraint doesn't need to be solved. This is similar to:

    f :: (Show (a -> a) => a) -> a -> a
    f _ x = x
    

    The proxy argument supplies the constraint, then the IfCxt constraint is solved to either the t or f argument, if it's t then there is some IfCxt instance where this constraint is supplied which means it can be solved directly, if it's f then the constraint is never demanded so it gets dropped.


    This solution is imperfect (as new modules can define new Show instances which won't work unless it also calls mkIfCxtInstances), but being able to do that would violate the open world assumption.

提交回复
热议问题