CPS 343/543 Lecture notes: Strong typing, type inference, higher-order functions, and currying



Strong typing

  • definitions vary
  • a simple, but incomplete definition is: `a strongly-typed programming language is one in which each name in a program has a single type associated with it, and that type can be determined at compile time' [COPL6] (i.e., by looking at the program) not running it; this means that all types are bound at compile-time (i.e., statically bound)
  • another, perhaps more general and all inclusive, definition is: a strongly-typed programming language is one in which `type errors are always detected' [COPL9]; means we must be able to determine the type of everything just by looking at the program, and not by running it
  • FORTRAN, C, and C++ are not strongly typed
    • languages with coercion (e.g., FORTRAN, C, and C++ are weakly typed)
    • C and C++ use coercion, int x = 3.4;
    • C and C++ support unions which are not type checked
  • Ada is almost strongly-typed; the programmer can suspend type checking
  • C# and Java (even though they are based on C/C++) are strongly typed but have casting
  • ML and Haskell are strongly-typed
  • in safety, Haskell is the Java of functional programming languages
  • why use a strongly-typed programming language? reliability and safety
  • why use a weakly-typed programming language? flexibility and efficiency
  • LISP is said to be a typeless language

  • ML comes with irremovable training wheels
      3-4; (* works *)
      
      (* 3-4.4; doesn't work *)
      
      (* no coercion in ML, why? *)
      
      real(3)-4.4; (* this is a type cast *)
      
      open Real;
      Real.< (2.9, 3.0); (* Real is called a structure; open a class in OO *)
      
      false andalso (1 / 0); (* doesn't work, why? *)
      false andalso (1 div 0); (* still doesn't work, but not because of a divide by 0 *)
      


Type inference in Haskell

    ``In Haskell every expression must have a type, which is calculated prior to evaluating the expression by a process called type inference. The key to this process is a typing rule for function application, which states that if f is a function that maps arguments of type A to results of type B, and e is an expression type A, then the application f e has type B:

         f :: AB       e :: A

                 f e :: B

    For example, the typing not False :: Bool can be inferred from this rule using the fact that not :: Bool → Bool and False :: Bool'' [PIH].


Type inference in ML

  • ML and Haskell use type inference to help relieve the programmer from associating a type with every name in a program
  • there is a built-in type inference engine (algorithm) to determine the type of a name based on the context (see inference rules in handout)
  • ML and Haskell do type inference
  • in what places in a program can we attached a type to a name? primarily:
    • variables
    • function parameters
    • return types
  • first let us see how to associate types with names (which to this point we have not done)
      - fun square (n) = n*n;
      val square = fn : int -> int
      -
      - fun square (n: real) = n*n;
      val square = fn : real -> real
      -
      - fun square (n): real = n*n;
      val square = fn : real -> real
      -
      - fun square (n: real): real = n*n;
      val square = fn : real -> real
      
  • key point: type inference gives us strong typing without explicit type declarations
  • for more information, see [EMLP] p. 63


How ML deduces types: examples

    (* the 0 returned in the first case
    causes ML to use the type
    "int list -> int" for summing *)
    fun summing (nil) = 0
    |   summing (x::xs) = x + summing (xs);
    
    fun f1 (a, b) = if (a < b) then 3.0 else 2.0;
    (* val f1 = fn : int * int -> real *)
    
    (*
    fun f2 (a, b) = if (a < b) then 3.0 else 2;
    stdIn:7.17-7.43 Error: types of if branches do not agree [literal]
      then branch: real
      else branch: int
      in expression:
        if a < b then 3.0 else 2
    *)
    
    fun f3 (a, b) = if (a + 0.0) < b then 1 else 2;
    (* val f1 = fn : real * real -> int *)
    
    See [EMLP] §3.2.4 (pp. 63-64) for more information.


Currying

  • based on Kleene's Smn theorem (also called the translation lemma, parameter theorem, or parameterization theorem) from computability theory
  • formally, states that for any function f (x1, x2, ..., xn),

    f (a1, a2, ..., am) = g (xm+1, xm+2, ..., xn), where m < n, such that g (am+1, am+2, ..., an) = f (a1, a2, ..., am, am+1, am+2, ..., an).


Curried form in Haskell

    -- notice anything interesting?
    
    Prelude> :t map
    map :: (a -> b) -> [a] -> [b]
    Prelude> :t foldl
    foldl :: (a -> b -> a) -> a -> [b] -> a
    Prelude> :t foldr
    foldr :: (a -> b -> b) -> b -> [a] -> b
    Prelude> 
    
    pow (0, _) = 1
    pow (1, b) = b
    pow (_, 0) = 0
    pow (e, b) = b * pow (e-1, b)
    
    powc 0 _ = 1
    powc 1 b = b
    powc _ 0 = 0
    powc e b = b * powc (e-1) b
    
    Main> :t pow
    pow :: (Num a, Num b) => (b,a) -> a
    Main> :t powc
    powc :: (Num a, Num b) => b -> a -> a
    Main>
    
    square = powc 2
    cube = powc 3
    
  • parenthesizing an operator converts it from an infix to prefix operator:
    -- called a 'section'
    inc = (+) 1
    inc1 = (+) 1.0
    
    Main> :t inc
    inc :: Integer -> Integer
    Main> :t inc1
    inc1 :: Double -> Double
    Main>
    
    -- courtesy [PIH] p. 118
    -- currying obviates the need for
    -- the helper function insertineach
    powerset [] = [[]]
    powerset (x:xs) =
       let
          temp = powerset xs
       in
          (map (x:) temp) ++ temp
    
  • functions curry and uncurry


Curried form in ML

    val ourimplode = foldr (op ^) "" o (map str);
    (* notice something peculiar about foldr and map *)
    
    fun pow 0 _ = 1
    |   pow 1 b = b
    |   pow _ 0 = 0
    |   pow e b = b * pow (e-1) b;
    
    (*
    int * int -> int
    =>
    int -> int -> int
     *)
    
    val square = pow 2;
    val cube = pow 3;
    
  • map, foldl, are foldr are defined in curried form (e.g., see definition of ourimplode above)
  • see [EMLP] Chapter 5 and, specifically §5.5 (pp. 168-173), for more information.


Variable-length argument lists in Scheme

Arguments to any function are always passed in as list. It is up to the programmer to decompose that argument list and group individual arguments in the formal parameter specification of the function definition using dot notation, if necessary.
    (define f (lambda (x) x))
    
    (f 1)
    (f '(1 2 3))
    
    ;;; x is just the list (1 2 3)
    (define f (lambda x x))
    
    (f 1 2 3)
    (f 1)
    
    ;;; uses pattern matching like ML and Haskell
    ;;; g and h take a variable number of arguments
    (define g (lambda (x . y) x))
    (define h (lambda (x . xs) xs))
    
    ;;; only 1 argument passed
    (g '(1 2 3))
    (h '(1 2 3))
    (write "a")
    (newline)
    
    ;;; now 2 arguments passed
    (g 1 '(2 3))
    (h 1 '(2 3))
    
    ;;; now 3 arguments passed
    (g 1 2 3)
    (h 1 2 3)
    


Currying in Scheme

  • any language with first-class closures can be used to define functions in curried form
  • first attempt:
    ;;; this is called `partial argument application'
    
    ;;; this example of currying is too tightly woven
    ;;; to the function being specialized
    ;;; moreover, pow now cannot be completely evaluated
    (define pow
      (lambda (e)
        (cond
          ((eqv? e 0) 1)
          (else
           ;; return a closure
           (lambda (b)
             (cond
               ((eqv? b 0) 0)
               ((eqv? b 1) 1)
               ((eqv? e 1) b)
               (else (* b ((pow (- e 1)) b))))))))) 
    
    > (define square (pow 2))
    > (square 4)
    16
    
  • second attempt:
    (define s11
      (lambda (f x)
        (list 'lambda '(y) (list f x 'y))))
    
    (define pow
      (lambda (e b)
        (cond
          ((eqv? b 0) 0)
          ((eqv? b 1) 1)
          ((eqv? e 0) 1)
          ((eqv? e 1) b)
          (else (* b (pow (- e 1) b))))))
    
    > (define square (s11 pow 2))
    > square
    (lambda (y) (#<procedure:pow> 2 y))
    > (eval square)
    #<procedure>
    > ((eval square) 4)
    16
    
  • a similar approach:
    ;;; this is called `generalized explicit currying'
    
    ;;; notice we must explicitly call curry which, in turn,
    ;;; means we must know a-priori how many arguments are contained
    ;;; in the complete argument list of the curried function
    
    ;;; however, this approach obviates the need explicitly to call eval and
    ;;; the need define multiple versions of s (e.g., to curry a 3 argument
    ;;; function in all ways possible would require s111, s12, and s21).
    (define curry
      (lambda (fun . args)
        (lambda x
          (apply fun (append args x)))))
    
    (define square (curry pow 2))
    
    > (square 4)
    16
    
    ;; examples courtesy Jeffrey A. Meunier's article `Function Currying in Scheme'
    
    ;; and we can continue to currying the result as we desire
    (define list1 (curry list 'A))
    (define list2 (curry list1 'tree))
    (define list3 (curry list2 'grows))
    
    (list3 'in 'Brooklyn.)
    
    ;; since the original function of the curried function (which is list)
    ;; can accept multiple arguments,
    ;; the curried function can also accept multiple arguments
    
    (define list4 (curry list 'A 'tree 'grows))
    (list4 'in 'Brooklyn.)
    
  • see Jeffrey A. Meunier's article Function Currying in Scheme for the details of implicit currying


Partial evaluation

Generalizes the idea of currying from any prefix of the argument list to any subset of the argument list.

Formally, states that for any function f(x1, x2, ..., xn),

f(a, b, ..., r) = g({x1, x2, ..., xn} - {a, b, ..., r}), where {a, b, ..., r} ⊆ {x1, x2, ..., xn}, such that g(s, t, ..., z) = f(a, b, ..., z).


References

    [COPL6] R.W. Sebesta. Concepts of Programming Languages. Addison-Wesley, Boston, MA, Sixth edition, 2003.
    [COPL9] R.W. Sebesta. Concepts of Programming Languages. Addison-Wesley, Boston, MA, Ninth edition, 2010.
    [EMLP] J.D. Ullman. Elements of ML Programming. Prentice Hall, Upper Saddle River, NJ, Second edition, 1997.
    [PIH] G. Hutton. Programming in Haskell. Cambridge University Press, Cambridge, 2007.
    [PLPP] K.C. Louden. Programming Languages: Principles and Practice. Brooks/Cole, Pacific Grove, CA, Second edition, 2002.
    [SICP] H. Abelson and G.J. Sussman. Structure and Interpretation of Computer Programs. MIT Press, Cambridge, MA, Second edition, 1996.

Return Home