On simplifying OCaml expressions

When writing a program in OCaml (resp. when writing a compiler for OCaml programs), one is tempted to simplify one’s program (resp. source programs). The goal of the following series of exercises is to reflect on which simplifications are valid and which are not. To this end, we introduce the following notion of equivalence: two expressions are observationally equivalent if evaluating them carries out the same computation.

Resources

The computational domain of discourse

Each OCaml program is inductively assembled using the syntax constructors enumerated in the BNF of OCaml:

<type> ::= int | bool | char | string | <type> * ... * <type> | unit | <type> -> <type>
<type_environment> ::= . | (<name> : <type>), <type_environment>
<name> ::= a concatenation of characters starting with a lowercase character
<formal> ::= <name> | () | (<formal>, ..., <formal>)
<expression> ::= <integer> | <boolean> | <character> | <string> | if <expression> then <expression> else <expression> | (<expression>, ..., <expression>) | <name> | fun <formal> -> <expression> | <expression> <expression> | let <formal> = <definiens> in <expression> | let <formal> = <definiens> and ... and <formal> = <definiens> in <expression> | ...
<integer> ::= 0 | 1 | -1 | 2 | -2 | ...
<boolean> ::= true | false
<character> ::= one character delimited by two single quotes
<string> ::= a series of characters delimited by two double quotes

Each of these syntactic constructions was introduced by describing:

  • their syntax, i.e., how they are written,
  • their typing constraints, i.e., the conditions under which the constructed expression has a type, and
  • how they are evaluated.

For example,

  • given a test expression, e1, a consequent expression, e2, and an alternative expression, e3, the corresponding conditional expression is written as if e1 then e2 else e3;

  • in any type environment G, the expression if e1 then e2 else e3 has type t whenever in that environment, e1 (the test) has type bool and each of e2 (the consequent) and e3 (the alternative) has type t:

    IFG |- e1 : boolG |- e2 : tG |- e3 : t
    G |- if e1 then e2 else e3 : t
  • to evaluate if e1 then e2 else e3 in an environment, we first evaluate e1 in this environment;
    • if evaluating e1 does not terminate, then evaluating if e1 then e2 else e3 does not terminate either;
    • if evaluating e1 raises an error, then evaluating if e1 then e2 else e3 also raises this error;
    • if evaluating e1 completes, it yields a Boolean (due to the typing rule):
      • if this Boolean is true, then evaluating if e1 then e2 else e3 in an environment reduces to evaluating e2 in this environment, and
      • if this Boolean is false, then evaluating if e1 then e2 else e3 in an environment reduces to evaluating e3 in this environment.

The Food for thought in Week 05 recapitulates how each expression is evaluated in OCaml:

  • either evaluation does not terminate,
  • or evaluation raises an error,
  • or evaluation yields a value.

Either way, evaluation may have side effects: data may be read or written (e.g., to trace the computation), files may be open or closed, etc.

Evaluation might not terminate

Evaluating an expression might not terminate (or again: might “diverge”):

  • For example, consider the following recursive OCaml function:

    let rec diverge_silently () =
      diverge_silently ();;
    

    The only thing it does is to tail-call itself.

    So calling this function does not terminate:

    # diverge_silently ();;
      C-c C-cInterrupted.
    #
    

    Food for thought: what is the type of diverge_silently and why does this type makes sense?

  • For another example, consider the following recursive OCaml function:

    let diverge_noisily () =
      let rec loop n =
        let () = Printf.printf "%d bottles of beer on the wall\n" n in
        loop (n + 1)
      in let () = Printf.printf "1 bottle of beer on the wall\n" in
         loop 2;;
    

    When called, this function emits a message and calls loop, which emits another message and then tail-calls itself.

    So calling this function does not terminate:

    # diverge_noisily ();;
    1 bottle of beer on the wall
    2 bottles of beer on the wall
    3 bottles of beer on the wall
    4 bottles of beer on the wall
    5 bottles of beer on the wall
    6 bottles of beer on the wall
    7 bottles of beer on the wall
    ...
    

    Food for thought: what is the type of diverge_noisily and why does this type makes sense?

Evaluation might raise an error

Evaluating an expression might raise an error.

  • If the expression contains an explicit assertion that fails, then evaluating this expression raises an error.

    For example, consider the following OCaml function:

    let zero_or_one n =
      let () = assert (0 <= n && n <= 1) in
      if n = 0
      then "zero"
      else "one";;
    

    When this function is called on an integer which is not 0 or 1, its assertion fails and an error is raised:

    # zero_or_one 2 = "zero";;
    Exception: Assert_failure ("//toplevel//", 1969, -55).
    #
    

    Evaluating zero_or_one 2 does not complete, nor does evaluating zero_or_one 2 = "zero".

  • If the expression contains an implicit assertion that fails, then evaluating this expression also raises an error:

    # String.get "" 0;;
    Exception: Invalid_argument "index out of bounds".
    #
    
  • If the expression contains a division by 0, then evaluating this expression raises an error:

    # (1 / 0) = 0;;
    Exception: Division_by_zero.
    #
    

    Evaluating 1 / 0 does not complete, nor does evaluating (1 / 0) = 0.

  • The implementation of OCaml dedicates a memory area, the “runtime stack”, to manage nested function calls. Too many nested calls (typically recursive calls) can make this memory area overflow.

    • For example, consider the following recursive OCaml function:

      let rec overflow_silently () =
        not (overflow_silently ());;
      

    Applying this function induces unboundedly many non-tail calls, which makes the runtime stack overflow:

    # overflow_silently ();;
    Stack overflow during evaluation (looping recursion?).
    #
    

    Evaluating the expression overflow_silently () does not complete, nor does evaluating not (overflow_silently ()).

    • For another example, consider the following recursive OCaml function:

      let rec overflow_noisily () =
        let () = Printf.printf "Thanks for calling.\n" in
        not (overflow_noisily ());;
      

    Applying this function induces unboundedly many non-tail calls, which makes the runtime stack overflow:

    # overflow_noisily ();;
    Thanks for calling.
    Thanks for calling.
    Thanks for calling.
    ...
    Stack overflow during evaluation (looping recursion?).
    #
    

    Evaluating the expression overflow_noisily () does not complete, nor does evaluating not (overflow_noisily ()).

Evaluation might yield a value

Evaluating an expression might terminate and yield a value, possibly with side effects, e.g., emitting traces for calls and returns:

# twice_v1 3;;
- : int = 6
# traced_twice_v1 3;;
twice_v1 3 ->
  visit 3 ->
    visit 2 ->
      visit 1 ->
        visit 0 ->
        visit 0 <- 0
      visit 1 <- 2
    visit 2 <- 4
  visit 3 <- 6
twice_v1 3 <- 6
- : int = 6
#

Evaluating twice_v1 3 and traced_twice_v1 3 both complete, but the latter evaluation can be observed to emit a trace of its recursive calls and returns.

Pure and impure expressions

The present section is a reprise of the section with the same name in the mini-project about the underlying determinism of OCaml.

Just like an OCaml expression is a combination of its parts, evaluating an OCaml expression is a combination of evaluating its parts:

  • if evaluating each of these parts performs no observable side effects and yields a value, then this expression is pure;
  • otherwise, this expression is impure.

As a corollary, evaluating a pure expression always silently completes, i.e., (1) always yields a value, and (2) performs no observable side effects such as tracing.

Observational equivalence

Two expressions are observationally equivalent when evaluating them carries out the same computation, whether this computation diverges, raises an error, or yields a value, with or without observable side effects.

For example (see Exercise 08 below),

  • using an_int, the tracing identity function over integers from the mini-project about the underlying determinism of OCaml:

    let an_int n =
      let () = Printf.printf "processing %d...\n" n in
      n;;
    

    the two expressions an_int 1 + an_int 10 and an_int 10 + an_int 1 are not observationally equivalent because while evaluating them yields the same result, we observe that their traces (i.e., what is printed out in the course of evaluation) are not emitted in the same order:

    # an_int 1 + an_int 10;;
    processing 10...
    processing 1...
    - : int = 11
    # an_int 10 + an_int 1;;
    processing 1...
    processing 10...
    - : int = 11
    #
    
  • the two expressions 1 + 10 and 10 + 1 are observationally equivalent because evaluating them silently yields the same result.

The series of exercises

The exercises below invite us to ponder whether some OCaml expressions are equivalent. To answer this question, one should enumerate each of the possibilities, namely:

  • are both expressions pure?
    • if they always evaluate to the same result, then yes they are equivalent
    • if not, they are not equivalent (and one should give an example illustrating the difference)
  • is either pure but the other impure?
    • then they are not equivalent (and one should give an example illustrating the difference)
  • otherwise, if they are both impure,
    • if evaluating each of them diverges, then yes they are equivalent if they perform the same side effects (if any) in the same order; otherwise they are not equivalent
    • if evaluating each of them raises an error, then they are equivalent if it is the same error and if they perform the same side effects (if any) in the same order; otherwise they are not equivalent and one should give an example illustrating the difference
    • if evaluating each of them give rise to a sequence of side effects and yields a value, then they are equivalent if these sequences are the same and this value is the same too; otherwise they are not equivalent and one should give an example illustrating the difference

To prove that two expressions are not observationally equivalent, one example is enough.

Exercise 01

  1. For any OCaml name b of type bool, are the following expressions observationally equivalent?

    • b
    • if b then true else false
    • b = true

    Justify your answer.

  2. For any OCaml expression e of type bool, are the following expressions observationally equivalent?

    • e
    • if e then true else false
    • e = true

    Justify your answer.

Solution for Exercise 01

  1. First of all, we note that the 3 expressions are pure.

    Evaluating b, a name of type bool, yields its value in the current environment. This value is either true or false, a Boolean.

    To evaluate if b then true else false, we first evaluate b. Evaluating b, a name of type bool, yields its value in the current environment, a Boolean. If this value is true, evaluating if b then true else false reduces to evaluating true, which yields true. If this value is false, evaluating if b then true else false reduces to evaluating false, which yields false.

    To evaluate b = true, we first evaluate true, which yields true. We then evaluate b, a name of type bool, which yields its value in the current environment, a Boolean. If this value is true, evaluating b = true reduces to applying the equality predicate to true and true, which yields true. If this value is false, evaluating b = true reduces to applying the equality predicate to false and true, which yields false.

    To sum up, if b denotes true in the current environment, then evaluating each of the 3 expressions yields true, and if b denotes false in the current environment, then evaluating each of the 3 expressions yields false.

    Therefore, evaluating the 3 expressions always yield the same value, and since each of them is pure, they are observationally equivalent.

    Generalizing, and by the same argument, we observe that for any value v of type bool, the expressions v, if v then true else false, and v = true are equivalent.

  2. First of all, we note that the 3 expressions are pure if e is pure, and are impure if e is impure.

    We also note that evaluating each of the 3 expressions requires one to evaluate e first. So if evaluating e induces side effects, these side effects will take place in the same order.

    Evaluating e either diverges, or raises an error, or yields a value, possibly performing side effects in passing. This value is either true or false, a Boolean.

    • If evaluating e diverges, then evaluating each of the expressions diverges too, which leads us to conclude that in this case, they are observationally equivalent.
    • If evaluating e raises an error, then evaluating each of the expressions raises this error, which leads us to conclude that in this case, they are observationally equivalent.
    • If evaluating e yields a value, performing side effects in passing, then since e is first evaluated when evaluating e, if e then true else false, and e = true, these side effects occur in the same order. The rest of the evaluations takes place as just described in a., which leads us to conclude that in this case, they are observationally equivalent.
    • If evaluating e yields a value, performing no side effects in passing, then e is pure, which as just described in a., leads us to conclude that in this case, they are observationally equivalent.

    So in all cases, evaluating the 3 expressions gives rise to the same computation: these 3 expressions are observationally equivalent.

Exercise 02

  1. For any OCaml name b2 of type bool, are the following expressions observationally equivalent?

    • b2 || not b2
    • true

    Justify your answer.

  2. For any OCaml expression e of type bool, are the following expressions observationally equivalent?

    • e || not e
    • true

    Justify your answer.

Solution for Exercise 02

  1. First of all, we note that the two expressions are pure.

    Evaluating b2, a name of type bool, yields its value in the current environment. This value is either true or false, a Boolean.

    To evaluate b2 || not b2, we first evaluate b2. Evaluating b2, a name of type bool, yields its value in the current environment, a Boolean.

    • If this value is true, evaluating b2 || not b2 yields true since OCaml implements short-circuit evaluation, pertaining to the fact that true is absorbing for boolean disjunction (just like 0 for multiplication).
    • If this value is false, evaluating b2 || not b2 reduces to evaluating not b2. To evaluate not b2, we first evaluate b2. Evaluating b2, a name of type bool, yields its value in the current environment, a Boolean which we know to be false since we already tested it. We then evaluate not, which yields OCaml’s built-in negation function. Applying this negation function to false yields true.

    So in both cases, evaluating b2 || not b2 yields true.

    As for the second expression, evaluating true yields true.

    Therefore, evaluating either expression always yields the same value, and since each of these expressions is pure, they are observationally equivalent.

    Generalizing, and by the same argument, we observe that for any value v of type bool, the expressions v || not v and true are observationally equivalent.

  2. First of all, we note that the first expression is pure if e is pure and is impure if e is impure, and that the second expression is pure.

    Therefore the two expressions are not observationally equivalent, which we can illustrate with a tracing version of the identity function:

    let a_bool b =
      let () = Printf.printf "processing %B...\n" b in
      b;;
    

    Here are two examples:

    1. e is a_bool true:

      # a_bool true || not (a_bool true);;
      processing true...
      - : bool = true
      #
      

      which illustrates short-circuit evaluation in passing, and

    2. e is a_bool false:

      # a_bool false || not (a_bool false);;
      processing false...
      processing false...
      - : bool = true
      #
      

    In each of these two examples, evaluating e gives rise to a trace. In contrast, evaluating true gives rise to no traces:

    # true;;
    - : bool = true
    #
    

    So the expressions e || not e and true are not observationally equivalent.

Exercise 03

  1. For any OCaml expression e, are the following expressions observationally equivalent?

    • let x = e in 42
    • 42

    Justify your answer.

  2. For any OCaml expression e, are the following expressions observationally equivalent?

    • e * 0
    • let x = e in 0

    Justify your answer.

Exercise 04

  1. For any OCaml expression e, are the following expressions observationally equivalent?

    • let x = e in x
    • e

    Justify your answer.

  2. For any OCaml expressions e1, e2, and e3, are the following expressions observationally equivalent?

    • let x2 = (let x1 = e1 in e2) in e3
    • let x1 = e1 in let x2 = e2 in e3

    Justify your answer.

Solution for Exercise 04

  1. First of all, we note that evaluating each of the two expressions requires e to be evaluated first. So if evaluating e induces side effects, these side effects will take place in the same order.

    So evaluating let x = e in x requires one to first evaluate e. If this evaluation yields a value, then evaluating let x = e in x reduces to evaluating x in an environment where x denotes this value.

    So in both cases, the evaluation proceeds in the same way and yields the same result: the two expressions are observationally equivalent.

  2. Evaluating let x2 = (let x1 = e1 in e2) in e3 in an environment G requires one to first evaluate let x1 = e1 in e2 in G, which requires one to first evaluate e1 in G. Assuming that evaluating e1 yields a value v1, e2 is then evaluated in an extension of G that binds x1 to v1 (let us name it G1). Assuming that evaluating e2 yields a value v2, e3 is then evaluated in an extension of G that binds x2 to v2.

    Evaluating let x1 = e1 in let x2 = e2 in e3 in the same environment G requires one to first evaluate e1 in G. Assuming that evaluating e1 yields a value v1 (which can only be the same value as above, due to the determinism of OCaml’s evaluation), let x2 = e2 in e3 is then evaluated in an extension of G that binds x1 to v1 (let us name it G1 too). Evaluating let x2 = e2 in e3 in G1 requires one to first evaluate e2 in G1. Assuming that evaluating e2 yields a value v2 (which again can only be the same value as above, due to the determinism of OCaml’s evaluation), e3 is then evaluated in an extension of G1 that binds x2 to v2.

    Therefore in the first case, e3 is evaluated in an extension of G that binds x2 to v2 and in the second case, e3 is evaluated in G1 that binds x2 to v2. So if e3 contains x1 and x1 is evaluated in the course of evaluating e3, the result of evaluation will differ: these two expressions are not observationally equivalent.

    To wit, let x1 denote 100 in the global environment, let e1 be 0, e2 be x1 + 10, and e3 be x1 + x2:

    # let x1 = 100;;
    val x1 : int = 100
    # let x2 = (let x1 = 0 in x1 + 10) in x1 + x2;;
    - : int = 110
    # let x1 = 0 in let x2 = x1 + 10 in x1 + x2;;
    - : int = 10
    #
    

    If x1 does not occur in e3 or if x1 occurs in e3 but is not evaluated in the course of evaluating e3, the two expressions are observationally equivalent.

Exercise 05

  1. For any OCaml expression e, are the following expressions observationally equivalent?

    • not e
    • if e then false else true

    Justify your answer.

  2. For any OCaml expressions e1, e2, and e3, are the following expressions observationally equivalent?

    • if e1 then e2 else e3
    • if not e1 then e3 else e2

    Justify your answer.

  3. For any OCaml expressions e1, e2, e3, e4, and e5, are the following expressions observationally equivalent?

    • if (if e1 then e2 else e3) then e4 else e5
    • if e1 then (if e2 then e4 else e5) else (if e3 then e4 else e5)

    Justify your answer.

Mechanical solution for Exercise 05.b

Pablito (out of breath, having ran all the way from the Mechanical solution for Exercise 09): I can do this, I can do this. First, the generic description of how conditional expressions are evaluated:

  • to evaluate if e1 then e2 else e3, we first evaluate e1;
    • if evaluating e1 does not terminate, then evaluating if e1 then e2 else e3 does not terminate either;
    • if evaluating e1 raises an error, then evaluating if e1 then e2 else e3 also raises this error;
    • if evaluating e1 yields a Boolean, then
      • if this Boolean is true, then evaluating if e1 then e2 else e3 reduces to evaluating e2, and
      • if this Boolean is false, then evaluating if e1 then e2 else e3 reduces to evaluating e3;

Pablito (thinking aloud): Lucky me, the names are the same in the description and in the first expression. Now for instantiating this description with the second expression: e1 is not e1, e2 is e3, and e3 is e2. Easy peasy:

  • to evaluate if not e1 then e3 else e2, we first evaluate not e1;
    • if evaluating not e1 does not terminate, then evaluating if not e1 then e3 else e2 does not terminate either;
    • if evaluating not e1 raises an error, then evaluating if not e1 then e3 else e2 also raises this error;
    • if evaluating not e1 yields a Boolean, then
      • if this Boolean is true, then evaluating if e1 then e3 else e2 reduces to evaluating e3, and
      • if this Boolean is false, then evaluating if e1 then e3 else e2 reduces to evaluating e2;

Pablito (vocalizing his train of thoughts): The generic description of how applications are evaluated, trimming it to the fact that we are applying a predefined function:

  • to evaluate e0 e1, we first evaluate e0 and e1:
    • if either of these first evaluations does not terminate, then evaluating e0 e1 does not terminate;
    • if either of these first evaluations raises an error, then evaluating e0 e1 also raises this error; and
    • if e0 and e1 evaluate to v0 and v1, then since OCaml is statically typed, v0 denotes a function; evaluating e0 e1 reduces to applying this function to v1;
      • if this function was predefined, applying it to v1 triggers its computation:
        • if this computation does not terminate, then evaluating e0 e1 does not terminate either;
        • if this computation raises an error (e.g., a division by 0), then evaluating e0 e1 also raises this error; and
        • if this computation yields a value, then evaluating e0 e1 also yields this value;

Pablito (celeritous): Now for instantiating this description: e0 is not and e1 is e1. Again, easy peasy:

  • to evaluate not e1, we first evaluate not and e1; not is pure and so it evaluates to a value, which is a predefined function that negates its argument:
    • if evaluating e1 does not terminate, then evaluating not e1 does not terminate;
    • if evaluating e1 raises an error, then evaluating not e1 also raises this error; and
    • if evaluating e1 completes, it yields a value, ah, let me call it b1, which is a Boolean since OCaml is statically typed; evaluating not e1 reduces to applying the negation function to b1;

Pablito (continuing): So, splicing this instantiated description of applications in the previous instantiation of conditional expressions:

  • to evaluate if not e1 then e3 else e2, we first evaluate not e1;
    • if evaluating e1 does not terminate, then evaluating if not e1 then e3 else e2 does not terminate either;
    • if evaluating e1 raises an error, then evaluating if not e1 then e3 else e2 also raises this error;
    • if evaluating not e1 yields a Boolean, then
      • if this Boolean is true, then evaluating if e1 then e3 else e2 reduces to evaluating e3, and
      • if this Boolean is false, then evaluating if e1 then e3 else e2 reduces to evaluating e2;

Pablito (reasoning): So all in all, in both cases, e1 is evaluated, and

  • if e1 evaluates to true, evaluating if e1 then e2 else e3 reduces to evaluating e2 and evaluating if not e1 then e3 else e2 reduces to evaluating e2, whereas
  • if e1 evaluates to false, evaluating if e1 then e2 else e3 reduces to evaluating e3 and evaluating if not e1 then e3 else e2 reduces to evaluating e3.

Pablito (concluding): So the two expressions are observationally equivalent.

Anton (who sedately arrived in the meanwhile): Yup. Awesome.

Pablito (beaming): Yes.

Anton: To solve Part c. of this exercise, however, the names in the abstract description are really clashing with the names in the concrete expressions.

Pablito (radical): Let me capitalize them:

  • to evaluate if E1 then E2 else E3, we first evaluate E1;
    • if evaluating E1 does not terminate, then evaluating if E1 then E2 else E3 does not terminate either;
    • if evaluating E1 raises an error, then evaluating if E1 then E2 else E3 also raises this error;
    • if evaluating E1 yields a Boolean, then
      • if this Boolean is true, then evaluating if E1 then E2 else E3 reduces to evaluating E2, and
      • if this Boolean is false, then evaluating if E1 then E2 else E3 reduces to evaluating E3;

Anton: That lifts the confusion indeed.

Peter Landin: It does, doesn’t it?

Mimer: Prof. Landin! Thank you so much for coming by! Er, would you mind autographing my copy of “The Mechanical Evaluation of Expressions”? Do forgive its worn-out appearance, it’s all the well thumbing.

Peter Landin: Sure. I’m not the Beatles, though.

Olin Shivers: Mine too?

Exercise 06

  1. Simplify the following OCaml expression (and justify your simplification):

    if (if b || not b then true else false) then true else false
    

    where b is an OCaml name.

  2. Simplify the following OCaml expression (and justify your simplification):

    if b then b else b
    

    where b is an OCaml name.

  3. Simplify the following OCaml expression (and justify your simplification):

    if e then e else e
    

    where e is an OCaml expression.

  4. Are the following two expressions observationally equivalent?

    • let x1 = e1 and x2 = e2 in x1 - x2
    • let x2 = e2 and x1 = e1 in x2 - x1

    where e1 and e2 are OCaml expressions.

Exercise 07

Given any OCaml expressions e1, e2, and e3, are the following two expressions observationally equivalent?

  • (e1 + e2) + e3
  • e1 + (e2 + e3)

Solution for Exercise 07

The question is whether + is associative in OCaml.

Reminders:

  • for any expressions e0 and e1, evaluating e0 e1 requires first evaluating e1;

    if evaluating e1 completes, it yields a value v1 (let us say of type t1, for some t1),

    then evaluating e0 e1 requires evaluating e0;

    if evaluating e0 completes, it yields a value v0, which is a function (let us say of type t1 -> t2, for some t2), due to the typing constraints of OCaml;

    evaluating e0 e1 then reduces to applying v0 to v1;

    if this application completes, it yields a value v2 of type t2, due to the typing constraints of OCaml;

  • for any expressions e1 and e2, e1 + e2 is syntactic sugar for Int.add e1 e2, or more precisely for (Int.add e1) e2 since application associates to the left;

So evaluating e1 + e2 takes the same steps as evaluating (Int.add e1) e2:

  • evaluating (Int.add e1) e2 requires first evaluating e2;

    if evaluating e2 completes, it yields a value v2, which is of type int due to the typing constraints of OCaml

    then evaluating (Int.add e1) e2 requires evaluating Int.add e1;

    evaluating Int.add e1 requires first evaluating e1;

    if evaluating e1 completes, it yields a value v1, which is of type int due to the typing constraints of OCaml;

    then evaluating Int.add e1 requires evaluating Int.add;

    evaluating Int.add yields the value this name denotes in the current environment; this value is a function of type int -> int -> int due to the typing constraints of OCaml;

    evaluating Int.add e1 then reduces to applying this function to v1; the result is a function of type int -> int, due to the typing constraints of OCaml;

    evaluating (Int.add e1) e2 then reduces to applying this function to v2; the result is a value of type int, due to the typing constraints of OCaml; more precisely, this value is the result of adding v1 to v2, because the function denoted by Int.add implements the addition function.

Here is a simplified version:

  • evaluating e1 + e2 requires first evaluating e2;

    if evaluating e2 completes, it yields a value v2, which is of type int, due to the typing constraints of OCaml;

    then evaluating e1 + e2 requires evaluating e1;

    if evaluating e1 completes, it yields a value v1, which is of type int, due to the typing constraints of OCaml;

    evaluating e1 + e2 then reduces to adding v1 to v2; the result is a value of type int, due to the typing constraints of OCaml.

Let us use this simplified version to answer the question:

  • Evaluating (e1 + e2) + e3 requires first evaluating e3. If evaluating e3 completes, it yields a value v3, which is an integer, due to the typing constraints of OCaml. Then evaluating (e1 + e2) + e3 requires evaluating e1 + e2. Evaluating e1 + e2 requires first evaluating e2. If evaluating e2 completes, it yields a value v2, which is an integer due to the typing constraints of OCaml. Then evaluating e1 + e2 requires evaluating e1. If evaluating e1 completes, it yields a value v1, which is an integer due to the typing constraints of OCaml. Evaluating e1 + e2 then reduces to adding v1 and v2. The result is a value v12, which is an integer due to the typing constraints of OCaml. Evaluating (e1 + e2) + e3 then reduces to adding v12 to v3; the result is a value v12_3, which is an integer due to the typing constraints of OCaml.
  • Evaluating e1 + (e2 + e3) requires first evaluating e2 + e3. Evaluating e2 + e3 requires first evaluating e3. If evaluating e3 completes, it yields a value v3, which is an integer due to the typing constraints of OCaml. Then evaluating e2 + e3 requires evaluating e2. If evaluating e2 completes, it yields a value v2, which is an integer due to the typing constraints of OCaml. Evaluating e2 + e3 then reduces to adding v2 and v3. The result is a value v23, which is an integer due to the typing constraints of OCaml. Then evaluating e1 + (e2 + e3) requires evaluating e1. If evaluating e1 completes, it yields a value v1, which is an integer due to the typing constraints of OCaml. Evaluating e1 + (e2 + e3) then reduces to adding v1 and v23. The result is a value v1_23, which is an integer due to the typing constraints of OCaml.

To sum up, in both cases,

  1. e3 is evaluated; if this evaluation completes,
  2. e2 is evaluated; if this evaluation completes,
  3. e1 is evaluated,

in that order. So if computational effects take place (from tracing to raising an error to diverging), they do in the same order. If the three evaluations complete, they give rise to the values v1, v2, v3. Evaluating the first expression gives rise to the addition (v1 + v2) + v3, and evaluating the first expression gives rise to the addition v1 + (v2 + v3), which yield the same result because addition is associative.

Therefore yes, the two expressions are observationally equivalent: + is associative in OCaml.

Exercise 08

Given any OCaml expressions e1 and e2, are the following two expressions observationally equivalent?

  • e1 + e2
  • e2 + e1

Solution for Exercise 08

The question is whether + is commutative in OCaml.

The following answer is based on the reminders listed in Solution for Exercise 07:

  • Evaluating e1 + e2 requires first evaluating e2. If evaluating e2 completes, it yields a value v2, which is an integer due to the typing constraints of OCaml. Then evaluating e1 + e2 requires evaluating e1. If evaluating e1 completes, it yields a value v1, which is an integer due to the typing constraints of OCaml. Evaluating e1 + e2 then reduces to adding v1 and v2. The result is a value v12, which is an integer due to the typing constraints of OCaml.
  • Evaluating e2 + e1 requires first evaluating e1. If evaluating e1 completes, it yields a value v1, which is an integer due to the typing constraints of OCaml. Then evaluating e2 + e1 requires evaluating e2. If evaluating e2 completes, it yields a value v2, which is an integer due to the typing constraints of OCaml. Evaluating e2 + e1 then reduces to adding v2 and v1. The result is a value v21, which is an integer due to the typing constraints of OCaml.

To sum up,

  • in the first case, e2 is evaluated and then e1 is evaluated; and
  • in the second case, e1 is evaluated and then e2 is evaluated.

So if computational effects take place (from tracing to raising an error to diverging), they do in a different order.

Therefore no, the two expressions are not observationally equivalent: + is not commutative in OCaml over arbitrary expressions. For a simple example that uses an_int, the tracing identity function over integers:

# an_int 1 + an_int 10;;
processing 10...
processing 1...
- : int = 11
# an_int 10 + an_int 1;;
processing 1...
processing 10...
- : int = 11
#

The two traces are not the same.

That said, if e1 and e2 are pure expressions, then it does not matter in which order they are evaluated, and the two expressions are observationally equivalent: + is commutative in OCaml over pure expressions.

Exercise 09

Given an OCaml expression e and a pure OCaml expression pe, are the following two expressions observationally equivalent?

  • e + pe
  • pe + e

Mechanical solution for Exercise 09

Anton: Mechanical now.

Alfrothul: I am tempted to think so.

Anton: Pray elaborate.

Dana: May I, Alfrothul?

Alfrothul: By all means.

Dana: The lecture notes so far have a common thread, namely abstracting concepts and instantiating them. Concepts are abstracted by parameterizing them, and then they are instantiated by supplying concrete arguments for these parameters.

Alfrothul: For example, functions are parameterized expressions.

Dana: And structurally recursive functions are abstracted into fold or parafold functions.

Anton: OK.

Dana: But it’s not just that. For example, in a grammar, non-terminals are instantiated to construct an abstract-syntax tree, and likewise, proof trees are constructed by instantiating proof rules.

Alfrothul: In the same vein, the characteristic equations of the S-m-n theorem are instantiated to account for specific situations, e.g., to specialize an interpreter.

Anton: All right, all right, I get it. Now what is generic here that you want to instantiate to solve Exercise 09 mechanically?

Dana: The evaluation of OCaml expressions.

Anton: Now what.

Alfrothul: Look at the beginning of the Solution for Exercise 08. It reminds us of the process of evaluating an addition in OCaml.

Anton: OK, I see that:

  • Evaluating e1 + e2 requires first evaluating e2. If evaluating e2 completes, it yields a value v2, which is an integer, due to the typing constraints of OCaml. Then evaluating e1 + e2 requires evaluating e1. If evaluating e1 completes, it yields a value v1, which is an integer, due to the typing constraints of OCaml. Evaluating e1 + e2 then reduces to adding v1 and v2. The result is a value v12 of type int, due to the typing constraints of OCaml.

Dana: This description is parameterized by e1 and e2.

Alfrothul: All that we need to do is to instantiate it with e and pe, and then with pe and e.

Dana: Then we can simplify the instantiations.

Alfrothul: And then we can compare the result of the simplifications.

Anton: Interesting. Let me try for e and pe:

  • Evaluating e + pe requires first evaluating pe. If evaluating pe completes, it yields a value, ah, let me call it n1, which is an integer, due to the typing constraints of OCaml. Then evaluating e + pe requires evaluating e. If evaluating e completes, it yields a value, ah, let me call it n2, which is an integer, due to the typing constraints of OCaml. Evaluating e + pe then reduces to adding n2 and n1. The result is a value, ah, let me call it n, of type int, due to the typing constraints of OCaml.

Wow, this specific description of how e + pe is evaluated can indeed be simplified. For example, pe is pure, so evaluating it always completes and makes no side effects. Let’s see:

  • Evaluating e + pe requires first evaluating pe. Since pe is pure, its evaluation completes and makes no side effects. This evaluation yields an integer n1, due to the typing constraints of OCaml. Then evaluating e + pe requires evaluating e. If evaluating e completes, it yields a value, ah, let me call it n2, which is an integer, due to the typing constraints of OCaml. Evaluating e + pe then reduces to adding n2 and n1. The result is an integer.

Pablito: The same goes for pe and e:

  • Evaluating pe + e requires first evaluating e. If evaluating e completes, it yields a value, ah, let me call it n2 for compatibility with your description, which is an integer due to the typing constraints of OCaml. Then evaluating pe + e requires evaluating pe. If evaluating pe completes, it yields a value, ah, let me call it n1, again for compatibility with your description, which is an integer due to the typing constraints of OCaml. Evaluating pe + e then reduces to adding n1 and n2. The result is a value, ah, let me call it n, of type int due to the typing constraints of OCaml.

OK, similar simplifications can take place:

  • Evaluating pe + e requires first evaluating e. If evaluating e completes, it yields a value, ah, let me call it n2, which is an integer due to the typing constraints of OCaml. Then evaluating pe + e requires evaluating pe. Since pe is pure, its evaluation completes and makes no side effects. This evaluation yields an integer n1, due to the typing constraints of OCaml. Evaluating pe + e then reduces to adding n1 and n2. The result is an integer.

Alfrothul: So in both cases, e is evaluated, which can be observed if it is impure. Otherwise, pe is evaluated, which cannot be observed since it is pure. And addition takes place, a pure operation that cannot be observed.

Dana: The only difference is that in one case, n2 and n1 are added, and in the other, n1 and n2 are added. Since addition is commutative, the result is the same.

Anton: So observably, the evaluations proceed in the same way and the result is the same.

Dana: Yes. The two expressions are observationally equivalent.

Alfrothul: And the argument was mechanical.

Pablito (leaving on a run): That was awesome. Thanks, guys!

Anton: Where is he going?

Dana (smiling): To Exercise 05.

Resources

Version

Changed the non-terminal for pure expressions to be pe and expanded Exercise 06 [04 Mar 2023]

Created [10 Jan 2023]