Automated reasoning in F#, Scala, Haskell, C++, and Julia
phdp.github.io
phdp.github.io
Suppose we represented Add instead as:
case class Add(exprs: NonEmptyList[Expr with NotAdd]) extends Expr
case class Const(value: Int) extends Expr with NotAdd
case class ... extends Expr with NotAdd
Now nested Add's are a compile time error, and your expressions are automatically simpler. I'm not sure how easy this is to do in a language without subtypes.I imagine it could be implemented with typeclasses in Haskell somehow (probably using existential types), but I doubt it would be as easy. (Any Haskell gurus care to comment?)
To see a more detailed illustration of how it would work, consider this free boolean algebra implementation: https://github.com/wingify/Oldmonk/blob/master/src/main/scal...
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
data IsAdd = IsAdd | NotAdd
data Expr :: IsAdd -> * where
Add :: [Expr 'NotAdd] -> Expr 'IsAdd
Const :: Integer -> Expr 'NotAdd
... -> Expr 'NotAdd
You could also use True and False instead of Add and IsAdd, or even type-level string literals as arbitrary symbols: data Expr :: Symbol -> * where
Add :: [Expr "NotAdd"] -> Expr "IsAdd"
Const :: Integer -> Expr "NotAdd"It's possible to express multiple properties using the same phantom type parameter is both languages. However, as you sort of alluded to, this encoding is much simpler if your language supports subtyping. Since OCaml does support subtyping, you can use polymorphic variants[1] for this purpose. For example, if you had the NotAdd property as well as the NotMult property, you could write down the type for an operation that depends on just the NotAdd operation like this:
val notAddOp : ([> `NotAdd] as 'a) expr -> 'a t
... and write down an operation that depends on both like this: val bothOp : ([> `NotAdd | `NotMult] as 'a) expr -> 'a u
In Haskell, things would get more complicated. The type of the first operation would like more like this: notAddOp : Contains NotAdd a -> Expr a -> T a
... where the first argument is acting like a proof witness that the type parameter 'a in some sense "contains" tye type NotAdd. Similarly again for the second, except with two witnesses: bothOp : Contains NotAdd a -> Contains NotMult a -> Expr a -> T a
I haven't thought it completely through, but I'm almost certain that a combination of type classes and GCH extensions would allow you to turn those proof witness arguments into type class contexts.Anyways, doing this sort of encoding of properties in types is all well and good until you start considering more realistic examples. Even in the one you presented, it's going to cause you problems if, say, you want to write an inliner for your language. Substituting an expression for a variable within a "NotAdd" addition may very well break the "NotAdd" property. This means that your inliner has to be aware of that property, so that it can preserve it while doing its job. In other words, the option of writing code that will break an invariant, and then immediately recover the invariant, is no longer on the table when you take this approach to verifying the correctness of your code. That may seem bad, until you try to verifiably balanced red-black tree without learning Coq and reading this[2].
Life's full of trade-offs.
[0]: https://en.wikipedia.org/wiki/Generalized_algebraic_data_typ...
[1]: https://realworldocaml.org/v1/en/html/variants.html#polymorp...
Also, FreeBool[A] (the example given) actually handles substitution quite well. The `flatMap` operation will properly flatten things out:
And(A,B,C).flatMap(C => D & E) = AND(A,B,D,E)
Yes, the compiler did help me get that right.Again, that example's way overshooting the complexity needed to prove the point. Even a seemingly simple invariant like the red-black balancing property is one that is impractical to preserve in this fashion without a proof assistant. And even then its practicality is still dubious.
In the author's example, he matches on a few rules. A zero rule, an identity rule, an evaluation rule, and the default like so:
let simplify1 e =
match e with
| Add (Const 0, x)
| Add (x, Const 0)
| Mul (x, Const 1)
| Mul (Const 1, x) -> x
| Mul (x, Const 0)
| Mul (Const 0, x) -> Const 0
| Add (Const a, Const b) -> Const (a + b)
| Mul (Const a, Const b) -> Const (a * b)
| _ -> e
Active Patterns let you define these patterns as reusable blocks like so: let (|IdentityExpr|_|) = function
| Add (Const 0, x)
| Add (x, Const 0)
| Mul (x, Const 1)
| Mul (Const 1, x) -> Some(x)
| x -> None
It has the effect of making your pattern matching code more readable with a syntax that somewhat resembles a Prolog horn clause. As a bonus, you can mix and match these patterns when doing advanced reasoning. let simplify1 e =
match e with
| IdentityExpr(x) -> x
| ZeroExpr() -> 0
| ConstantExpr(expr) -> Const(eval expr)
| _ -> eThe book is pretty expensive, but I want to get a copy.
It was below "Sum types and pattern matching are awesome."
I agree, it's a pricy book though.
It's pretty common and pretty unfortunate...
https://news.ycombinator.com/item?id=9877314
I checked back on Amazon, and it was over $80. Demand drove price. It is still on B&N for $86 right now:
http://www.barnesandnoble.com/w/common-lisp-modules-mark-wat...
[1] http://paulkoerbitz.de/posts/Sum-Types-Visitors-and-the-Expr...
[1] - https://gist.github.com/EricLagerg/beb586714cd9f509c812