Indeed, you are experiencing the XY problem. You are not taking the advantage of dependent types, which you can do as shown below.
Regarding your problem X: one just has to perform a transport across equality, but in a universe:
def abEq (A B : Type) (a : A) (ab : A = B): B :=
@eq.rec Type A (λ (X : Type), X) a B ab
Namely, suppose we have:
- a type
U
- element
u : U
- a type family
P : U → Type
- an element
x : P u
- an element
v : U
- equality
e : u = v
Then eq.rec U u P x v e
will produce an element of P v
. We say that we transported x : P u
to P v
along e
. If we set U := Type
, u := A
, P := λ X, X
, x := a
, v := B
and e := ab
then we get your example.
Now let's get onto problem Y.
I am including a fairly elaborate example of how to go about formalizing universal algebra because the classical treatment of universal algebra is not the right way to formalize the topic, but I see people falling into the trap repeatedly. In particular, one should not use natural numbers as arities – that creates a host of unecessary complications.
-- A signature has a type of operation symbols, and each symbol
-- has an arity. Improtantly, the arity is a type, not a number!
-- That is, if we want a symbol to have arity 3 we do not use
-- the number 3 but instead (any) type with 3 elements.
structure signature :=
(symbol : Type) -- the type of operation symbols
(arity : symbol → Type) -- the arities of symbols
namespace arity
-- we define arities for constants, unary and binary symbols,
-- as these are most common.
inductive const : Type
inductive unary : Type
| u1 : unary -- the index of the only argument
open unary
inductive binary : Type
| b1 : binary -- the index of the first argument
| b2 : binary -- the index of the second argument
open binary
end arity
-- the type of algebras for a given signature,
-- note that each symbol op is interpreted as a map which
-- takes a function (args : S.arity op → carrier) and returns
-- an element of the carrier. If the type S.arity op has
-- n elements, then args is (equivalent to) an n-tuple,
-- as it should be.
structure algebra (S : signature) :=
(carrier : Type) -- the underlying carrier
(act : Π (op : S.symbol), (S.arity op → carrier) → carrier)
-- Example: the free algebra generated by a signature S and a set X of generators
-- the carrier of the free algebra is the type of trees whose leaves are
-- the generators and the nodes are operation symbols
inductive tree (Leaf : Type) (Node : Type) (arity : Node → Type) : Type
| leaf : Leaf → tree -- the injection of generators into the tree algebra
| node : Π (t : Node), (arity t → tree) → tree
-- the free algebra on S generated by X
def free (S : signature) (X : Type) : algebra S :=
{ carrier := tree X S.symbol S.arity,
act := tree.node }
-- the type of homomorphisms between two S-algebras
structure hom {S : signature} (X : algebra S) (Y : algebra S) :=
(map : X.carrier → Y.carrier)
(is_hom : Π (op : S.symbol) (args : S.arity op → X.carrier),
map (X.act op args) = Y.act op (map ∘ args) )
-- composition of homomorphisms is a homomorphism
def hom_compose {S : signature} {X Y Z : algebra S}
(g : hom Y Z) (f : hom X Y) : hom X Z :=
{ map := g.map ∘ f.map,
is_hom := begin intros op args, simp [g.is_hom, f.is_hom] end
}
-- example: the signature of a group
-- the type of symbols of a group
inductive group_symbol : Type
| uni : group_symbol -- unit
| inv : group_symbol -- inverse
| mul : group_symbol -- multiplication
open group_symbol
def group : signature :=
{ symbol := group_symbol,
arity := λ op, match op with
| uni := arity.const
| inv := arity.unary
| mul := arity.binary
end }
-- the group Z/3Z
-- the carrier of Z/3Z
inductive Z3_carrier : Type
| z0 : Z3_carrier
| z1 : Z3_carrier
| z2 : Z3_carrier
open Z3_carrier
-- the inverse operation in Z/3Z
def Z3_inv : Z3_carrier → Z3_carrier
| z0 := z0
| z1 := z2
| z2 := z1
-- addition in Z/3Z
def Z3_add : Z3_carrier → Z3_carrier → Z3_carrier
| z0 z0 := z0
| z0 z1 := z1
| z0 z2 := z2
| z1 z0 := z1
| z1 z1 := z2
| z1 z3 := z0
| z2 z0 := z2
| z2 z1 := z0
| z2 z2 := z1
-- the group Z/3Z
def Z3 : algebra group :=
{ carrier := Z3_carrier,
act := λ op,
match op with
| uni := λ args, z0
| inv := λ args, Z3_inv (args arity.unary.u1)
| mul := λ args, Z3_add (args arity.binary.b1) (args arity.binary.b2)
end
}