Say we have a function with signature
foo :: a -> b -> Int
Is it possible to enforce a constraint ensuring that a and b are different? That is
foo :: Int -> String -> Int -- ok
foo :: Int -> Int -> Int -- not ok
The purpose of this question is to learn more about Haskell and perhaps solve a design issue I'm facing. My particular case does not make sense if a == b, so I'd like to disallow that at the compiler level. I probably could make this issue go away with a different design altogether but that's besides the point now - the Pandoras box has been opened and I'd like to learn if an equality constraint on the type level is possible.
Haskell also incorporates polymorphic types---types that are universally quantified in some way over all types. Polymorphic type expressions essentially describe families of types. For example, (forall a)[a] is the family of types consisting of, for every type a, the type of lists of a.
Haskell has three basic ways to declare a new type: The data declaration, which defines new data types. The type declaration for type synonyms, that is, alternative names for existing types. The newtype declaration, which defines new data types equivalent to existing ones.
The == is an operator for comparing if two things are equal. It is quite normal haskell function with type "Eq a => a -> a -> Bool". The type tells that it works on every type of a value that implements Eq typeclass, so it is kind of overloaded.
The Nil constructor is an empty list. It contains no objects. So any time you're using the [] expression, you're actually using Nil . Then the second constructor concatenates a single element with another list.
Like pretty much any other language, Haskell has integers, floating point numbers, characters, and strings. We can define a function that uses all of these basic types: myFunction :: Int -> Float -> Char -> String -> String myFunction x y c str = (c : str) ++ “ “ ++ show x ++ show y myResult :: String myResult = myFunction 4 5.5 ‘a’ “bcd”
First, lists in Haskell are homogenous. This means that a Haskell list can only hold elements of the same type Second, lists in Haskell are (internally) implemented as linked lists. This is different from many other languages, where the word "list" and "array" is used interchangably.
Haskell is a functional language and it is strictly typed, which means the data type used in the entire application will be known to the compiler at compile time.
Appending / Joining / Growing Haskell lists There are fourways to join / concatentate / append / grow Haskell lists: (++):: list1 -> list2 -> joined-list When you have a few known lists that you want to join, you can use the ++operator: [True, False] ++[False, False] [1, 2, 3] ++[4, 5, 6] ++[10, 20, 30, 50, 90]
Like this?
{-# LANGUAGE DataKinds #-}
module TyEq where
import Data.Type.Equality (type (==))
import GHC.TypeLits
import Data.Kind (Constraint)
class ((a == b) ~ eq) => Foo a b eq where
foo :: a -> b -> Int
instance (TypeError ('Text "NAUGHTY!"), (a == b) ~ True) => Foo a b 'True where
foo _ _ = error "The types have failed us!"
instance (a == b) ~ False => Foo a b 'False where
foo _ _ = 42
--_ = foo True True -- NAUGHTY!
_ = foo () True
_ = foo True (42 :: Int)
Notice how the eq
index is needed in the Foo type class, or we're getting 'duplicate instance' errors despite differing instance contexts (only heads are taken into account).
We can streamline that a bit using purpose-specific type family:
type family Check a b :: Constraint where
Check a a = TypeError ('Text "VERY NAUGHTY!")
Check a b = ()
foo' :: Check a b => a -> b -> Int
foo' _ _ = 42
--_ = foo' True True -- VERY NAUGHTY!
_ = foo' () True
_ = foo' True (42 :: Int)
I'm pretty sure I've learnt that all from Sandy Maguire's 'Thinking with Types', which I sincerely recommend to everyone interested :)
The type a :~: b -> Void
is inhabited (by a total function) exactly when a
and b
are different, so one lightweight way would be to take one as an argument. You can then ask the compiler to check whether callers supply a total argument with -Wincomplete-patterns
. A complete example might look like this:
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeOperators #-}
import Data.Void
import Data.Type.Equality
foo :: (a :~: b -> Void) -> a -> b -> Int
foo _ _ _ = 0
fooUseGood :: Int
fooUseGood = foo (\case) True () -- accepted, no warning
fooUseBad :: Int
fooUseBad = foo (\case) () () -- warning: Patterns not matched: Refl
Here's a riff on Artur's version. It uses machinery developed for the trivial-constraints
package.
{-# LANGUAGE DataKinds, KindSignatures, MultiParamTypeClasses, FlexibleInstances, FlexibleContexts, AllowAmbiguousTypes, TypeApplications, ScopedTypeVariables, ExplicitNamespaces, TypeOperators, UndecidableSuperClasses, UndecidableInstances #-}
import Data.Type.Equality (type (==))
import GHC.TypeLits
import GHC.Exts (Any)
-- The Any constraint prevents malicious/ignorant
-- instantiation with no = undefined. The only
-- way to produce an instance of this class is in a local context
-- using something deeply unsafe like unsafeCoerce, magicDict, or unsafePerformIO.
class Any => Bottom where
no :: a
-- Why not just put the `no` method in this class?
-- Because then `no` will need to be applied to the type argument `e`, which is
-- very annoying and unnecessary.
class (TypeError e, Bottom) => Oops e
class Foo a b (eq :: Bool) where
foo' :: a -> b -> Int
foo :: forall a b. Foo a b (a == b) => a -> b -> Int
foo = foo' @a @b @(a == b)
instance Oops ('Text "NAUGHTY!") => Foo a b 'True where
foo' = no
instance Foo a b 'False where
foo' _ _ = 42
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With