Search code examples
pattern-matchingsmlml

destructuring tuple with let in ML


In sml can I use a tuple in a let ? If so what would the syntax be? I can require this as concentric pattern matches, but it seems there should be a less boilerplate way. In the let below, I'd like to bind v1 and v2 to the two values of the tuple returned from calling interpExp. Then I'd like to call interpExp with one of those values to get two more values.

fun performOp (e1,op,e2,table) = 
    let val (v1,t1) = interpExp(e1,table)   (* interpExp returns 2-tuple, bind v1 and t2 to those two values *)
        and val (v2,t2) = interpExp(e1,t1)  (* use t1 which was bound on previous line *)
        and val v3 = (case op of
                           Plus => v1 + v2
                     | Minus => v1 - v2
                     | Times => v1 * v2
                     | Div => v1 / v2)
    in (v3,t2)
    end

On further trial and error, it seems the second and third val are unnecessary, and thereafter it seems the v1 and v2 are not in scope for the next clause.

fun performOp (e1,op,e2,table) = 
    let val (v1,t1) = interpExp(e1,table) 
        and (v2,t2) = interpExp(e1,t1)  (* oops  t1, not in scope *)
        and v3 = (case op of
                           Plus => v1 + v2
                     | Minus => v1 - v2
                     | Times => v1 * v2
                     | Div => v1 / v2)
    in (v3,t2)
    end

On further experimentation I've discovered yet another syntax, but I'm not sure what the difference is, i.e., using val rather than and in the let.

fun performOp (e1,op,e2,table) = 
    let val (v1,t1) = interpExp(e1,table) 
        val (v2,t2) = interpExp(e1,t1)  (* oops  t1, not in scope *)
        val v3 = (case op of
                           Plus => v1 + v2
                     | Minus => v1 - v2
                     | Times => v1 * v2
                     | Div => v1 / v2)
    in (v3,t2)
    end

Solution

  • Generally speaking, the and keyword is used in SML for introducing multiple mutually recursive functions or datatypes. For example, we could define gratuitously inefficient parity checking as

    fun isEven 0 = true
      | isEven 1 = false
      | isEven n = isOdd (n - 1)
    
    and isOdd 0 = false
      | isOdd 0 = true
      | isOdd n = isEven (n - 1)
    

    And an example with datatypes:

    datatype 'a exposed = Exposed of 'a * 'a stream
    and      'a stream  = Stream of unit -> 'a exposed
    

    However, and is also supported (I presume for consistency's sake?) in some other contexts, where it merely has the effect of introducing all the bindings tied together simultaneously. For instance, in

    val x = 7
    and y = 8
    

    both x and y are introduced at once. To now directly address your immediate examples:

    1. The first one you present will encounter a syntax error because it ought to be and instead of val and
    2. The second one you present introduces all of those bindings at the same time, and thus you cannot rely on t1 in the second---it has not yet been introduced.
    3. The final one is correct with respect to using val

    However, all of these also suffer from syntax errors because op is also a reserved word (it's principally used for allowing infix functions to be treated as prefix functions, like in val sum = foldr op+ 0). If you change this, leaving you with something like

    fun performOp (e1, binop, e2, table) = 
        let val (v1, t1) = interpExp (e1, table) 
            val (v2, t2) = interpExp (e1, t1)
            val v3 = (case binop of
                       Plus  => v1 + v2
                     | Minus => v1 - v2
                     | Times => v1 * v2
                     | Div   => v1 / v2)
        in (v3,t2)
        end
    

    then it should work as intended. I should also note that / is used for division on reals, so if you intend to work with ints instead, it ought to be div (also infix).