Isomorphisms in Dex

Iso a b is the type of an isomorphism between a and b.

:t MkIso
((a:Type) ?-> (b:Type) ?-> {bwd: b -> a & fwd: a -> b} -> Iso a b)

This is a normal ADT, and you can construct your own isomorphisms.

def cycle_three {a b c} : Iso (a & b & c) (b & c & a) = MkIso { fwd = \(a, b, c). (b, c, a) , bwd = \(b, c, a). (a, b, c) }

Isomorphisms can be applied with app_iso, applied in reverse with rev_iso, and flipped with flip_iso

:p app_iso cycle_three (1, 2.0, 3)
(2., (3, 1))
:p rev_iso cycle_three (1, 2.0, 3)
(3, (1, 2.))
:p app_iso (flip_iso cycle_three) (1, 2.0, 3)
(3, (1, 2.))

They can also be composed with &>>:

:p app_iso (cycle_three &>> cycle_three) (1, 2.0, 3)
(3, (1, 2.))
:p app_iso (cycle_three &>> cycle_three &>> cycle_three) (1, 2.0, 3)
(1, (2., 3))

Note that we assume but do not check that the isomorphism is lawful (i.e. app_iso iso $ rev_iso iso x == x for all x, or equivalently iso &>> (flip_iso iso) == id_iso).

In addition, Dex will automatically write some useful isomorphisms for you to extract fields from records and variants. There are four syntactic forms that produce isos. We will start with the first two:

  • #x produces a "lens-like" record accessor Iso {x:a & ...r} (a & {&...r})
  • #?x produces a "prism-like" variant matcher Iso {x:a | ...r} (a | {|...r})
%passes parse :t #b :: Iso {a:Int & b:Float & c:Unit} _
(Iso {a: Int32 & b: Float32 & c: Unit} (Float32 & {a: Int32 & c: Unit}))
(MkIso {fwd=(\{b=x, ...r}. (,) x r), bwd=(\(x, r). {b=x, ...r})} : Iso {a: Int & b: Float & c: Unit} _)
%passes parse :t #?b :: Iso {a:Int | b:Float | c:Unit} _
(Iso {a: Int32 | b: Float32 | c: Unit} (Float32 | {a: Int32 | c: Unit}))
(MkIso {fwd=(\v. case v {| b = x |} -> (Left x) {|b| ...r |} -> (Right r)) , bwd=(\v. case v ((Left x)) -> {| b = x |} ((Right r)) -> {|b| ...r |})} : Iso {a: Int | b: Float | c: Unit} _)

There are also two "zipper" forms, described later on this page:

  • #&x produces a "record-zipper" isomorphism
    Iso ({&...l} & {x:a & ...r}) ({x:a & ...l} & {&...r})
    
  • #|x produces a "variant-zipper" isomorphism
    Iso ({|...l} | {x:a | ...r}) ({x:a | ...l} | {|...r})
    

Record accessors and lens-like helpers

Record accessor isomorphisms can be passed into the helper function get_at:

:t get_at
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (Iso a (b & c)) -> a -> b)
:p get_at #foo {foo=1, bar=2.0}
1

We can also do other types of things:

:p pop_at #foo {foo=1, bar=2.0}
{bar = 2.}
:p push_at #foo 3.0 {foo=1, bar=2.0}
{bar = 2., foo = 3., foo = 1}
:p set_at #foo 2 {foo=1, bar=2.0}
{bar = 2., foo = 2}

These helper functions work with any "lens-like" isomorphism. For instance, we can select everything except for a particular field:

:t except_lens
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (Iso a (b & c)) -> Iso a (c & b))
:p get_at (except_lens #foo) {foo=1, bar=2.0, baz=3}
{bar = 2., baz = 3}

Variant accessors and prism-like helpers

Similarly, there are prism-like helpers

:t match_with
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (Iso a (b | c)) -> a -> Maybe b)
:t build_with
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (Iso a (b | c)) -> b -> a)

which can be used with variant accessors or any other prism-like isomorphism:

:p match_with #?foo $ {|foo = 1|}::{foo:Int | bar:Float}
(Just 1)
:p match_with #?foo $ {|bar = 1.0|}::{foo:Int | bar:Float}
Nothing
:p build_with #?foo 3 :: {foo:Int | bar:Float}
{| foo = 3 |}
:p match_with (except_prism #?foo) $ {|bar = 1.0|}::{foo:Int | bar:Float}
(Just {| bar = 1. |})

Record zipper isomorphisms

The isomorphisms shown above are specialized for removing a single field from an object. As such, they don't compose well when trying to work with more than one field at a time. When using multiple fields, a better choice is to use a "zipper isomorphism", which moves a subset of fields from one place to another. For instance:

%passes parse :t #&a :: Iso ({&} & {a:Int & b:Float & c:Unit}) _
(Iso ({&} & {a: Int32 & b: Float32 & c: Unit}) ({a: Int32} & {b: Float32 & c: Unit}))
(MkIso {fwd=(\({...l}, {a=x, ...r}). (,) {a=x, ...l} {...r}) , bwd=(\({a=x, ...l}, {...r}). (,) {...l} {a=x, ...r})} : Iso ((&) {} {a: Int & b: Float & c: Unit}) _)
:t (#&a &>> #&b) :: Iso ({&} & {a:Int & b:Float & c:Unit}) _
(Iso ({&} & {a: Int32 & b: Float32 & c: Unit}) ({a: Int32 & b: Float32} & {c: Unit}))

#&a and #&b are isomorphisms that move a given field from the record on the right to the record on the left; when composed, they move both fields.

The main use for record zipper isomorphisms is to specify multiple named axes when using a record type as an index set:

:t over_fields
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (v:Type) ?-> (v#0:(Ix b)) ?=> (v#1:(Ix c)) ?=> (Iso ({&} & a) (b & c)) -> (_autoq:(Ix a)) ?=> (a => v) -> b => c => v)
-- :p
-- x = for {a, b, c}:{a:Fin 2 & b:Fin 2 & c:Fin 2}.
-- ordinal a * 100 + ordinal b * 10 + ordinal c
-- v1 = x
-- v2 = sum $ over_fields (#&b) x
-- v3 = sum $ over_fields (#&b &>> #&c) x
-- v4 = sum $ over_fields (#&a &>> #&b &>> #&c) x
-- (v1, v2, v3, v4)
-- > ( [0, 100, 10, 110, 1, 101, 11, 111]@{a: Fin 2 & b: Fin 2 & c: Fin 2}
-- > , ([10, 210, 12, 212]@{a: Fin 2 & c: Fin 2}, ([22, 422]@{a: Fin 2}, [444]@{ &})) )

Note that over_fields is just a simple wrapper combining split_r and over_lens:

:t split_r
((a:Type) ?-> Iso a ({&} & a))
:t over_lens
((a:Type) ?-> (b:Type) ?-> (c:Type) ?-> (v:Type) ?-> (v#0:(Ix b)) ?=> (v#1:(Ix c)) ?=> (Iso a (b & c)) -> (_autoq:(Ix a)) ?=> (a => v) -> b => c => v)

over_lens alone can be used with any lens-like isomorphism, for instance an ordinary record accessor lens.

def abc_to_tuple {a b c} : Iso {a:a & b:b & c:c} (c&b&a) = fwd = \({a, b, c}). (c, b, a) bwd = \(c, b, a). {a, b, c} MkIso {fwd, bwd}
instance Ix {a:n & b:m & c:q} given {n m q} [Ix n, Ix m, Ix q] size = size n * size m * size q ordinal = ordinal <<< app_iso abc_to_tuple unsafe_from_ordinal = unsafe_from_ordinal _ >>> rev_iso abc_to_tuple
def bc_to_pair {b c} : Iso {b:b & c:c} (c&b) = fwd = \({b, c}). (c, b) bwd = \(c, b). {b, c} MkIso {fwd, bwd}
instance Ix {b:n & c:m} given {n m} [Ix n, Ix m] size = size n * size m ordinal = ordinal <<< app_iso bc_to_pair unsafe_from_ordinal = unsafe_from_ordinal _ >>> rev_iso bc_to_pair
:p x = for {a, b, c}:{a:Fin 2 & b:Fin 2 & c:Fin 2}. ordinal a * 100 + ordinal b * 10 + ordinal c over_lens #a x
[[0, 10, 1, 11], [100, 110, 101, 111]]

split_r can be used if you want to process multiple fields at once:

:p push_at (split_r &>> #&a &>> #&b) {a=1, b=2.0} {c=3, d=4.0}
{a = 1, b = 2., c = 3, d = 4.}

Variant zipper isomorphisms

Just as there are record zipper isomorphisms, there are also variant zipper isomorphisms:

%passes parse :t #|a :: Iso ({| } | {a:Int | b:Float | c:Unit}) _
(Iso ({ |} | {a: Int32 | b: Float32 | c: Unit}) ({a: Int32} | {b: Float32 | c: Unit}))
(MkIso {fwd=(\v. case v ((Left l)) -> (Left {|a| ...l |}) ((Right w)) -> (case w {| a = x |} -> (Left {| a = x |}) {|a| ...r |} -> (Right r))) , bwd=(\v. case v ((Left w)) -> (case w {| a = x |} -> (Right {| a = x |}) {|a| ...r |} -> (Left r)) ((Right l)) -> (Right {|a| ...l |}))} : Iso ((|) { |} {a: Int | b: Float | c: Unit}) _)

split_v makes a prism zipper into an ordinary prism isomorphism:

:t split_v
((a:Type) ?-> Iso a ({ |} | a))
:p vals : (Fin 3)=>{a:_ | b:_ | c:_ } = [{|a = 1|}, {|b = 2|}, {|c = 3|}] for i. match_with (split_v &>> #|a &>> #|b) vals.i
[(Just {| a = 1 |}), (Just {| b = 2 |}), Nothing]

slice_fields uses this to specific named variants from a variant-indexed table:

def abc_to_either {a b c} : Iso {a:a | b:b | c:c} (a|(b|c)) = fwd = \v. case v of {|a=x|} -> Left x {|b=x|} -> Right (Left x) {|c=x|} -> Right (Right x) bwd = \v. case v of Left x -> {|a=x|} Right x -> case x of Left y -> {|b=y|} Right y -> {|c=y|} MkIso {fwd, bwd}
instance Ix {a:n | b:m | c:q} given {n m q} [Ix n, Ix m, Ix q] size = size n + size m + size q ordinal = ordinal <<< app_iso abc_to_either unsafe_from_ordinal = unsafe_from_ordinal _ >>> rev_iso abc_to_either
def ab_to_either {a b} : Iso {a:a | b:b} (a|b) = fwd = \v. case v of {|a=x|} -> Left x {|b=x|} -> Right x bwd = \v. case v of Left x -> {|a=x|} Right x -> {|b=x|} MkIso {fwd, bwd}
instance Ix {a:n | b:m} given {n m} [Ix n, Ix m] size = size n + size m ordinal = ordinal <<< app_iso ab_to_either unsafe_from_ordinal = unsafe_from_ordinal _ >>> rev_iso ab_to_either
def ac_to_either {a c} : Iso {a:a | c:c} (a|c) = fwd = \v. case v of {|a=x|} -> Left x {|c=x|} -> Right x bwd = \v. case v of Left x -> {|a=x|} Right x -> {|c=x|} MkIso {fwd, bwd}
instance Ix {a:n | c:m} given {n m} [Ix n, Ix m] size = size n + size m ordinal = ordinal <<< app_iso ac_to_either unsafe_from_ordinal = unsafe_from_ordinal _ >>> rev_iso ac_to_either
:p x = iota {a:Fin 2 | b:Fin 2 | c:Fin 2} v1 = x v2 = slice_fields (#|a &>> #|b) x v3 = slice_fields (#|a &>> #|c) x v4 = slice_fields (#|a &>> #|b &>> #|c) x (v1, v2, v3, v4)
([0, 1, 2, 3, 4, 5], ([0, 1, 2, 3], ([0, 1, 4, 5], [0, 1, 2, 3, 4, 5])))