coercible-subtypes: Coercible but only in one direction

[ bsd3, data, library ] [ Propose Tags ]

Newtype wrapper Data.Type.Coercion.Sub.Sub around Data.Type.Coercion.Coercion to represent unidirectional coercion, and combinators for it, like Data.Type.Coercion.Sub.mapR which extends coercion over covariant Functor.

[Skip to Readme]


Note: This package has metadata revisions in the cabal description newer than included in the tarball. To unpack the package including the revisions, use 'cabal get'.

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


  • No Candidates
Versions [RSS],,,,
Change log
Dependencies base (>=4.12 && <4.21), profunctors [details]
License BSD-3-Clause
Copyright (c) 2020-2021 Koji Miyazato
Author Koji Miyazato
Revised Revision 1 made by viercc at 2024-05-16T13:23:11Z
Category Data
Home page
Bug tracker
Source repo head: git clone -b master
Uploaded by viercc at 2023-08-03T09:44:54Z
Distributions NixOS:
Downloads 648 total (20 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2023-08-03 [all 1 reports]

Readme for coercible-subtypes-

[back to package description]


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.