Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

haskell

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 type. If a is an instance of Show, then show (V a) should return show a otherwise an error message should be returned. Or in Pseudo-Haskell:

instance Show (V a) where
    show (V a) = if a instanceof Show
                   then show a
                   else "Some Error."

How could this behaviour be implemented in Haskell?

like image 251
Vector Avatar asked Mar 03 '16 23:03

Vector


3 Answers

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)
        "<<unshowable>>"

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 _ = "<<unshowable>>"

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.

like image 175
xnyhps Avatar answered Nov 07 '22 06:11

xnyhps


As I said in a comment, the runtime objects allocated in memory don't have type tags in a Haskell program. There is therefore no universal instanceof operation like in, say, Java.

It's also important to consider the implications of the following. In Haskell, to a first approximation (i.e., ignoring some fancy stuff that beginners shouldn't tackle too soon), all runtime function calls are monomorphic. I.e., the compiler knows, directly or indirectly, the monomorphic (non-generic) type of every function call in an executable program. Even though your V type's show function has a generic type:

-- Specialized to `V a`
show :: V a -> String  -- generic; has variable `a`

...you can't actually write a program that calls the function at runtime without, directly or indirectly, telling the compiler exactly what type a will be in every single call. So for example:

-- Here you tell it directly that `a := Int`
example1 = show (V (1 :: Int)) 

-- Here you're not saying which type `a` is, but this just "puts off" 
-- the decision—for `example2` to be called, *something* in the call
-- graph will have to pick a monomorphic type for `a`.
example2 :: a -> String
example2 x = show (V x) ++ example1

Seen in this light, hopefully you can spot the problem with what you're asking:

instance Show (V a) where
    show (V a) = if a instanceof Show
                   then show a
                   else "Some Error."

Basically, since the type for the a parameter will be known at compilation time for any actual call to your show function, there's no point to testing for this type at runtime—you can test for it at compilation time! Once you grasp this, you're led to Will Sewell's suggestion:

-- No call to `show (V x)` will compile unless `x` is of a `Show` type.
instance Show a => Show (V a) where ...

EDIT: A more constructive answer perhaps might be this: your V type needs to be a tagged union of multiple cases. This does require using the GADTs extension:

{-# LANGUAGE GADTs #-}

-- This definition requires `GADTs`.  It has two constructors:
data V a where
  -- The `Showable` constructor can only be used with `Show` types.
  Showable   :: Show a => a -> V a
  -- The `Unshowable` constructor can be used with any type.
  Unshowable :: a -> V a

instance Show (V a) where
  show (Showable a) = show a
  show (Unshowable a) = "Some Error."

But this isn't a runtime check of whether a type is a Show instance—your code is responsible for knowing at compilation time where the Showable constructor is to be used.

like image 14
Luis Casillas Avatar answered Nov 07 '22 07:11

Luis Casillas


Even if you could do this, it would be a bad design. I would recommend adding a Show constraint to a:

instance Show a => Show (V a) where ...

If you want to store members in a container data type that are not an instance of Show, then you should create a new data type fore them.

like image 2
Will Sewell Avatar answered Nov 07 '22 07:11

Will Sewell