Sneak Peek: Bolt Math

Adjoints, anyone?
Last time I was building something using units, I needed the arithmetic equivalent to symmetric set difference. Take the monoid operation • to be addition in a lattice of non-negative quantities (e.g. energy). This makes this Semiringresiduated.

Try to take amount y from amount x where both are non-negative. Conceptually, x-y is the unique number such that y+(x-y) = x, or, in the language of residuals, subtraction is right adjoint to addition in the lattice of numbers. If all quantities are non-negative (e.g. energy), the proper difference y-x must remain a pair and uniqueness is lost. We can, however, choose a canonical representative:

  • If y > x, then the result is the pair (0, y-x) signifying that there was more to take away than what was there.
  • If x > y then the result is the pair (x-y, 0) signifying what is left over after taking away some.

I have the impression that primary school kids use a related concept when doing arithmetic involving numbers larger than 10. The fundamental operation being to split a quantity (number) using a smaller one.

2 Likes

No worries!

I don’t think we are actually in any sort of disagreement, and so I feel the need to clear some air first - I 100% absolutely love the questions, and give your reaction I think may have come across as a fair bit sharper than I intended (I was going for short, I am almost always too long-winded) so apologies for that, and thank you for not returning the favor!

I actually want to thank you for being clear on this because I could not tell what you were asking (this is not meant as disrespect, this is on me if you’ll allow me to explain shortly). I restated my wedge argument because I was not sure.

Actually, precisely! Minor correction, wedge product generalizes multiplication, so *, but yes, because scalar multiplication actually is the special case of wedge products for scalars (because they are grade 0 vectors). Its not because I really want to, its because it really is.

Subtraction has the same thing going on, with affine vs vector spaces and diff :: p -> p -> v: Not an act, not homogenous - but definitely a specialization of subtraction, for which the symbol is appropriate. The notational flexibility (thank you for that phrase) allows me to express this, and it really helps minimize the number of symbolic operators, which for me is extremely important


I love it here because people are kind and welcome, so I feel safe talking about this, because the topic is math communication which makes it oh so relevant:

If you read any of my older writing, you may have notice that my writing style is, well, let’s just say extremely odd :slight_smile: I have been working hard on improving, though.

To sum up a lot rather quickly, I am actually partially dyslexic, and I struggle to read math notation; or more precisely, Tachyphemia is a neurological disorder known to most people as ‘dyslexia’, except it also can include other things:

  • Cluttering (word order, speaking and parsing speech)
  • Dysgraphia (writing and fine motor skills)
  • Dyspraxia (coordination and gross motor skills)
  • Dyslexia (reading and writing)
  • Dyscalculia (math and number sense)

It doesn’t affect my intelligence - obviously, I have fantastic linguistics reading and math skills or I wouldn’t be here, but I can be very slow to write or to speak if I have not planned to, and in a great twist of irony, despite being strong at reading prose and doing math in my head, I am more or less symbol-blind* and can’t really read math notation or musical notation or the greek alphabet (not without some sort of legend or reference) which is probably why I got into programming over mathematics, where function names are usually words, and a program speaks for itself.

Since it does affect my ability to parse symbols, I do try to minimize their number and complexity, and to unify them and avoid making redundant operators wherever possible. This greatly colors my library design, but I do not think that it is a problem, since it not that I oppose the creation of convenience operators or anything, I just want to make sure my library works without them.


* I do better with shapes (form) than symbols (intent), and while there are pros and cons to that, I do get really annoyed by overdesigned UI because it quickly starts turning into visual noise for me and I can’t play a lot of games because of it - for instance, I love playing board games like Tokaido with friends, but I am constantly asking what things are because although it is beautiful, the heavily stylized artwork means I can’t tell what anything actually means.

6 Likes

I would not call a pairing an action in disguise. Actions happen between two objects –one acting on the another. Pairings work with three objects and they usually model some sort of duality. CS related examples show up in cryptography.

2 Likes

Math Update: Semigroup, and the Cooler Semigroup

So, I have been paying close attention to feedback, and I thought I would address one of the concerns that is on many people’s minds - lawfulness. I have a ton more coming down the pipeline (topological spaces, manifolds, discrete spaces, projective and conformal geometry - fun stuff) but they need a solid foundation, and I’m still nailing down some design points, so today…

*SNAP*

I dont know why I am using avengers infinity war memes, I haven’t even seen the movies.

Another thing that bothers me about Haskell, supposedly one of the best languages, is that we cannot define laws for typeclasses, and as a result, comments and documentation are littered with -- INVARIANT: blah blah blah, meaning you have to scan the documentation to even know about this invisible constraint that the compiler can’t warn you against. I hate that - don’t you?

Things like LiquidHaskell exist, but they tend to be plugins that define a new layer eg centered over parsing comments, and while that’s pretty darn cool, I just really like sticking with Haskell. Can’t we do better?

Algebra: It’s the law!

This isn’t my first attempt at codifying algebraic laws into Haskell, but it is so far, my most successful. I have made previous attempts some years back, only those were simple, and limited to demonstrating magmas associativity and semigroups - I never really got any further with it, not because it didn’t work (it did), but because it didn’t really do or add anything, since it was too limited to even describe equality.

That has changed.

Operations

We will start with a generalization of the heterogenous operators:

class Operation (r :: Type -> Type -> Constraint) a b where
    type Result r a b :: Type
    op :: a -> b -> Result r a b

The r constraint allows us to specify the typeclass that defines the operation, eg:

instance Operation Addition Int Int where
    type Result Addition Int Int = Int
    op = plus

We use this via op @Addition @Int @Int 1 2, and by doing so, we have promoted our typeclass to a first-class type. A little verbose, but we’ll deal with that by sealing it up later as a magma.

Actions

Operations are really generic, actions are more restricted - one of the arguments is the same type as the result, so either a -> b -> b, or a -> b -> a:

class (Operation r a b, Result r a b ~ b) => LeftAction r a b where
    lact :: a -> b -> b
    lact = op @r

class (Operation r a b, Result r a b ~ a) => RightAction r a b where
    actr :: a -> b -> a
    actr = op @r

These functions cover operations that perform some action over an object without changing its type. Using it is as simple as lact @Multiplication @Float @(Vector Float).

Relations / Pairings

As mentioned by in a few comments, operations of the form a -> a -> b are a ‘pairing’ but I think the most accurate term here is ‘relation’ as in, an open homogenous relationship.

type Codomain r a = Result r a a
class (Operation r a a) => Relation r a where
    rel :: a -> a -> Codomain r a
    rel = op @r

We also defined Codomain - neat!

Laws

Now comes the part where things start getting… legal. See, the standard way to go about enforcing constraints on classes is to add functions that add new capabilities or requirements. This has caused me some consternation, because it doesn’t scale well (typeclass explosions) and it can be hard to enforce the non-existence of a thing, and you end up with a bunch of empty typeclasses like algebraic-graphs:

class Graph g => Undirected g
    -- Empty class
class Graph g => Reflexive g
    -- Empty class
class Graph g => Transitive g
    -- Empty class
class (Reflexive g, Transitive g) => Preorder g
    -- Empty class

It’s great that you can express that a constraint follows a law - its not so great that these classes are all useless outside of the algebraic-graphs typeclass hierarchy. They’re also completely empty, and amount to nothing more than swearsy-realsy promises - and ultimately, I don’t think it’s quite the right approach - and I hope you’ll follow along, and come to see the same.

Instead of defining a typeclass that says “this class has legal behavior”, we just define a typeclass that says it can follow laws:

class (Operation r a b) => Lawful r a b where
    lawful :: a -> b -> Bool

Specifically, this class doesn’t assert that it is lawful in general, but rather, that we can check whether a particular operation is lawful. This is meaningful, because there can be valid inputs that break the law, which is not exactly the same thing as an invalid input that throws an error and breaks the computer - though that certainly is one possible outcome of failing to adhere to a law.

As a result, this class sort of pre-generalizes several things:

  • Throwing catchable exceptions
  • Throwing fatal errors
  • Returning exceptional values (Maybe, Either Error)
  • Interrupt and recovery

We can use it via lawful @Division @Int @Int 1 0 which yields False, allowing us to perform lawfulness checks before attempting the operation.

Adding to this is a typeclass for specific laws:

class (Lawful r a b) => Law p r a b where
    is :: a -> b -> Bool
    isnt :: a -> b -> Bool

We can use this via is @Open @Division or even is @Lawful @Division - that second one is actually ratherm important, because Lawful itself is a law that checks whether an operation follows all laws:

instance (Lawful r a b) => Law Lawful r a b where
    is = lawful @r
    isnt a b = not (lawful @r a b)

So, how do we actually specify that an operation does in fact always follow all laws? Why, we use the mathematical definition:

class (Lawful r a b) => WellDefined r a b where
    type Definition r a b :: Type
    definition :: Definition r a b
    -- lawful always returns true for well-defined relations
    defined :: a -> b -> Bool
    defined _ _ = True

I decided to tack on the ability to retrieve a definition, too. Something tells me that will be handy in the future.

In a neat bit of symmetry, we can also define indeterminate forms, and distinguish between functions that have undefined results, and functions that have more than one result:

class (Lawful r a b) => Indeterminate r a b where
    type IndeterminateForm r a b :: Type -- 0, 1, or many possible results
    indeterminate :: a -> b -> Bool
    indeterminateForm :: a -> b -> IndeterminateForm r a b

data Undefined -- Eg, Void

class (Indeterminate r a b, IndeterminateForm r a b ~ Undefined) => Indefinite r a b where
    indefinite :: a -> b -> Bool
    indefinite = indeterminate @r

Note that we don’t give instances to specific types, we give instances to constraints - so that means we can’t really define laws without defining properties to apply them, AND defining the data type they operate over. That’s a bit tedious, and its mostly just a bit of repetative boilerplate like when I defined instance Addition Int Int, so you’ll have to use your imagination.

Groupoids

Okay, so let’s define some properties / laws:

class (Relation r a) => Open r a where
    open :: a -> a -> Bool

class (Open r a, Codomain r a ~ a) => Closed r a where
    closed :: a -> a -> Bool

class
    ( Operation r a b -- Result r a b ~ d
    , Operation r b c -- Result r b c ~ e
    , Operation r a (Result r b c) -- Operation r a e
    , Operation r (Result r a b) c -- Operation r d c
    , Result r (Result r a b) c ~ Result r a (Result r b c) -- Result r d c ~ Result r a e
    ) => Associative r a b c where
    associative :: a -> b -> c -> Bool
    default associative :: (Eq (Result r a (Result r b c))) => a -> b -> c -> Bool
    associative a b c = op @r (op @r a b) c == op @r a (op @r b c)

class
    ( Operation r a b
    , Operation r b a
    , Result r a b ~ Result r b a
    ) => Commutative r a b where
    commutative :: a -> b -> Bool
    default commutative :: (Eq (Result r a b)) => a -> b -> Bool
    commutative a b = op @r a b == op @r b a

class (Relation r a) => Unital r a where
    identity :: a

class (Relation r a) => Invertible r a where
    inverse :: a -> a

Oof! Defining heterogenous associativity and commutativity was a pain in the arse - but well worth it, because now these get used to define our magma typeclasses:

class (LeftAction r a a, RightAction r a a, Relation r a, Closed r a) => Magma r a where
    act :: a -> a -> a
    act = rel @r

class (Magma r a, Associative r a a a) => Semigroup r a where
    append :: a -> a -> a
    append = act @r

class (Semigroup r a, Unital r a) => Monoid r a where
    empty :: a
    empty = identity @r

class (Monoid r a, Invertible r a) => Group r a where

Big group hug, everyone! No longer do we have to hem and haw about which instance of Monoid is the most important and which get relegated to newtype wrappers - now multiple instances (eg, of Semigroup, Monoid, etc) can coexist simultaneously, and can be easily selected between!

Now, we could make also law instances, given an actual operation data type - instance Law Closed Addition Int Int, instance Law Associative Addition Int Int etc so long as we also have instance Closed Addition Int Int and instance Associative Int Int.

But I’ve got something else for closing:

Functions

Lets generalize Operation to support arbitrary arity.

First, we need a function typeclass:

type family Call (args :: [Type]) out :: Type where
    Call '[] out = out
    Call (a ': args) out = a -> Call args out

class Function r (arity :: Nat) (args :: [Type]) | r -> arity args where
    type Return r arity args :: Type
    call :: Call args (Return r arity args)

Fundeps AND type families, oh my - the type families avoid parameter explosions while fundeps enforce injectivity without requiring intervening constructors like data families would have.

All this does though is curry a type-level list of arguments into a proper lambda, though - but now we can describe imperative functions more correctly.

Defining unary and binary operations takes a little bit of thought, but is ultimately just forwarding arguments.

class (Function r 1 '[a], Return r 1 '[a] ~ Output r a) => UnaryOperation (r :: Type -> Constraint) a where
    type Output r a :: Type
    unop :: a -> Output r a
    unop = call @r @1 @'[a]

class (Function r 2 '[a, b], Return r 2 '[a, b] ~ Result r a b) => Operation (r :: Type -> Type -> Constraint) a b where
    type Result r a b :: Type
    op :: a -> b -> Result r a b

What’s neat is that since this is all occurring at the type level, so all of this gets elided by the compiler, hopefully giving it zero runtime cost - but how can we know this?

Lets define a function:

data Honk
instance Function Honk 1 '[Int] where
    type Return Honk 1 '[Int] = IO ()
    -- NOTE: The awkward syntax / lack of inference can be elided with an
    -- appropriate use of let- or where- clauses
    call :: Call '[Int] (Return Honk 1 '[Int])
    call = go where
        go 0 = return ()
        go n = print "Honk!" >> go (n - 1)

Note that Honk has no values, so it disappears entirely during compilation and call @Honk just collapses directly to the go function.

Finally, to test it out:

> honk = call @Honk
> honk 3
Honk!
Honk!
Honk!

Silly goose, functions types are for people!

Conclusion

I know that the plethora of type annotations are going to some worries, so I will head those off with a reminder - you are looking at the exposed seedy underbelly of the machine, we will cover it with elegance and ergonomics when we are satisfied that it is correct.

We are giving up a lot of type inference, but what we get in return is an effortless scaling of typeclasses, so this is best understood as targeting the machinery that powers the guts and runtime. Imagine how this integrates with the memalloc library that is being developed, or with writing interpreters with first-class access to Haskell functions, because after Function comes Method and Object, by opening a way to allow for function definitions to become first-class ‘Typed functions’ (Data functions?), like how data types can be first class via Generic.


Challenges for the studious reader

  • Define the equivalent of Reader using Function
  • Define the equivalent of Lambda a b using Function.
  • How do these two differ? How are they related?
  • Did you spot the Church encoding?
2 Likes

You mean Nothing, right?

Not quite - lawful performs a check without performing the operation, and returns a boolean result as to whether or not it would fail. You can then use that to choose to return Nothing if Result r a b ~ Maybe c, or throw an exception, etc.

This distinction is rather precise, I admit.

1 Like

Got it, I misunderstood.
By the way, is arity a necessary type parameter for Function? I don’t see where it gets used, and wouldn’t a Length type family on args work if it was ever needed?

type Function :: k -> [Type] -> Contraint
class Function r args | r -> args where
    type Return r args :: Type
    call :: Call args (Return r args)

Won’t this also compile and be simpler, with the same amount of type inference? I don’t see arity doing any work in the code.

1 Like

Oh I love it when someone asks good questions! I know you are paying close attention :smiling_face_with_three_hearts:

The answer is yes, you are correct! It would be simpler, we could even calculate it from the type-level args list, except…

I didn’t want to rule out support for variadic functions! I haven’t written code to demonstrate handling them, but it should be fairly trivial - you repeat the last argument type indefinitely until the arity runs out, and viola, variadic functions :grin:

Edit: I could probably do a little work to differentiate them so I can seal up the arity as a non-argument for non-variadic functions, thank you for that idea!

1 Like

One other thing to note: Since Function has a fundep, I think that the associated type family return could be written as

class Function r {- arity ? -} args | r -> {- arity ? -} args where 
    type Return r :: Type

I think this is a bit simpler and makes less typing (keystrokes, not types) needed
About variadic functions:

This is my effect on making a variadic function like what you describe.

type Repeat :: Nat -> k -> [k]
type family Repeat n a where 
    Repeat 0 _ = []
    Repeat n a = a : Repeat (n-1) a
type family Call' (args :: [Type]) out :: Type where
    Call' '[] out = out
    Call' (a ': args) out = a -> Call' args out

type Call :: Nat -> [Type] -> Type -> Type
type family Call arity args out where
    Call 0 '[a] out = TypeError ('Text "Arity can't be less than list of arguments")
    Call n '[a] out = Call' (Replicate n a)
    Call 0 '[]   out = out
    Call _ '[]   out = TypeError ('Text "Arity can't be less than list of arguments")
    Call n (a ': args) out = a -> Call (n-1) args out

I think the current implementation would be something like this

-- Hypothetical example, not realistic
data SubtractOrAddn n 
instance Function (SubtractOrAddn n) n '[Bool,Int] where  
    type Return (SubtractOrAddn n) = Int -- Assumes Return only takes in the r
    call :: Call n '[Bool,Int] (Return SubtractOrAdd4)
    call True = manyfoldl' (n - 1) (+) 0 -- This requires a surprising amount of type hackery to do, as well as knowledge of CPS. 
-- We also assume RequiredTypeArguments was used to define manyfoldl'
    call False a = manyfoldl' (n - 2) subtract a

-- One reason not to like is that we don't actually take in an '[Bool,Int].
-- Another is that one might think that 
-- > call @(SubtractOrAddn 2) takes in 2 ints, but it only takes in 1. 
-- This isn't apparent from the definition (imo)

This could be done instead:

instance (x ~ Bool ': Repeat (n-1) Int) => Function (SubtractOrAddn n) n x where 
    type Return (SubtractOrAddn n) = Int
    call = -- Same thing, 

This has the advantage of no special casing of Varidic functions in the class, and still allows users to make variadic functions, with no loss of generality! It’s also very clear in the implementation that the Int is repeated n-1 times.
Also, variadic functions require a large amount of type level shenanigans to even implement, so I’d be very surprised if many users decide to implement them. If you want, I can share my code for creating them. e.g. stuff like mayfoldl’. It took me a while to make so I’d be more than willing to share it with anyone (assuming I’d be referenced in the package).

1 Like

Its about to be midnight for me so I’ll have to chew on it in the morning but I definitely appreciate any help with this rather sizeable venture I’ve undertaken - many minds make for light work!

Shenanigans!

But yes, you can see why I found this an appropriate place to stop and think for a bit :grin:

1 Like

Before I retire for the day, I was just thinking that for Function 0 '[], it’s not really a function. Also, I just uploaded my variadic function stuff, it’s not polished or anything but feel free to take a look: haskell-variadic/Main.hs at main · ashokkimmel/haskell-variadic · GitHub

2 Likes

I like the concepts here, but I’m struggling with the feeling that we could do slightly better by combating the boolean-ness with parse don’t validate.

It feels odd for me that with all this talk of checking for lawfulness there’s no “proof” carried through to let you do the operation, or even that lawful doesn’t just do the operation for you.

I’d like to see the potential “Boolean blindness” mitigated in a good way.

5 Likes

It’s a value - a function with no arguments that returns itself!

It has always bothered me that these typeclasses were solely attached to the type eg Int when the proper convention is unambiguously defined as the combination of an operator and a type eg (+, Int).

But in all seriousness, Parse Dont Validate is a design pattern, and this is a mathematical description, so they are orthogonal but complementary. How do you know when to return Nothing instead of Just when parsing? The design pattern doesn’t omit checks, it performs them up front and uses types to propagate their legality - and these classes provide the laws that could be used during the parse.

Perhaps I need to add a Legal data family that connects a Lawful instance to a data type’s wrapper that expresses that it satisfies the law and does exactly this. I also feel like there is some interaction to be had with the WellDefined class…

Remember, these new classes don’t displace newtypes for distinguishing between validly different types eg like Natural and Whole numbers which are distinguished by the presence or lack of a zero 0 - we need those to combat boolean blindness, as you said. Newtype of this kind are thus still quite useful, and so I would expect to eg wrap up a value in such a newtype to propagate that law, once I have proven (witnessed?) that it obeys.

What the new classes do is replace the newtypes that get used as a shim to allow a single type to conform to and temporarily access multiple instances of the same typeclass - ie using Sum and Product to select between Addition and Multiplication during a fold so you can take advantage of Monoid. Newtypes of this kind are more of a hack. There is some nuance and overlap between the two use cases, so many newtypes get used in both ways.


I’m still shaking out this design quite heavily - as pleased as I am with the result, it is only my first successful attempt at expressing this rigor, and there are a few things I don’t like about it, so I’m not afraid to make changes - keep the feedback coming.

3 Likes

I feel (but can not prove) that this will not get you very far. One reason is that not all laws are decidable, because Result r a b might not even have a decent Eq instance that lawful can use. (E.g. point-wise operations on function spaces.) The other case where this fails is when the law involves universal quantification, possibly not only over the elements of a and b but also over type variables. Think about the Monad laws.
Of course you can keep adding type parameters to Lawful to cater for all the universal quantification, but the discussion on arity shows that this will be very tricky at best.

Why don’t you do it the categorical way and describe diagrams that ought to commute? For example, the associative law diagram has as starting point a triple and two paths that should be equal. But since equality may not be decidable, we just yield a pair that we require to be “morally equal”.

associative :: Semigroup a => (a,a,a) -> (a,a)
associative (x,y,z) = (x <> (y <> z), (x <> y) <> z)

Whenever Eq a is in scope, you can still post-compose with uncurry (==) and obtain the lawful test.

So you have gained no static guarantees, only more convenient ways to write test suites.

3 Likes

Yes, this approach worked for simple laws, but I’ve already found it does not scale well, especially for laws that invoke other laws. Take for example the trimedial relation, which states that any 3 elements generate a subset that has the medial property, which is wx * yz = wy * xz, so it has 3^4 = 81 combinations to test just in the first generation, and it continues going, so yeah. Some laws just cannot be proven by exhaustive iteration.

I’m not surprised, I hadn’t really even planned on doing any heavy algebraic laws in this library, it was more for vector and geometric spaces and I had to deal with the Num hierarchy because it was in the way, but when people mentioned algebraic laws, it I wanted to see what I could do, so this was a bit spur of the moment, and I got a lot further than I expected.

Another problem that I’ve discovered in this design is an insufficient description / separation of properties vs the functions that define them - take for example ‘identity’, we have (using addition to illustrate):

  • the identity element, 0
  • the identity law, x + 0 = x
  • the identity definition using inverse, 0 = x + (-x)

You might think that these belong in the same class, but no, they don’t because you can define identity elements without inverses (eg addition over natural numbers). And then there are things with left and right identities that unify, etc. All things that could and should be described better.

I’ll give this approach some thought, as it also might help with considering left and right laws, and how to handle them.

We mustn’t let perfect be the enemy of good - I think that is still a worthwhile benefit, being able to generate tests and test suites automatically based on laws, it might help provide an alternative to people writing -- INVARIANT: Foo in the code and then writing tests in a separate place to back it up.


So definitely more work to be done, but the best time to plant a tree for shade is 20 years ago, and the second best time is today. The improvements to the definition of Semigroup Monoid and Group alone have made this worth it for me.

Do you know about Lawvere theories? The unifying approach to laws there is to have a signature associated with a class of algebraic objects. In Haskell, we could hide the arity of the signature in a type family. Roughly speaking,

class Lawvere (theory :: Type -> Constraint) where
   type Signature theory :: Type -> Type
   operations :: theory a => Signature theory a -> a
   type Laws theory :: Type -> Type
   lawful :: (theory a, Eq a) => Laws theory a -> Bool

data MonoidLaws a where
   NeutralLeft :: a -> MonoidLaws a
   NeutralRight :: a -> MonoidLaws a
   Assoc :: a -> a -> a -> MonoidLaws a
lawfulMonoid :: (Monoid a, Eq a) => MonoidLaws a -> Bool
lawfulMonoid (NeutralLeft a) = a == mempty `mappend` a
lawfulMonoid (NeutralRight a) = a == a `mappend` mempty
lawfulMonoid (Assoc x y z) = mappend (mappend x y) z == mappend x (mappend y z)

instance Lawvere Monoid where
   type Signature Monoid a = Either () (a,a)
   operations (Left ()) = mempty
   operations (Right (x,y)) = mappend x y
   type Laws Monoid a = MonoidLaws a
   lawful = lawfulMonoid

Lawvere combined the signature and laws in a single category L (objects = arities, morphisms = laws) and instances of the theory (models) are then functors from this category. Homomorphisms are natural transformations between such functors.

For the sake of efficiency, one could formulate the laws as Haskell rewrite rules.

EDIT: For a more principled approach, see this topic.

9 Likes

Really, this is a very fixable issue with Haddock. At the very least it should preserve explicit qualification when there is ambiguity, but maybe always.

2 Likes

@ApothecaLabs has graciously allowed me to revive this thread because I’ve reached a point in the development of a library with a similar goal, flex, that I feel comfortable sharing my progress. I’ve hinted at this library in another thread, and I’ve included a slightly modified version of the vector math mentioned there in flex as well.

This library initially had the lofty goal of “improving the Num hierarchy”, and I claim that it has achieved it. It includes the theories of additive and multiplicative groups, rings, modules, algebras, and inner product spaces. The instances are tested with the use of Lawvere theories defined similarly to the above reply by @olf:

class Variety (var :: k -> Constraint) where
  type Requirements var :: k -> Constraint
  data Signature var :: k -> Type
  data Operations var :: k -> Type
    -- ^ usually a newtype wrapper for `x`
    -- c.f. instance Variety Semigroup, instance Variety Module
  operations :: (var x) => Signature var x -> Operations var x
  data Laws var :: k -> Type
  lawful :: (var x, Requirements var x) => Laws var x -> Bool

flex provides an alternative Functor/Foldable/Traversable/Applicative/Monad hierarchy, defined in Flex.Math.Category (that I hinted at in yet another thread). It also includes profunctors, bifunctors & indexed functors/foldables/traversables, which makes defining optics straightforward (see Flex.Math.Optics for a lightweight, nearly drop-in replacement for lens).

Since this library is not (yet) on Hackage, to get started, clone flex into your project’s parent directory and add it to your project using the cabal.project stanza:

-- in the cabal.project file
packages:
  .
  ../flex

-- in the .cabal file
build-depends:
  ..., flex

Documentation forthcoming.

7 Likes

(Minor aside not to detract from @mixphix s work, this library-discussion isn’t dead, just like 4th in priority queue behind my other projects and my health - I very much appreciate the work in my absence!)

3 Likes

The Requirements constraint in the Varietey class is used in the lawful method only. Perhaps one could off-load this into other classes that are dedicated to checking a law? Mapping to Bool might not be the only way to do it. (The theory itself does not care whether equations are decidable, only that they are expressible.) Perhaps the flex user wants to employ an SMT solver or QuickCheck for particular varieties or members thereof.

class Equate req var | var -> req where
   lawful :: (var x, req x) => Laws var x -> Bool

type Equate0 = Equate Eq
type Equate1 = Equate Eq1

class QCheck var where
   liftGen :: Arbitrary x => Gen (Laws var x)
   toProp  :: Law (var x) -> Property

One historical comment: The median operation in the Flex.Math.Lattice.Median can be found in Grau’s ternary boolean algebras from 1947. Using this operation, one can make any complemented pair in a distributive lattice the new top and bottom of another lattice structure. Prime exampe is the lattice M2 which has one “vertical” and one “horizontal” lattice structure.