You can merge the two classes together into one that expresses decidability of whether or not the type is inhabited:
{-# LANGUAGE TypeFamilies, DataKinds
, KindSignatures, TypeApplications, UndecidableInstances
, ScopedTypeVariables, UnicodeSyntax #-}
import Data.Kind (Type)
import Data.Type.Bool
import Data.Void
data Inhabitedness :: Bool -> Type -> Type where
IsEmpty :: (a -> Void) -> Inhabitedness 'False a
IsInhabited :: a -> Inhabitedness 'True a
class KnownInhabitedness a where
type IsInhabited a :: Bool
inhabitedness :: Inhabitedness (IsInhabited a) a
instance ∀ a b . (KnownInhabitedness a, KnownInhabitedness b)
=> KnownInhabitedness (a -> b) where
type IsInhabited (a -> b) = Not (IsInhabited a) || IsInhabited b
inhabitedness = case (inhabitedness @a, inhabitedness @b) of
(IsEmpty no_a, _) -> IsInhabited $ absurd . no_a
(_, IsInhabited b) -> IsInhabited $ const b
(IsInhabited a, IsEmpty no_b) -> IsEmpty $ \f -> no_b $ f a
To get again your simpler interface, use
{-# LANGUAGE ConstraintKinds #-}
type Empty a = (KnownInhabitedness a, IsInhabited a ~ 'False)
type NonEmpty a = (KnownInhabitedness a, IsInhabited a ~ 'True)
exampleless :: ∀ a . Empty a => a -> Void
exampleless = case inhabitedness @a of
IsEmpty no_a -> no_a
example :: ∀ a . NonEmpty a => a
example = case inhabitedness @a of
IsInhabited a -> a
\_ -> ()
andabsurd
, the two implementations forNonEmpty (Void -> ())
, are distinguishable functions in Haskell. Which behavior should it have? Any solution will have to choose one or the other, which seems unfortunate. – Disassemble\_ -> () :: Void -> ()
andabsurd
distinguishable? They certainly agree on all possible input values (since there are 0 of them), which is as far as I understand it the "moral" definition of equality of functions. Presumably you mean there's some Haskell function/expression that will behave differently depending on which of these 2 functions you use - and if so I'd be intrigued to see one because I can't see myself how to construct one. – Intransigentundefined
will throw an exception in one case and give you()
in the other, which you can distinguish inIO
. – DisassembleIO
would allow us to distinguish two copies of the exact same code compiled with different optimisation settings. IOW, no laws / transformation rules could ever be formulated. – Viipuri