This library provides a way to branch on whether a constraint is satisfied:
{-# OPTIONS_GHC -fplugin=IfSat.Plugin #-}
module MyModule where
import Data.Constraint.If ( IfSat, ifSat )
hypot :: forall a. ( Floating a, IfSat (FMA a) ) => a -> a -> a
hypot = ifSat @(FMA a) withFMA withoutFMA
where
withFMA :: FMA a => a -> a -> a
withFMA x y =
let
h = sqrt $ fma x x (y * y)
h² = h * h
x² = x * x
u = fma (-y) y (h² - x²) + fma h h (-h²) - fma x x (-x²)
in
h - u / ( 2 * h )
withoutFMA :: a -> a -> a
withoutFMA x y = sqrt ( x * x + y * y )
hypot x y
computes the value of sqrt( x² + y² )
in a different way
depending on whether a fused multiply-add operation fma
is available
for the type a
. This makes use of the ifSat
function:
ifSat :: IfSat c => ( c => r ) -> r -> r
which chooses the first branch when c
is satisfied, and uses the second branch
as a fallback otherwise.
What is important to understand is that the branch selection happens
precisely when we need to solve the IfSat ct
constraint.
{-# OPTIONS_GHC -fplugin=IfSat.Plugin #-}
module M1 where
showFun :: forall (a :: Type). IfSat ( Show ( a -> a ) ) => ( a -> a ) -> String
showFun = ifSat @( Show (a -> a) ) show ( \ _ -> "<<function>>" )
test1 :: ( Bool -> Bool ) -> String
test1 fun = showFun fun
----------------------------------------
{-# OPTIONS_GHC -fplugin=IfSat.Plugin #-}
module M2 where
import M1
instance Show ( Bool -> Bool ) where
show f = show [ f False, f True ]
test2 :: ( a -> a ) -> String
test2 fun = showFun fun
test3 :: ( Bool -> Bool ) -> String
test3 fun = showFun fun
After loading M2
, we get the following results:
test1 not
"<<function>>"
In this example, to typecheck test1
we need to solve IfSat (Show (Bool -> Bool))
inside module M1
.
As no instance for Show (Bool -> Bool)
is available in M1
, we pick the second branch,
resulting in "<<function>>"
.
test2 not
"<<function>>"
In this example, we must solve IfSat (Show (a -> a))
within M2
.
There is no such instance in M2
, so we pick the second branch.
It doesn't matter that we are calling test2
with a function of type
Bool -> Bool
: we had to solve IfSat (Show (a -> a))
when type-checking
the type signature of test2
.
test3 not
"[True, False]"
In this last example, we must solve IfSat (Show (Bool -> Bool))
, but as we're in M2
,
such an instance is available, so we choose the first branch.
Note in particular that test1
and test3
have the exact same definition
(same type signature, same body), but produce a different result.
This is because the satisfiability check happens in different contexts.
If you need to use additional constraints in the fallback branch, you can instead use the
constraint disjunction operator (||)
, by way of the dispatch
function:
dispatch :: ( c || d ) => ( c => r ) -> ( d => r ) -> r
The dispatch
function will select the c => r
branch when c
is satisfied,
and otherwise fall back to the d => r
branch.
As with ifSat
, the selection occurs precisely when the c || d
constraint is solved.
This functionality is strictly more general, as IfSat ct
is the special case in which
the second constraint is trivially satisfied (using the trivial constraint () :: Constraint
):
type IfSat ct = ( ct || () )
ifSat :: forall ct r. IfSat ct => ( c => r ) -> r -> r
ifSat f g = dispatch @ct @() f g
If you prefer working at the type-level, this library has got you covered, with the IsSat
type family.
To reduce IsSat ct
, GHC will first attempt to solve ct
. If it succeeds, then IsSat ct
reduces to True
;
otherwise, it reduces to False
. This means that the satisfiability check is performed precisely at the time of type-family reduction.
This is clearly quite dangerous: the type family application IsSat ct
might evaluate to False
in a certain context,
but then in another module when more instances become available it will switch to evaluating to True
.
This allows us to unsafely coerce between any two types:
module M1 where
type F :: Bool -> Type
type family F b where
F False = Float
F True = Text
class C
foo :: Float -> F (IsSat C)
foo x = x
----------------------------------------
module M2 where
import M1
instance C
unsafeCoerce :: Float -> Text
unsafeCoerce x = foo x
To avoid such issues, it's recommended to restrict usage of IsSat
to internal constraints,
e.g. a typeclass that is defined internally in a library and isn't exported.
Yes. Mike Izbicki's ifCxt
library inspired this library,
with constraint disjunction (||)
taken from Noah Luck Easterly's constraint-unions
.
What's the difference? The above libraries require users to manually declare instances
for the typeclasses they want to work with, e.g. by using Template Haskell.
On the other hand, this library only requires users to enable the plugin,
which directly hooks into GHC to solve c || d
and IfSat c
constraints,
without requiring large amounts of instances to be defined by hand.
This also means that users have more flexibility: as we saw above, branch selection occurs
when the c || d
or IfSat c
constraints are discharged, looking at all the information
that is available at that point. This includes instance declarations,
Given constraints, local evidence (e.g. from GADT pattern matches), etc.
Furthermore, this library isn't limited to working with typeclasses and their instances:
any constraint can be passed to IfSat
, e.g. an equality constraint involving a type family,
which might only be satisfied in the presence of further type-family equations.