coercible-subtypes
This library provides unidirectional (one-way) variant of Coercion.
The variant is a type Sub
defined in Data.Type.Coercion.Sub
.
Sub a b
can be used to convert a type a
to another type b
.
upcastWith :: Sub a b -> a -> b
For all Sub a b
values, the runtime representation of a
and
b
values are same, so upcastWith
do not require any computation
to return b
value, just coerces
GHC to treat a value of a
as type b
.
This feature is not different to Coercion
.
The difference is that while Coercion
represents
bidirectional relation, Sub
represents unidirectional relation.
Coercion a b
and its underlying type class Coercible a b
witnesse you can coerce both a
to b
and b
to a
.
Unlike that, Sub a b
only allows you to coerce a
to b
, not b
to a
.
Usage Example
To use this library effectively, it must be used at two places: a library
and its user code. For this example, let's assume they are written by two people,
a library author and a user.
The library author writes a module RightTriangle
below.
module RightTriangle(Triangle(), toEdges, getEdges, fromEdges) where
import Data.Coerce
import Data.Type.Coercion.Sub
newtype Triangle = MkTriangle (Int, Int, Int)
-- | Triangles can be coerced into 3-tuples of Ints
toEdges :: Sub Triangle (Int, Int, Int)
toEdges = sub
getEdges :: Triangle -> (Int, Int, Int)
getEdges = coerce
-- | Creates right triangle from lengths of edges (a,b,c)
--
-- > *
-- > |\ c
-- > a| \
-- > *--*
-- > b
--
-- (a^2 + b^2 == c^2) must hold.
fromEdges :: (Int, Int, Int) -> Maybe Triangle
fromEdges = {- Omit -}
The author wants to protect the invariant condition a^2 + b^2 == c^2
.
For that purpose, the author can't export the constructor of Triangle
.
Because it is symmetric, Coercion Triangle (Int,Int,Int)
can't be exported either.
The user is building an application using RightTriangle
module.
module Main where
import Data.Map (Map)
import RightTriangle
import Data.Type.Coercion.Sub
main :: IO ()
main = ......
In this application, the user has to convert Map String Triangle
to
Map String (Int, Int, Int)
, revealing the edge lengths of the triangles.
While it is easy to do so with fmap getEdges
,
using fmap
here can make an entire copy of the Map†.
This is wasted work and memory. Instead, the user can use mapR toEdges
to get
Sub (Map String Triangle) (Map String (Int, Int, Int))
and then upcastWith
to perform zero cost coercion over Map
.
Comparison against other methods
There are some other methods to achive the goal of this library.
-
Just give up coercion
- This is just for better performance, so not doing it
is always an option.
-
Rewrite rules
-
Rewrite rules based method is currently employed, and working at our hand.
So, it is possible you don't need this library at all.
-
The downside is whether it works or not is on the provider of the
"container" type in use, and GHC doing expected optimizations.
Without reading source codes and examining the GHC optimization result (e.g. -ddump-rule-firings
),
you can't be sure you are doing the conversion zero-cost.
For Data.Map
, which containers
package provides, can optimize fmap
away via proper inlining and rewrite rules. The purpose of this library
is turning optimizations into explicit codes, or handling the cases when the container type in use does not
provide such an opportunity via rewrite rules.