@hackage coercible-subtypes0.3.0.1

Coercible but only in one direction

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.