According to this course, all constructors (for inductive types) are injective and disjoint:
...Similar principles apply to all inductively defined types: all
constructors are injective, and the values built from distinct
constructors are never equal. For lists, the cons constructor is
injective and nil is different from every non-empty list. For
booleans, true and false are unequal.
(And the inversion tactic based on this assumption)
I am just wondering how do we know this assumption holds?
How do we know that, e.g., we cannot define natural numbers based on
1) a Successor and maybe a "Double" constructor like this:
Inductive num: Type :=
| O : num
| S : num -> num
| D : num -> num.
and
2) some plus function so that one number 2 can be reached via two different sequences/routes of constructors, S (S O) and D (S O)?
What's the mechanism in Coq that ensures the above will never happen?
P.S.
I am not suggesting the above num example is possible. I am just wondering what makes it impossible.
Thanks
When you define an inductive data type in Coq, you are essentially
defining a tree type. Each constructor gives a kind of node that is
allowed to occur in your tree, and its arguments determine the
children and elements that that node can have. Finally, functions
defined on inductive types (with the match clause) can check the
constructors that were used to produce a value of that type in
arbitrary ways. This makes Coq constructors very different from
constructors you see in an OO language, for instance. An object
constructor is implemented as a regular function that initializes a
value of a given type; Coq constructors, on the other hand, are
enumerating the possible values that the representation of our type
allows. To understand this difference better, we can compare the
different functions we can define on an object in a traditional OO
language, and on an element of an inductive type in Coq. Let's use
your num type as an example. Here's an object-oriented definition:
class Num {
int val;
private Num(int v) {
this.val = v;
}
/* These are the three "constructors", even though they
wouldn't correspond to what is called a "constructor" in
Java, for instance */
public static zero() {
return new Num(0);
}
public static succ(Num n) {
return new Num(n.val + 1);
}
public static doub(Num n) {
return new Num(2 * n.val);
}
}
And here's a definition in Coq:
Inductive num : Type :=
| zero : num
| succ : num -> num
| doub : num -> num.
In the OO example, when we write a function that takes a Num
argument, there's no way of knowing which "constructor" was used to
produce that value, because this information is not stored in the
val field. In particular Num.doub(Num.succ(Num.zero())) and
Num.succ(Num.succ(Num.zero())) would be equal values.
In the Coq example, on the other hand, things change, because we can
determine which constructor was used to form a num value, thanks to
the match statement. For instance, using Coq strings, we could write
a function like this:
Require Import Coq.Strings.String.
Open Scope string_scope.
Definition cons_name (n : num) : string :=
match n with
| zero => "zero"
| succ _ => "succ"
| doub _ => "doub"
end.
In particular, even though your intended meaning for the constructors
implies that succ (succ zero) and doub (succ zero) should be
"morally" equal, we can distinguish them by applying the cons_name
function to them:
Compute cons_name (doub (succ zero)). (* ==> "doub" *)
Compute cons_name (succ (succ zero)). (* ==> "succ" *)
As a matter of fact, we can use match to distinguish between succ
and doub in arbitrary ways:
match n with
| zero => false
| succ _ => false
| doub _ => true
end
Now, a = b in Coq means that there is no possible way we can
distinguish between a and b. The above examples show why doub
(succ zero) and succ (succ zero) cannot be equal, because we can
write functions that don't respect the meaning that we had in mind
when we wrote that type.
This explains why constructors are disjoint. That they are injective
is actually also a consequence of pattern-matching. For instance,
suppose that we wanted to prove the following statement:
forall n m, succ n = succ m -> n = m
We can begin the proof with
intros n m H.
Leading us to
n, m : num
H : succ n = succ m
===============================
n = m
Notice that this goal is by simplification equivalent to
n, m : num
H : succ n = succ m
===============================
match succ n with
| succ n' => n' = m
| _ => True
end
If we do rewrite H, we obtain
n, m : num
H : succ n = succ m
===============================
match succ m with
| succ n' => n' = m
| _ => True
end
which simplifies to
n, m : num
H : succ n = succ m
===============================
m = m
At this point, we can conclude with reflexivity. This technique is
quite general, and is actually at the core of what inversion does.
There is none: the constructors O, S and D are indeed disjoint and injective but the semantics for nums you have in your head is not, as a function, injective.
That is why num would usually be considered to be a bad representation of the natural numbers: working up-to equivalence is quite annoying.
Related
I am currently reading the first volume of the softwarefoundations series. In one of the exercises I am supposed to write a function which turns a natural number (unary form) into the equivalent binary number.
This is my code/approach:
Inductive bin : Type :=
| Z
| B0 (n : bin)
| B1 (n : bin).
Fixpoint evenb (n:nat) : bool :=
match n with
| O => true
| S O => false
| S (S n') => evenb n'
end.
Fixpoint nat_to_bin (n:nat) : bin :=
match n with
| 0 => Z
| 1 => B1 Z
| 2 => B0 (B1 Z)
| m => match evenb(m) with
| true => B0 (nat_to_bin (Nat.div m 2))
| false => B1 (nat_to_bin (Nat.modulo m 2))
end
end.
I am using https://jscoq.github.io/scratchpad.html to work on these exercises.
Now I get this error message:
Recursive definition of nat_to_bin is ill-formed. In environment
nat_to_bin : nat -> bin
n : nat
n0 : nat
n1 : nat
n2 : nat
Recursive call to nat_to_bin has principal argument equal to "Nat.div
n 2 " instead of one of the following variables: "n0" "n1" "n2" .
Recursive definition is: "fun n : nat => match n with
| 0 => Z
| 1 => B1 Z
| 2 => B0 (B1 Z )
| S (S (S _) ) =>
if evenb n then B0 (nat_to_bin (Nat.div n 2 ) )
else B1 (nat_to_bin (Nat.modulo n 2 ) )
end " .
To retain good logical properties, all functions definable in Coq are terminating. To enforce that, there is a restriction on fixpoint definitions, like the one you are trying to do, called the guard condition. This restriction is roughly that the recursive call can only be done on subterms of the argument of the function.
This is not the case in your definition, where you apply nat_to_bin to the terms (Nat.div n 2) and (Nat.modulo n 2) which are functions applied to n. Although you can mathematically prove that those are always smaller than n, they are no subterms of n, so your function does not respect the guard condition.
If you wanted to define nat_to_bin in the way you are doing, you would need to resort to well-founded induction, which would use the well-foundedness of the order on nat to allow you to call you function on any term you can prove smaller than n. However, this solution is quite complex, because it would force you to do some proofs that are not that easy.
Instead, I would advise going another way: just above in the book, it is suggested to define a function incr : bin -> bin that increments a binary number by one. You can use that one to define nat_to_bin by a simple recursion on n, like this:
Fixpoint nat_to_bin (n:nat) : bin :=
match n with
| 0 => Z
| S n' => incr (nat_to_bin n')
end.
As for incr itself, you should also be able to write it down using a simple recursion on your binary number, as they are written with low-order bit outside.
In pure functional languages like Haskell, is there an algorithm to get the inverse of a function, (edit) when it is bijective? And is there a specific way to program your function so it is?
In some cases, yes! There's a beautiful paper called Bidirectionalization for Free! which discusses a few cases -- when your function is sufficiently polymorphic -- where it is possible, completely automatically to derive an inverse function. (It also discusses what makes the problem hard when the functions are not polymorphic.)
What you get out in the case your function is invertible is the inverse (with a spurious input); in other cases, you get a function which tries to "merge" an old input value and a new output value.
No, it's not possible in general.
Proof: consider bijective functions of type
type F = [Bit] -> [Bit]
with
data Bit = B0 | B1
Assume we have an inverter inv :: F -> F such that inv f . f ≡ id. Say we have tested it for the function f = id, by confirming that
inv f (repeat B0) -> (B0 : ls)
Since this first B0 in the output must have come after some finite time, we have an upper bound n on both the depth to which inv had actually evaluated our test input to obtain this result, as well as the number of times it can have called f. Define now a family of functions
g j (B1 : B0 : ... (n+j times) ... B0 : ls)
= B0 : ... (n+j times) ... B0 : B1 : ls
g j (B0 : ... (n+j times) ... B0 : B1 : ls)
= B1 : B0 : ... (n+j times) ... B0 : ls
g j l = l
Clearly, for all 0<j≤n, g j is a bijection, in fact self-inverse. So we should be able to confirm
inv (g j) (replicate (n+j) B0 ++ B1 : repeat B0) -> (B1 : ls)
but to fulfill this, inv (g j) would have needed to either
evaluate g j (B1 : repeat B0) to a depth of n+j > n
evaluate head $ g j l for at least n different lists matching replicate (n+j) B0 ++ B1 : ls
Up to that point, at least one of the g j is indistinguishable from f, and since inv f hadn't done either of these evaluations, inv could not possibly have told it apart – short of doing some runtime-measurements on its own, which is only possible in the IO Monad.
⬜
You can look it up on wikipedia, it's called Reversible Computing.
In general you can't do it though and none of the functional languages have that option. For example:
f :: a -> Int
f _ = 1
This function does not have an inverse.
Not in most functional languages, but in logic programming or relational programming, most functions you define are in fact not functions but "relations", and these can be used in both directions. See for example prolog or kanren.
Tasks like this are almost always undecidable. You can have a solution for some specific functions, but not in general.
Here, you cannot even recognize which functions have an inverse. Quoting Barendregt, H. P. The Lambda Calculus: Its Syntax and Semantics. North Holland, Amsterdam (1984):
A set of lambda-terms is nontrivial if it is neither the empty nor the full set. If A and B are two nontrivial, disjoint sets of lambda-terms closed under (beta) equality, then A and B are recursively inseparable.
Let's take A to be the set of lambda terms that represent invertible functions and B the rest. Both are non-empty and closed under beta equality. So it's not possible to decide whether a function is invertible or not.
(This applies to the untyped lambda calculus. TBH I don't know if the argument can be directly adapted to a typed lambda calculus when we know the type of a function that we want to invert. But I'm pretty sure it will be similar.)
If you can enumerate the domain of the function and can compare elements of the range for equality, you can - in a rather straightforward way. By enumerate I mean having a list of all the elements available. I'll stick to Haskell, since I don't know Ocaml (or even how to capitalise it properly ;-)
What you want to do is run through the elements of the domain and see if they're equal to the element of the range you're trying to invert, and take the first one that works:
inv :: Eq b => [a] -> (a -> b) -> (b -> a)
inv domain f b = head [ a | a <- domain, f a == b ]
Since you've stated that f is a bijection, there's bound to be one and only one such element. The trick, of course, is to ensure that your enumeration of the domain actually reaches all the elements in a finite time. If you're trying to invert a bijection from Integer to Integer, using [0,1 ..] ++ [-1,-2 ..] won't work as you'll never get to the negative numbers. Concretely, inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3) will never yield a value.
However, 0 : concatMap (\x -> [x,-x]) [1..] will work, as this runs through the integers in the following order [0,1,-1,2,-2,3,-3, and so on]. Indeed inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3) promptly returns -4!
The Control.Monad.Omega package can help you run through lists of tuples etcetera in a good way; I'm sure there's more packages like that - but I don't know them.
Of course, this approach is rather low-brow and brute-force, not to mention ugly and inefficient! So I'll end with a few remarks on the last part of your question, on how to 'write' bijections. The type system of Haskell isn't up to proving that a function is a bijection - you really want something like Agda for that - but it is willing to trust you.
(Warning: untested code follows)
So can you define a datatype of Bijection s between types a and b:
data Bi a b = Bi {
apply :: a -> b,
invert :: b -> a
}
along with as many constants (where you can say 'I know they're bijections!') as you like, such as:
notBi :: Bi Bool Bool
notBi = Bi not not
add1Bi :: Bi Integer Integer
add1Bi = Bi (+1) (subtract 1)
and a couple of smart combinators, such as:
idBi :: Bi a a
idBi = Bi id id
invertBi :: Bi a b -> Bi b a
invertBi (Bi a i) = (Bi i a)
composeBi :: Bi a b -> Bi b c -> Bi a c
composeBi (Bi a1 i1) (Bi a2 i2) = Bi (a2 . a1) (i1 . i2)
mapBi :: Bi a b -> Bi [a] [b]
mapBi (Bi a i) = Bi (map a) (map i)
bruteForceBi :: Eq b => [a] -> (a -> b) -> Bi a b
bruteForceBi domain f = Bi f (inv domain f)
I think you could then do invert (mapBi add1Bi) [1,5,6] and get [0,4,5]. If you pick your combinators in a smart way, I think the number of times you'll have to write a Bi constant by hand could be quite limited.
After all, if you know a function is a bijection, you'll hopefully have a proof-sketch of that fact in your head, which the Curry-Howard isomorphism should be able to turn into a program :-)
I've recently been dealing with issues like this, and no, I'd say that (a) it's not difficult in many case, but (b) it's not efficient at all.
Basically, suppose you have f :: a -> b, and that f is indeed a bjiection. You can compute the inverse f' :: b -> a in a really dumb way:
import Data.List
-- | Class for types whose values are recursively enumerable.
class Enumerable a where
-- | Produce the list of all values of type #a#.
enumerate :: [a]
-- | Note, this is only guaranteed to terminate if #f# is a bijection!
invert :: (Enumerable a, Eq b) => (a -> b) -> b -> Maybe a
invert f b = find (\a -> f a == b) enumerate
If f is a bijection and enumerate truly produces all values of a, then you will eventually hit an a such that f a == b.
Types that have a Bounded and an Enum instance can be trivially made RecursivelyEnumerable. Pairs of Enumerable types can also be made Enumerable:
instance (Enumerable a, Enumerable b) => Enumerable (a, b) where
enumerate = crossWith (,) enumerate enumerate
crossWith :: (a -> b -> c) -> [a] -> [b] -> [c]
crossWith f _ [] = []
crossWith f [] _ = []
crossWith f (x0:xs) (y0:ys) =
f x0 y0 : interleave (map (f x0) ys)
(interleave (map (flip f y0) xs)
(crossWith f xs ys))
interleave :: [a] -> [a] -> [a]
interleave xs [] = xs
interleave [] ys = []
interleave (x:xs) ys = x : interleave ys xs
Same goes for disjunctions of Enumerable types:
instance (Enumerable a, Enumerable b) => Enumerable (Either a b) where
enumerate = enumerateEither enumerate enumerate
enumerateEither :: [a] -> [b] -> [Either a b]
enumerateEither [] ys = map Right ys
enumerateEither xs [] = map Left xs
enumerateEither (x:xs) (y:ys) = Left x : Right y : enumerateEither xs ys
The fact that we can do this both for (,) and Either probably means that we can do it for any algebraic data type.
Not every function has an inverse. If you limit the discussion to one-to-one functions, the ability to invert an arbitrary function grants the ability to crack any cryptosystem. We kind of have to hope this isn't feasible, even in theory!
In some cases, it is possible to find the inverse of a bijective function by converting it into a symbolic representation. Based on this example, I wrote this Haskell program to find inverses of some simple polynomial functions:
bijective_function x = x*2+1
main = do
print $ bijective_function 3
print $ inverse_function bijective_function (bijective_function 3)
data Expr = X | Const Double |
Plus Expr Expr | Subtract Expr Expr | Mult Expr Expr | Div Expr Expr |
Negate Expr | Inverse Expr |
Exp Expr | Log Expr | Sin Expr | Atanh Expr | Sinh Expr | Acosh Expr | Cosh Expr | Tan Expr | Cos Expr |Asinh Expr|Atan Expr|Acos Expr|Asin Expr|Abs Expr|Signum Expr|Integer
deriving (Show, Eq)
instance Num Expr where
(+) = Plus
(-) = Subtract
(*) = Mult
abs = Abs
signum = Signum
negate = Negate
fromInteger a = Const $ fromIntegral a
instance Fractional Expr where
recip = Inverse
fromRational a = Const $ realToFrac a
(/) = Div
instance Floating Expr where
pi = Const pi
exp = Exp
log = Log
sin = Sin
atanh = Atanh
sinh = Sinh
cosh = Cosh
acosh = Acosh
cos = Cos
tan = Tan
asin = Asin
acos = Acos
atan = Atan
asinh = Asinh
fromFunction f = f X
toFunction :: Expr -> (Double -> Double)
toFunction X = \x -> x
toFunction (Negate a) = \a -> (negate a)
toFunction (Const a) = const a
toFunction (Plus a b) = \x -> (toFunction a x) + (toFunction b x)
toFunction (Subtract a b) = \x -> (toFunction a x) - (toFunction b x)
toFunction (Mult a b) = \x -> (toFunction a x) * (toFunction b x)
toFunction (Div a b) = \x -> (toFunction a x) / (toFunction b x)
with_function func x = toFunction $ func $ fromFunction x
simplify X = X
simplify (Div (Const a) (Const b)) = Const (a/b)
simplify (Mult (Const a) (Const b)) | a == 0 || b == 0 = 0 | otherwise = Const (a*b)
simplify (Negate (Negate a)) = simplify a
simplify (Subtract a b) = simplify ( Plus (simplify a) (Negate (simplify b)) )
simplify (Div a b) | a == b = Const 1.0 | otherwise = simplify (Div (simplify a) (simplify b))
simplify (Mult a b) = simplify (Mult (simplify a) (simplify b))
simplify (Const a) = Const a
simplify (Plus (Const a) (Const b)) = Const (a+b)
simplify (Plus a (Const b)) = simplify (Plus (Const b) (simplify a))
simplify (Plus (Mult (Const a) X) (Mult (Const b) X)) = (simplify (Mult (Const (a+b)) X))
simplify (Plus (Const a) b) = simplify (Plus (simplify b) (Const a))
simplify (Plus X a) = simplify (Plus (Mult 1 X) (simplify a))
simplify (Plus a X) = simplify (Plus (Mult 1 X) (simplify a))
simplify (Plus a b) = (simplify (Plus (simplify a) (simplify b)))
simplify a = a
inverse X = X
inverse (Const a) = simplify (Const a)
inverse (Mult (Const a) (Const b)) = Const (a * b)
inverse (Mult (Const a) X) = (Div X (Const a))
inverse (Plus X (Const a)) = (Subtract X (Const a))
inverse (Negate x) = Negate (inverse x)
inverse a = inverse (simplify a)
inverse_function x = with_function inverse x
This example only works with arithmetic expressions, but it could probably be generalized to work with lists as well. There are also several implementations of computer algebra systems in Haskell that may be used to find the inverse of a bijective function.
No, not all functions even have inverses. For instance, what would the inverse of this function be?
f x = 1
Im pretty much new to Haskell, so if Im missing key concept, please point it out.
Lets say we have these two functions:
fact n
| n == 0 = 1
| n > 0 = n * (fact (n - 1))
The polymorphic type for fact is (Eq t, Num t) => t -> t Because n is used in the if condition and n must be of valid type to do the == check. Therefor t must be a Number and t can be of any type within class constraint Eq t
fib n
| n == 1 = 1
| n == 2 = 1
| n > 2 = fib (n - 1) + fib (n - 2)
Then why is the polymorphic type of fib is (Eq a, Num a, Num t) => a -> t?
I don't understand, please help.
Haskell always aims to derive the most generic type signature.
Now for fact, we know that the type of the output, should be the same as the type of the input:
fact n | n == 0 = 1
| n > 0 = n * (fact (n - 1))
This is due to the last line. We use n * (fact (n-1)). So we use a multiplication (*) :: a -> a -> a. Multiplication thus takes two members of the same type and returns a member of that type. Since we multiply with n, and n is input, the output is of the same type as the input. Since we use n == 0, we know that (==) :: Eq a => a -> a -> Bool so that means that that input type should have Eq a =>, and furthermore 0 :: Num a => a. So the resulting type is fact :: (Num a, Eq a) => a -> a.
Now for fib, we see:
fib n | n == 1 = 1
| n == 2 = 1
| n > 2 = fib (n - 1) + fib (n - 2)
Now we know that for n, the type constraints are again Eq a, Num a, since we use n == 1, and (==) :: Eq a => a -> a -> Bool and 1 :: Num a => a. But the input n is never directly used in the output. Indeed, the last line has fib (n-1) + fib (n-2), but here we use n-1 and n-2 as input of a new call. So that means we can safely asume that the input type and the output type act independently. The output type, still has a type constraint: Num t: this is since we return 1 for the first two cases, and 1 :: Num t => t, and we also return the addition of two outputs: fib (n-1) + fib (n-2), so again (+) :: Num t => t -> t -> t.
The difference is that in fact, you use the argument directly in an arithmetic expression which makes up the final result:
fact n | ... = n * ...
IOW, if you write out the expanded arithmetic expression, n appears in it:
fact 3 ≡ n * (n-1) * (n-2) * 1
This fixes that the argument must have the same type as the result, because
(*) :: Num n => n -> n -> n
Not so in fib: here the actual result is only composed of literals and of sub-results. IOW, the expanded expression looks like
fib 3 ≡ (1 + 1) + 1
No n in here, so no unification between argument and result required.
Of course, in both cases you also used n to decide how this arithmetic expression looks, but for that you've just used equality comparisons with literals, whose type is not connected to the final result.
Note that you can also give fib a type-preservig signature: (Eq a, Num a, Num t) => a -> t is strictly more general than (Eq t, Num t) => t -> t. Conversely, you can make a fact that doesn't require input- and output to be the same type, by following it with a conversion function:
fact' :: (Eq a, Integral a, Num t) => a -> t
fact' = fromIntegral . fact
This doesn't make a lot of sense though, because Integer is pretty much the only type that can reliably be used in fact, but to achieve that in the above version you need to start out with Integer. Hence if anything, you should do the following:
fact'' :: (Eq t, Integral a, Num t) => a -> t
fact'' = fact . fromIntegral
This can then be used also as Int -> Integer, which is somewhat sensible.
I'd recommend to just keep the signature (Eq t, Num t) => t -> t though, and only add conversion operations where it's actually needed. Or really, what I'd recommend is to not use fact at all – this is a very expensive function that's hardly ever really useful in practice; most applications that naïvely end up with a factorial really just need something like binomial coefficients, and those can be implemented more efficiently without a factorial.
I'm reviewing an old exam in my Haskell programming course and I can't seem to wrap my head around this function (I think there is just too little information given).
The code given is
myId x = x
function n f
| n > 0 = f . function (n-1) f
| otherwise = myId
I know that if I for example call the function with the input 2 (*2), I will get a function as result. And if I call it with (-2) (*2) 1 I will get the result 1.
I just don't know how? Also I can't wrap my head around the typecast of the function.
I know that these two options are correct but I don't understand why (probably parentheses that confuse me at the moment).
function :: (Num a, Ord a) => a -> (a -> a) -> a -> a
function :: (Num a, Ord b) => a -> (b -> b) -> b -> b
Anyone that can clarify how I should "read" this function and how I should understand how the typecast works (been reading my Programming in Haskell literature and from Learn You a Haskell but been going in circle for a few days now).
function takes some number n and a function f :: a -> a, and composes that function with itself n times, returning another function of type a -> a. When the returned function is applied to a value of type a, the result is essentially the same as executing f in a loop n times, using the output of each previous step as the input for the next.
Perhaps it is easier to see the similarity if the final parameter is made explicit:
function :: (Ord a, Num a) -> a -> (b -> b) -> b -> b
function n f x
| n > 0 = f (function (n-1) f x)
| otherwise = x
This is functionally equivalent to your point-free function.
In Haskell, a function f :: a -> b -> c can be interpreted either as "a function that takes an a and a b and returns a c" or "a function that takes an a and returns a function from b to c". When you apply a function to one or more inputs, think of each input as eliminating one of the function's arguments. In this instance, function 10 returns a new function with type (a -> a) -> a -> a, and function 2 (*2) returns a function with type Num a => a -> a.
When you think of it this way, it should hopefully be clear why function (-2) (*2) 1 returns a number while function 2 (*2) returns a function. There is no type casting going on; when you are applying the three argument function to two inputs, you get another function back instead of a value, since you didn't provide the final input needed to compute that value.
I want to prove the lemma below. I am trying to to use tactic 'destruct', but I
can't prove it. Please any body guide me how can I prove such lemmas. I can prove it for EmptyString, but not for variables s1 and s2. Thanks
Inductive nat : Set :=
| O : nat
| S : nat -> nat.
Inductive string : Set :=
| EmptyString : string
| String : ascii -> string -> string.
Fixpoint CompStrings (sa : string) (sb : string) {struct sb}: bool :=
match sa with
| EmptyString => match sb with
| EmptyString => true
| String b sb'=> false
end
| String a sa' => match sb with
| EmptyString => false
| String b sb'=> CompStrings sa' sb'
end
end.
Lemma Eq_lenght : forall (s1 s2 : string),
(CompStrings s1 s2) = true -> (Eq_nat (length s1) (length s2)) = true.
First off, let me argue about style. You could have written your function CompStrings as this:
Fixpoint CompStrings' (sa : string) (sb : string) {struct sb}: bool :=
match sa, sb with
| EmptyString, EmptyString => true
| EmptyString, _
| _, EmptyString => false
| String a sa', String b sb'=> CompStrings sa' sb'
end.
I find it easier to read. Here is a proof it's the same as yours, in case you're suspicious:
Theorem CompStrings'ok: forall sa sb, CompStrings sa sb = CompStrings' sa sb.
Proof.
intros. destruct sa, sb; simpl; reflexivity.
Qed.
Now, this will be a two-fold answer. First I'm just going to hint you at the direction for the proof. Then, I'll give you a full proof that I encourage you not to read before you've tried it yourself.
First off, I assumed this definition of length since you did not provide it:
Fixpoint length (s: string): nat :=
match s with
| EmptyString => O
| String _ rest => S (length rest)
end.
And since I did not have Eq_nat either, I proceeded to prove that the lengths are propositionally equal. It should be fairly trivial to adapt to Eq_nat.
Lemma Eq_length' : forall (s1 s2 : string),
CompStrings s1 s2 = true ->
length s1 = length s2.
Proof.
induction s1.
(* TODO *)
Admitted.
So here is the start! You want to prove a property about the inductive data type string. The thing is, you will want to proceed by case analysis, but if you just do it with destructs, it'll never end. This is why we proceed by induction. That is, you will need to prove that if s1 is the EmptyString, then the property holds, and that if the property holds for a substring, then it holds for the string with one character added. The two cases are fairly simple, in each case you can proceed by case analysis on s2 (that is, using destruct).
Note that I did not do intros s1 s2 C. before doing induction s1.. This is fairly important for one reason: if you do it (try!), your induction hypothesis will be too constrained as it will talk about one particular s2, rather than being quantified by it. This can be tricky when you start doing proofs by induction. So, be sure to try to continue this proof:
Lemma Eq_length'_will_fail : forall (s1 s2 : string),
CompStrings s1 s2 = true ->
length s1 = length s2.
Proof.
intros s1 s2 C. induction s1.
(* TODO *)
Admitted.
eventually, you'll find that your induction hypothesis can't be applied to your goal, because it's speaking about a particular s2.
I hope you've tried these two exercises.
Now if you're stuck, here is one way to prove the first goal.
Don't cheat! :)
Lemma Eq_length' : forall (s1 s2 : string),
CompStrings s1 s2 = true ->
length s1 = length s2.
Proof.
induction s1.
intros s2 C. destruct s2. reflexivity. inversion C.
intros s2 C. destruct s2. inversion C. simpl in *. f_equal.
exact (IHs1 _ C).
Qed.
To put that in intelligible terms:
let's prove the property forall s2, CompStrings s1 s2 = true -> length s1 = s2 by induction on s1:
in the case where s1 is the EmptyString, let's look at the shape of s2:
s2 is the EmptyString, then both lengths are equal to 0, so reflexivity.;
s2 is a String _ _, so there is a contradiction in the hypothesis, shown by inversion C.;
in the case where s1 is a String char1 rest1, let's look at the shape of s2, supposing the property true for rest:
s2 is the EmptyString, so there is a contradiction in the hypothesis, show by inversion C.;
s2 is a String char2 rest2, then length s1 = S (length rest1) and length s2 = S (length rest2), therefore we need to prove S (length rest1) = S (length rest2). Also, the hypothesis C simplifies into C: CompStrings rest1 rest2 = true. It is the perfect occasion to use the induction hypothesis to prove that length rest1 = length rest2, and then use that result somehow to prove the goal.
Note that for that last step, there are many ways to proceed to prove S (length rest1) = S (length rest2). One of which is using f_equal. which asks you to prove a pairwise equality between the parameters of the constructor. You could also use a rewrite (IHs1 _ C). then use reflexivity on that goal.
Hopefully this will help you not only solve this particular goal, but get a first understanding at proofs by induction!
To close on this, here are two interesting links.
This presents the basics of induction (see paragraph "Induction on lists").
This explains, better than me, why and how to generalize your induction hypotheses. You'll learn how to solve the goal where I did intros s1 s2 C. by putting back the s2 in the goal before starting the induction, using the tactic generalize (dependent).
In general, I'd recommend reading the whole book. It's slow-paced and very didactic.